01. 개방-폐쇄 원칙

  • 유연한 설계란 기존의 코드를 수정하지 않고도 애플리케이션의 동작을 확장할 수 있는 설계입니다.

컴파일타임 의존성을 고정시키고 런타임 의존성을 변경하라

  • 유연하고 재사용 가능한 설계에서 런타임 의존성과 컴파일타임 의존성은 서로 다른 구조를 가집니다. 이로인해 컴파일타임 의존성을 수정하지 않고도 런타임 의존성을 쉽게 변경 할 수 있습니다.
    • 컴파일타임 의존성은 코드에서 드러나는 클래스들 사이의 관계입니다.
    • 런타임 의존성은 실행시에 협력에 참여하는 객체들 사이의 관계입니다.

추상화가 핵심이다(DIP?)

  • 개방-폐쇄 원칙의 핵심은 추상화에 의존하는 것입니다.
    • 추상화란 핵심적인 부분만 남기고 불필요한 부분은 생략함으로써 복잡성을 극복하는 기법 입니다.
  • 추상화 부분은 수정에 대해 닫혀 있습니다. 추상화를 통해 생략된 부분은 확장의 여지를 남깁니다.
  • 추상화는 설계의 확장을 가능하게 하지만 중요한 점은 의존성의 방향입니다. 수정에 대한 영향을 최소화 하기 위해서는 모든 요소가 추상화에 의존해야 합니다.
  • 변경에 의한 파급효과를 최대한 피하기 위해 변하는 것과 변하지 않는 것이 무엇인지를 이해하고 이를 추상화의 목적으로 삼아야 합니다.
abstract class DiscountPolicy {
  constructor(private conditions: DiscountCondition[]) {
    this.conditions = conditions;
  }

  // 변하지 않는 부분
  public calculateDiscountAmount(screening: Screening): Money {
    for (each in this.conditions) {
      if (each.isSatisfiedBy(screening)) {
        return getDiscountedFee(screening);
      }
    }

    return screening.getMovieFee();
  }

  // 변하는 부분(추상화)
  protected abstract getDiscountAmount(screening: Screening): Money;
}

02. 생성 사용 분리(IoC?)

  • 동일한 클래스 안에서 객체 생성과 사용이라는 두 가지 이질적인 목적을 가진 코드가 공존하는 것은 문제입니다.
  • 생성과 사용을 분리(separation use from creation)해야 합니다.
    • 사용으로부터 생성을 분리하는 데 사용되는 가장 보편적인 방법은 객체를 생성하는 책임을 클라이언트로 옮기는 것입니다.
class Client {
  public getAvatarFee(): Money {
    const avatar: Movie = new Movie(
      '아바타',
      Duration.ofMinutes(120),
      Money.wons(10000),
      new AmountDiscountPolicy(),
    );
    return avatar.getFee();
  }
}

FACTORY 추가하기

  • 객체 생성과 관련된 책임만 전담하는 별도의 객체(FACTORY)를 추가하고 Client는 이 객체를 사용하도록 만들 수 있습니다.
class Factory {
  public createAvatarMovie(): Movie {
    return new Movie(
      '아바타',
      Duration.ofMinutes(120),
      Money.wons(10000),
      new AmountDiscountPolicy(),
    );
  }
}

// Client는 Factory를 사용해 생성된 Movie의 인스턴스를 반환받아 사용하기만 하면 됩니다.
class Client {
  constructor(private factory: Factory) {}

  public getAvatarFee(): Money {
    const avatar: Movie = factory.createMNadMAxMovie();
    return avatar.getFee();
  }
}
  • Client는 오직 사용과 관련된 책임만 지고 생성과 관련된 어떤 지식도 가지지 않을 수 있습니다.

순수한 가공물에게 책임 할당하기

  • 책임 할당의 가장 기본이 되는 원칙은 책임을 수행하는 데 필요한 정보를 가장 많이 알고 있는 INFORMATION EXPERT(정보 전문가)에게 책임을 할당하는 것이지만 FACTORY는 도메인 모델에 속하지 않는 순수한 기술적인 결정으로 결합도를 낮추고 재사용성을 높이기 위해 도메인 개념에게 할당돼 있던 객체 생성 책임을 도메인 개념과는 아무런 상관 없는 가공의 객체로 이동시킨 것입니다.
  • 어떤 행동을 추가하려고 이 행동을 책임질 마땅한 도메인 개념이 존재하지 않는다면 PURE FABRICATION을 추가하고 이 객체에게 책임을 할당합니다.
    • PURE FABRICATION(순수한 제조)은 표현적 분해보다 행위적 분해에 의해 생성되는 것이 일반적입니다.
  • 객체지향이 실서계를 모방해야 한다는 헛된 주장에 현혹될 필요가 없습니다. 우리가 애플리케이션을 구축하는 것은 사용자들이 원하는 기능을 제공하기 위해서지 실세계를 모방하거나 시뮬레이션 하기 위한 것이 아닙니다.

