개발/개발서적

클린코드 리뷰

devriver 2022. 5. 22. 22:31
<클린코드 애자일 소프트웨어 장인 정신> ch 06. 객체와 자료 구조 읽고 정리한 글입니다.

private variable를 사용하는 이유

A 클래스의 public member 변수 m이 있다고 가정해보자. 변수 m은 public이므로 B 클래스에서 접근할 수 있다.

B 클래스에서 변수 m을 사용하고 있는 상황에서 A 클래스의 변수 m의 타입이나 이름 등을 변경하면 어떻게 될까?

우리는 B 클래스 내에서 변수 m을 사용하고 있는 부분을 수정해야한다. 또한, 변수 m을 사용하고 있을지도 모르는 다른 클래스들도 찾아봐야 한다. 만약 member 변수 m이 private 변수라면 어떨까? 변경의 여파를 신경쓸 필요 없이 변수 m의 타입과 이름을 맘대로 바꿀 수 있다. 만약 버그가 생긴다고 해도 우리는 A 클래스의 구현 위주로 살펴보면 된다.

 

그렇다면 왜 get, set 함수는 당연하게 public으로 하여 private variables를 외부에 노출하는 걸까? 답은 천천히..

 

자료 추상화

아래 두 클래스를 통해 자료 추상화 개념을 익혀보자

public class Point1 {
  public double x;
  public double y;
}

public interface Point2 {
  double getX();
  double getY();
  void setCartesian(double x, double y);
  double getR();
  double getTheta();
  void setPolar(double r, double theta);
}

두 클래스는 모두 2차원 점을 표현하는 클래스지만, Point1은 구현을 외부로 노출하고 Point2는 구현을 숨긴다는 차이점이 있다.

 

Point2 인터페이스를 좀 더 살펴보면 setCartesian, setPolar 메서드가 있다. 실제로 Point2가 Cartesian(직교) 좌표계로 사용할지 Polar(극) 좌표계로 사용할지는 알 수 없지만 인터페이스는 x, y, r, theta를 가진 자료구조임을 명백히 알 수 있다.

또한, Point2는 x, y, r, theta의 get, set을 강제한다. 조회(get)은 개별 값으로만 가능하고 설정(set)은 x,y 쌍, r, theta 쌍으로만 할 수 있도록 강제한다.

 

반면 Point1은 x, y를 가진 직교 좌표계임이 명백하다. 또한 개별적으로 좌표값을 읽고 설정하도록 강제한다. 변수를 private로 변경하더라도 각 값마다 get, set 메서드를 추가한다면 구현을 외부로 노출한다는 건 동일하다.

밑줄 친 문장을 이해 못한 사람을 위해 자세히 설명하자면, 변수를 get, set 함수를 추가해서 다룬다고 구현이 감춰지고 추상화되지 않는다는 의미다. 그보다는 추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료를 조작할 수 있어야한다.

 

아래 클래스를 통해 추상화에 대해 더 알아보자

public interface Vehicle1 {
  double getFuelTankCapacityInGallons();
  double getGallonsOfGasoline();
}

public interface Vehicle2 {
  double getPercentFuelRemaining();
}

Vehicle1은 자동차 연료 상태를 구체적인 숫자 값으로 알려준다. 두 메서드가 변수 값을 그대로 반환할 뿐이라는 게 명백하다. 반면 Vehicle2는 자동차 연료 상태를 백분율이라는 추상적인 개념으로 알려준다. 정보가 어떤 자료에서 왔는지 드러나지 않는다.

자료를 세세하게 공개하기보다는 Vehicle2처럼 추상적인 개념으로 표현하는 것이 좋다.

 

인터페이스나 get/set 함수만으로는 추상화가 쉽지 않다. 객체가 포함하는 자료를 표현하는 방법을 진지하게 고민해야한다. 아무 생각 없이 get/set 함수를 추가하는 것은 권장하지 않는다.

 

객체와 자료 구조의 차이

객체는 자료를 숨기고 자료를 다루는 함수만 공개한다.

자료 구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.

 

자료 구조를 사용하는 절차적인 코드는 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.

객체를 사용하는 객체 지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.

 

자료 구조를 사용하는 절차적인 코드는 자료 구조를 추가하려면 모든 함수를 고쳐야 한다.

객체를 사용하는 객체 지향 코드는 새 함수를 추가하려면 모든 클래스를 수정해야 한다.

 

디미터 법칙

어떤 객체가 다른 객체에 대해 지나치게 많이 알다보니 결합도가 높아지는 것을 발견하고 이를 개선하고자 객체에 자료는 숨기고 함수를 공개하도록 변경한 것이 디미터 법칙이다. 즉, 개발자는 자신이 조작하는 객체의 자료 구조는 몰라야한다는 것이 이 법칙의 핵심이다. 

 

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

위 코드는 train wreck로 불리는 코드로 지양해야한다.

하지만 디미터 법칙도 위반하는 코드일까?

Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();

첫번째 코드를 단순히 풀어써보았다. 이 코드는 디미터 법칙을 위반할까?

ctxt, opts, scratchDir이 객체라면 자료 구조를 그대로 노출하므로 디미터의 법칙을 위반한다.

하지만 자료구조라면 당연히 내부 구조를 노출하므로 디미터 법칙이 적용되지 않는다. 

위 예제가 디미터 법칙 위반 여부를 명백히 구분할 수 없는 이유는 ctxt, opts, scratchDir가 객체일 수도 자료구조 일수도 있기 때문이다.

 

그럼 위 예제를 혼란을 야기하지 않고 명백히 디미터의 법칙을 위반하지않는 코드로 바꿔보자.

final String outputDir = ctxt.options.scratchDir.absolutePath;

이 코드는 ctxt, opts, scratchDir는 자료 구조이며 함수 없이 공개 변수만 있다는 것을 확실히 알 수 있다.

자료구조는 무조건 함수 없기 공개 변수만 객체는 비공개 변수와 공개 함수를 포함한다로 정리한다면 위 문제가 혼란을 야기하진 않을 것이다.

 

하지만,  ctxt, opts, scratchDir가 진짜 객체라면 위 코드는 디미터의 법칙을 위반한다. 코드를 적절하게 변경해보자.

// 1
ctxt.getAbsolutePathOfScratechDirectoryOption();

// 2
ctxt.getScratechDirectoryOption().getAbsolutePath();

1번은 ctxt 객체가 공개해야하는 메서드가 많아지고 2번은 getScratechDirectoryOption()가 자료구조를 반환한다고 가정해야만한다.

 

ctxt가 객체라면 뭔가를 하라고 말해야한다. 자료구조를 드러내라고 말하면 안 된다.

 

다시 처음으로 돌아가서 코드의 의도를 파악해보자.

임시 디렉토리의 절대 경로인 outputDir을 얻어서 무엇을 하려고 하는 걸까? 결론은 임시 파일을 생성하기 위해서 임시 디렉토리의 절대 경로를 얻으려고 한다. 그렇다면 ctxt 객체에 임시 파일을 생성하라고 시키는 방식으로 변경하면 어떨까?

BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);

 

ctxt 내부 구조도 숨기고 해당 함수는 자신이 몰라도 되는 다른 객체를 탐색할 필요도 없다. 디미터의 원칙도 위반하지 않는다.

 

결론

객체와 자료 구조의 차이를 이해하고 직면한 문제에 최적인 해결책이 객체인지 자료 구조인지 잘 판단해보자.