Clean Code - 객체와 자료구조

Park Suyong·2022년 1월 7일
0

Study

목록 보기
9/12

자료 추상화

"자료를 세세하게 공개하기 보다 추상적인 개념으로 표현하는 편이 좋다. 개발자는 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각학 고민해야 한다. getter, setter를 그냥 추가하는 것은 정말 나쁘다."

아래 두 개의 코드를 비교해 보자.

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

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

두 코드를 비교했을 때 1번 코드는 구현을 외부로 노출하고 2번 코드는 구현을 완전히 감췄다.

1번 코드의 경우 직교좌표계를 사용한다. 또한, 구현을 노출한다. 만약 변수를 private 하게 선언하였다고 하더라도 gettersetter를 제공한다면 구현을 외부로 노출하는 셈이다.

반면 2번 코드의 경우, 직교좌표계인지 극좌표계인지 알 수 있는 길이 없다. 둘 다 아닐 수도 있다. 그럼에도 불구하고 인터페이스는 자료구조를 명백하게 표현하고 있다.
정확히는 자료구조 이상을 표현한다. 좌표를 읽을 때는 getX, getY를 통해 각각 따로 읽도록 강제하지만 값을 세팅할 때는 두 좌표를 한 번에 하도록 말이다.

구현을 감추기 위해서는 추상화가 필요하다.

아래 두 코드를 보도록 하자.

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

public interface Vehicle {
    double getPercentFuelRemaining();
}

1번 코드의 경우 자동차 연료의 상태를 구체적인 값으로 반환한다. 반면 2번 코드의 경우 그 값을 백분율이라는 추상적인 값으로 반환한다.

심지어, 1번 코드의 경우 그 값을 읽어서 반환하는 함수라는 것이 명백하지만 2번 코드의 경우 그 값을 어디서 가져오는지 알 수 없다.

결론적으로 자료는 추상적으로 표현하는 것이 좋다. 또한, 인터페이스나 getter, setter 함수 만으로는 추상화가 이뤄지지 않는다.

자료/객체 비대칭

"객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다. 자료구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다."

예를 들어 사각형, 삼각형, 원 이라는 자료구조가 있다고 가정한다. 또, 도형 클래스는 넓이를 구하는 함수를 갖고 있다.

이 때, 만약 도형 클래스에 둘레 길이를 구하는 함수를 추가해 보자. 이 때 사각형, 삼각형, 원 이라는 자료구조는 아무 영향도 받지 않는다. 이 상태에서 다른 도형을 추가해 보자. 그렇다면 둘레 길이를 구하는 함수, 넓이를 구하는 함수 모두 변경되어야 한다.

그렇다면 반대로 사각형, 삼각형, 원이라는 객체가 있다고 가정하자. 각 클래스에는 넓이를 구하는 함수가 포함되어 있다. 따라서, 새 도형을 추가한다고 하더라도 기존 함수에 아무런 영향을 미치지 않는다. 반면, 새 함수를 추가하고 싶다면 모든 클래스를 수정해야 한다.

즉, 자료구조를 사용하는 절차적인 코드는 기존 코드를 변경하지 않으면서 새로운 함수를 추가하기 쉽다. 반면 객체 지향 코드는 기존 함수를 변경하지 않으면서 새로운 클래스를 추가하기 쉽다.


이말은 즉슨, 객체 지향 코드에서 어려운 변경은 절차적 코드에서 쉽고 절차적 코드에서 변경하기 어려운 것은 객체 지향 코드에서 쉽다.

따라서, 복잡한 시스템을 구축하는 과정에서 새로운 자료 구조가 필요한 경우 객체 지향 기법을 통해 코딩하고, 새로운 함수가 필요한 경우 절차적 코드와 자료구조가 필요하다. 상황에 맞춰 작성하는 것이 옳다.

디미터 법칙

"디미터 법칙은 객체는 내부 구조를 공개해서는 안 된다는 의미이다."

디미터 법칙을 좀 더 정확히 표현하자면,
클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다.

  1. 클래스 C
  2. f가 생성한 객체
  3. f 인수로 넘어온 객체
  4. C 인스턴스 변수에 저장된 객체

하지만 위 객체에서 허용된 메서드가 반환하는 객체의 메서드는 호출하면 안 된다.

다음 코드를 보자.

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

위 코드는 디미터 법칙을 위반하는 것처럼 보인다. 메서드가 반환하는 객체의 메서드를 호출했기 때문이다.