03 의존성 주입(DI)

  • 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법을 의존성 주입(Dependency Injection)이라고 부릅니다.
  • 생성자 주입, setter 주입(property 주입), 메서드 주입, 인스턴스 주입
    • 메서드 주입을 의존성 주입의 한 종류로 볼 것인가에 대해서는 논란의 여지가 있습니다.
    • 인스턴스 주입은 근본적으로 setter 주입이나 프로퍼티 주입과 동일합니다.

숨겨진 의존성은 나쁘다

  • 의존성을 구현 내부로 감출 경우 의존성과 관련된 문제가 컴파일타임이 아닌 런타임에 가서야 발견됩니다.
  • 캡슐화는 코드를 읽고 이해하는 행위와 관련이 있습니다. 클래스의 퍼블릭 인터페이스만으로 사용하는 방법을 이해할 수 있는 코드가 캡슐화의 관점에서 훌륭한 코드입니다. 하지만 숨겨진 의존성은 이를 해칩니다.
  • 의존성 주입은 필요한 의존성은 클래스의 퍼블릭 인터페이스에 명시적으로 드러냅니다. 의존성을 이해하기 위해 코드 내부를 읽을 필요가 없기 때문에 의존성 주입은 객체의 캡슐을 단단하게 보호합니다.
  • 명시적인 의존성에 초점을 맞추어야 합니다. 이는 유연성을 향상시키는 가장 효과적인 방법입니다.

04 의존성 역전 원칙(DIP)

추상화와 의존성 역전

  • 어떤 협력에서 중요한 정책이나 의사결정, 비즈니스의 본질을 담고 있는 것은 상위 수준의 클래스입니다.
  • 하위 수준의 변경을 한다고 해서 상위 수준이 영향을 받아서는 안 됩니다. 의존성은 변경에 전파와 관련된 것이기 때문에 설계는 변경의 영향을 최소화하도록 의존성을 관리해야 합니다.
  1. 상위 수준의 모듈을 하위 수준의 모듈이 의존해서는 안 됩니다. 둘 모두 추상화에 의존해야 합니다.
  2. 추상화는 구체적인 사항에 의존해서는 안 됩니다. 구체적인 사항은 추상화에 의존해야 합니다.
  • 의존성 역전이라는 단어를 사용한 이유는 전통적인 절차형 프로그래밍과는 의존성의 방향이 반대이기 때문입니다.

의존성 역전 원칙과 패키지

  • 별도의 독립적인 패키지가 아니라 클라이언트가 속한 패키지에 포함해야 합니다.
  • SEPARATED INTERFACE 패턴
  • 잘 설계된 객체지향 애플리케이션에서는 인터페이스의 소유권을 서버가 아닌 클라이언트에 위치 시킵니다.

05. 유연성이 대한 조언

유연한 설계는 유연성이 필요할 때만 옳다

  • 단순함과 명확함의 미덕을 버리게 될 가능성이 높습니다. 유연항 설계라는 말의 이면에는 복잡한 설계라는 의미가 숨어 있습니다.
  • 정적인 클래스의 구조와 실행 시점의 동적인 객체 구조가 다릅니다.

협력과 책임이 중요하다

  • 객체의 협력과 책임이 중요합니다.
  • 설계를 유연하게 만들기 위해서는 협력에 참여하는 객체가 다른 객체에게 어떤 메시지를 전송하는지가 중요합니다.
  • 설게를 유연하게 만들기 위해서는 먼저 역할, 책임, 협력에 초점을 맞춰야 합니다. 다양한 컨텍스트에서 협력을 재상용할 필요가 없다면 설계를 유연하게 만들 당위성도 함께 사라집니다.
  • 객체를 생성하는 방법에 대한 결정은 모든 책임이 자리를 잡은 후 가장 마지막 시점에 내리는 것이 적절합니다.


참고