본문 바로가기

책 리뷰/엘레강트 오브젝트

엘레강트 오브젝트 [2장 - (1)]

⌗ 엘레강트 오브젝트 책을 리뷰해보도록 하겠습니다.

책의 내용은 다양한 언어로 되어있지만, 저는 예시 코드로 Typescript 를 사용해 제 사족을 조금씩 붙일 예정입니다.

 

Chapter2. Education

 

이번 챕터의 메인 주제는 '객체는 작아야 한다' 입니다. 객체가 작아야만 유지보수에 용이한 우아한 객체라고 할 수 있습니다.

그렇게 만들 수 있는 방법을 보도록 하겠습니다.

 

가능하면 적게 캡슐화하세요

 

4개 또는 그 이하의 객체를 캡슐화할 것을 권장합니다. 더 많은 객체를 캡슐화해야 한다면, 클래스에 문제가 있는 것이기 때문에 리팩토링이 필요하다고 볼 수 있습니다.

 

class Cash {
  private digits: number;
  private cents: number;
  private currency: string;
  
  constructor(digits: number, cents: number, currency: string) {
    this.digits = digits;
    this.cents = cents;
    this.currency = currency;
  }
}

const a = new Cash(25, 50, 'USD');
const b = new Cash(25, 90, 'USD');

console.info(a === b); // false

 

위 예시의 Cash 클래스는 3개의 객체를 캡슐화하고 있고 이 객체들이 함께 모여 Cash 클래스의 객체를 식별합니다.

물론 식별자라고 부를 수 없긴 합니다. 위 예시에서처럼 a, b 는 서로 다른 객체로 판별이 되고, 이를 언어의 설계 결함이라고 하기도 합니다.

일단 상태는 같다고 볼 수 있으며, 의미상 상태는 객체의 식별자 라고 할 수 있습니다.

 

내부에 캡슐화된 모든 객체들이 객체의 식별자를 구성하는 요소라면 이 요소가 4개를 넘어갈 경우 직관적이지 않게 됩니다.

우리의 사고방식이 4개 이상의 요소로 구성된 좌표를 이해하는건 너무도 어려워하기 때문입니다.

4개를 넘어간다면, 해당 클래스를 더 작은 클래스로 분해하는 리팩토링 과정을 거쳐야 한다는 점을 기억하세요.

 

이 책에선 이런 애매한 기준으로 4개 라는 숫자를 명시하고 있지만 이또한 저자의 경험에 근거한 기준일 뿐 위 내용을 봐도 모두를 납득시킬만한 주장은 아니라는 생각은 듭니다. 하지만 저자가 정말 실무에서 OOP 프로그래밍을 계속해 다뤄왔다면 이 주장은 충분히 합리적이라고 생각하고, 실제론 어쩔 수 없이 4개보다 더 많은 객체를 캡슐화하는 클래스가 있을 것 같네요.

 

최소한 뭔가는 캡슐화하세요

 

class Year {
  public read(): number {
    return Math.floor(Date.now() / (1000 * 60 * 60 * 24 * 30 * 12) + 1970);
  }
}

 

위 Year 클래스의 인스턴스는 어떤 것도 캡슐화하지 않기 때문에 이 클래스의 모든 객체들은 동일합니다.

하지만 이 또한 잘못되었고, 너무 많이 캡슐화하는 것도 좋지 않지만 아무것도 캡슐화하지 않는 방식 또한 좋지 않습니다.

객체지향 진영에선 static method 가 악명이 높다고 들어왔는데 프로퍼티가 없는 클래스는 이와 유사합니다.

이런 클래스는 오직 행동만을 포함하는데, 순수한 OOP 에선 기술적으로 프로퍼티가 없는 클래스를 만들 수 없기 때문에 문제입니다.

 

class Year {
  private millis: number;

  constructor(millis: number) {
    this.millis = millis;
  }

  public read(): number {
    return Math.floor(this.millis / (1000 * 60 * 60 * 24 * 30 * 12) + 1970);
  }
}

 

최소한 캡슐화할 어떠한 프로퍼티가 하나라도 있어야 합니다. 객체가 nothing 과 비슷한 무언가가 아니라면 말이죠.

 

항상 인터페이스를 사용하세요

 

객체들은 서로 필요하기에 결합됩니다. 하지만 어플리케이션이 커져가고 객체의 수가 수십개를 넘어가기 시작하면

객체 사이에 강한 결합도가 심각한 문제로 떠오릅니다. 아마 많은 개발자가 알고 있는 부분일거라 생각하고, 결합도 문제는 유지보수성에 큰 영향을 미칩니다.

 

따라서 최선을 다해 객체를 분리해야하고 여기서 객체 분리란 상호 작용하는 다른 객체를 수정하지 않고도 해당 객체를 수정할 수 있도록

만든다는 것을 의미합니다. 바로 인터페이스를 사용해서 말이죠.

 

interface Cash {
  multiply(factor: number): Cash;
}

 

위 코드에서 Cash 는 인터페이스입니다. 이는 즉 객체가 다른 객체와 의사소통하기 위해 따라야 하는 계약입니다.

 

class DefaultCash implements Cash {
  private dollars: number;

  public constructor(dollars: number) {
    this.dollars = dollars;
  }

  public multiply(factor: number): Cash {
    return new DefaultCash(this.dollars * factor);
  }
}

class Employee {
  private salary: Cash;
}

 

금액이 필요하면, 실제 구현 대신 계약에 의존하면 됩니다.

위 예시에서 Employee 는 Cash 인터페이스 구현 방법엔 전혀 관심이 없고, multiply 가 어떻게 동작하는지도 관심 없습니다.

동작 방식을 모르기 때문에, Cash 인터페이스를 이용하면 Employee 와 DefaultCash 를 느슨하게 분리할 수 있는 것이죠.

이미 OOP 진영에서 많이 얘기하는 것처럼 이렇게 함으로써 DefaultCash 내부 구현을 변경하거나, Cash 인터페이스의 구현체를 다른 것으로 변경해도 Employee 에는 아무 영향이 없습니다.

 

즉 앞으로 이런 규칙이 들어가야 합니다. 클래스 안의 모든 퍼블릭 메서드가 인터페이스를 구현하도록 만드세요.

자바에서처럼 @Override 와 같은 어노테이션은 없지만, typescript 에서도 인터페이스를 만들고 이를 구현하는 클래스가 이를 만족하지 않으면 컴파일은 실패합니다.

 

인터페이스를 사용하지 않으면 클래스간의 결합도가 강해지도록 조장합니다.

인터페이스를 사용하고 동일한 인터페이스를 구현하는 여러 클래스가 존재한다면, 이는 서로 쉽게 대체할 수 있음을 의미하고 이것이

느슨한 결합도입니다. 워낙 OOP 에서 많이 하는 얘기이기 때문에, 인터페이스에 대해 더 뭔가를 적을 필요는 없을 것 같습니다.