이러한 코드를 기차 충돌(train wreck) 이라 한다. 여러 전동차가 한 줄로 이어진 기차처럼 보이기에 매우 조잡하다. 따라서 피하는 것이 좋다.

위 코드를 아래와 같이 수정해 본다.

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

우선 기차 충돌은 피했다. 이 코드는 디미터 법칙을 위반하는 것일까? 정답은 확답 불가다. 왜냐하면 ctxt, Options, ScratchDir이 객체인지, 자료구조인지에 따른 차이이다.

객체라면 내부 구조를 반드시 숨기고 action을 취하는 함수만 공개해야 한다. 그러므로 위 코드는 디미터 법칙에 위배된다. 하지만 자료구조라면? 내부 구조를 노출하므로 디미터 법칙이 적용되지 않는다. 그러나, 위 예제는 조회 함수를 사용하는 바람에 혼란을 일으키게 된다. 따라서, 다음과 같이 구현한다면 디미터 법칙을 거론할 필요가 없다.

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

그러나, 자료구조는 무조건 함수 없이 공개 변수만 포함하고 객체는 비공개 변수와 공개 함수를 포함한다면, 문제는 훨씬 간단히 풀릴 것이다.

잡종 구조

때때로 코드에서 절반은 객체, 절반은 자료구조인 구조가 있다. 이를 잡종 구조라고 하는데, 잡종 구조를 살펴 보면 중요한 기능을 수행하는 함수도 존재하며 공개 변수나 getter setter 함수도 있다. getter setter 함수는 비공개 변수를 그대로 노출하게 된다.

이러한 구조는 새로운 함수는 물론이며 새로운 자료구조 또한 추가하기 어렵다. 따라서, 최대한 피하는 것이 좋다.

구조체 감추기

앞서 보았던 ctxt options scratchDir이 정말 객체라면 내부 구조를 감춰야 하므로 앞선 코드 예제처럼 줄줄이 엮어서는 안 된다. 객체라면 어떠한 행동을 해야 한다. 다음 두 코드를 살펴 보자.

ctxt.getAbsolutePathOfScratchDirectoryOption();
ctxt.getScratchDirectoryOption().getAbsolutePath();

1번째 코드는 ctxt 객체가 알아야 하는 정보가 너무 많아진다. 2번째 코드는 getScratchDirectoryOption()이 자료 구조라는 전제가 깔려 있어야 한다. 다만, 두 방법 모두 내키지 않는 방법일 것이다.

코드를 좀 더 파헤쳐 보자. 임시 디렉토리의 절대 경로값이 필요한 것이다. 그렇다면, ctxt 객체에 임시 파일을 생성하라고 시키는 것은 어떨까?

BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);

객체에게 이를 수행하라고 지시하기에 최적이다. 이로써 ctxt는 내부 구조를 드러내지 않게 되며 디미터 법칙을 위반하지 않는다.

자료 전달 객체

"자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스다. 자료 구조체를 때로는 자료 전달 객체(Data Transfer Object)라 한다."

활성 레코드

활성 레코드는 DTO의 특수한 형태인데, 공개 변수가 있거나 비공개 변수에 getter setter가 있는 자료구조이다. 대게 savefind와 같은 탐색 함수도 제공한다. 활성 레코드는 데이터베이스 테이블이나 다른 소스에서 자료를 직접 반환한 결과이다.

그런데, 여기에 비즈니스 로직을 추가하여 이러한 자료구조를 객체로 취급하는 경우가 있다. 이는 잡종 구조로 취급되기 때문에 사용하지 말아야 한다. 따라서, 비즈니스 로직을 담으면서 내부 데이터를 숨기는 객체는 따로 생성해야 한다.

결론

"객체는 action을 공개하며, data를 숨긴다. 그렇기 때문에 새로운 action을 추가하는 것은 어려우나 새로운 객체 타입을 생성하는 것은 쉽다. 반면, 자료구조의 경우 data를 공개하며 별다른 action이 없다. 따라서 새로운 action을 추가하는 것은 쉬우나 자료구조를 새로 추가하는 것은 어렵다."

따라서 새로운 객체 타입을 생성하는 것이 필요하다면 객체가 더 적합하고, 새로운 동작을 정의하려는 경우 자료구조 및 절차적 코드가 적합하다.

최적의 해결책을 선택하는 것이 중요하다.

profile
Android Developer

0개의 댓글