Ch.9 유연한 설계

개방-폐쇄 원칙 (Open-Closed Principle, OCP)

  • OCP - 소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.

    • 확장에 대해 열려 있다 - 요구사항이 변경되면 이 변경에 맞게 새로운 '동작'을 추가하기 쉽다.

    • 수정에 대해 닫혀 있다 - 기존의 '코드'를 수정하지 않고도 애플리케이션의 '동작'을 추가하거나 변경할 수 있다.

  • 핵심은 추상화의존하는 것이다.

추상화 - 핵심만 남기고 불필요한 부분은 생략함으로써 복잡성을 극복하는 기법.

  • 추상화를 거치면 문맥이 바뀌더라도 변하지 않는 부분만 남고 문맥에 따라 변하는 부분은 생략된다.

사실 개방-폐쇄 원칙(OCP)은 런타임 의존성과 컴파일타임 의존성에 대한 이야기이다.

컴파일타임 의존성, 런타임 의존성

  • 예제에서...

    • 컴파일타임 의존성 - Movie는 추상 클래스인 DiscountPolicy에 의존한다.

    • 런타임 의존성 - MovieDiscountPolicy를 구현한 AmountDiscountPolicy, PercentDiscountPolicy에 의존한다.

  • OCP을 잘 따르는 코드는 컴파일타임 의존성을 수정하지 않고도 런타임 의존성을 쉽게 변경할 수 있다.

    • 예제에서 중복 할인 정책을 구현하는 OverlappedDiscountPolicy 클래스를 추가로 구현하더라도 Movie 클래스는 여전히 DiscountPolicy 클래스에만 의존할 수 있다.

주의점 & 참고사항

  • OCP적인(?) 설계는 공짜가 아니다. 단순히 추상화를 했다고 해서 수정에 대해 닫힌 설계가 되는 건 아니다.

    • 변하는 것과 변하지 않는 것을 구분하고 추상화의 목적으로 삼아야 한다.

생성과 사용을 분리하자. (Separate use from creation)

생성자 외부에서 생성해 생성자로 전달하자.

  • 추상화에만 의존시키기 위해서 생성자 내부에서 new 키워드로 구체화된 인스턴스를 만들지 말자.

  • 생성에 특화된 FACTORY를 추가할 수도 있다.

PURE FABRICATION

모든 책임을 도메인 객체에게 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하와 같은 문제에 봉착할 수 있다.

그럴 땐, 주저하지 말고 도메인 개념을 표현한 객체가 아닌, 설계자가 편의를 위해 임의로 만들어낸 가공의 객체인 PURE FABRICATION (순수한 가공물)에 책임을 할당하자. (편의상 만들어낸 클래스)

Dependency Injection (의존성 주입)

외부에 독립적인 객체가 인스턴스를 생성한 후 이를 전달해 의존성을 해결하는 방법.

왜 "의존성 주입"인가?

  • 외부에서 의존성의 대상을 해결한 후 이를 사용하는 객체 쪽으로 주입하기 때문이다.

앞에 장에서 여러 번 설명했듯, 세 가지 방법이 있다.

  1. 생성자 주입 (Constructor Injection)

  2. Setter 주입 (Setter Injection)

  3. 메서드 주입 (Method Injection)

  4. (Bonus) 인터페이스 주입 (Interface Injection) (e.g., DiscountPolicyInjectable)

메서드 주입은 메서드 호출 주입 (Method Call Injection)이라고도 부른다. 메서드 실행 시 인자를 이용해 의존성을 해결하며, 메서드가 의존성을 필요로 하는 유일한 경우일 때 사용할 수 있다.

주입된 의존성이 한 두개의 메서드에서만 사용된다면 메서드 주입이 나을 수 있다.

Service Locator

클래스를 만들어 static 키워드를 이용해 singleton 패턴을 구현한다.

스프링의 컨테이너 / @Configuration 클래스와는 완전히 다르다.

  • @Configuration, @Bean 숨겨진 의존성을 그냥 전역 클래스로 import해서 주입하기 때문.

public Move(...) {
    // ...
    this.discountPolicy = ServiceLocator.discountPolicy();
}

SERVICE LOCATOR의 단점은, 숨겨진 의존성이 생긴다는 것이다. 의존성이 있음에도 불구하고 인터페이스 어디에도 의존성에 대한 정보가 표시돼 있지 않다.

  • implements, extends 또는 함수의 매개변수 등으로 의존성을 표현할 수 있는데 말이다!

숨겨진 의존성

  • 의존성을 숨기는 코드는 단위 테스트 작성이 어렵다.

    • Service Locator는 정적 변수로 객체를 관리하므로 모든 단위 테스트가 상태를 공유하게 된다. 그래서 테스트가 고립되어야 한다는 단위 테스트의 기본 원칙을 위반한다.

  • 숨겨진 의존성은 의존성을 이해하기 위해 코드의 내부 구현을 이해할 것을 강요한다.

    • 따라서 숨겨진 의존성은 캡슐화를 위반한다.

  • 숨겨진 의존성은 의존성의 대상을 설정하는 시점, 해결하는 시점을 멀리 떨어뜨려 놔서 코드를 이해하고 디버깅하기 어렵게 만든다.

의존성 주입은 이러한 문제를 깔끔하게 해결한다.

  • 필요한 의존성이 클래스의 퍼블릭 인터페이스에 명시적으로 드러난다.

  • 의존성을 이해하기 위해 코드 내부를 읽을 필요가 없으므로 캡슐화를 지킬 수 있다.

  • 단위 테스트를 작성할 때 SERVICE LOCATOR에 객체를 추가하거나 제거할 필요가 없다. 그저 필요한 인자를 전달해서 필요한 객체를 생성하면 된다.

결론은... SERVICE LOCATOR 패턴을 떠나서 숨겨진 의존성보다 명시적 의존성이 좋다는 것!

의존성 역전

유연하고 재사용 가능한 설계를 위해 모든 의존성을 추상 클래스, 인터페이스와 같은 추상화를 향하도록 하자.

여기서 나오는 개념이 의존성 역전 원칙 (Dependency Inversion Principle, DIP)이다.

  • 상위 수준의 모듈은 하위 수준의 모듈에 의존해선 안 된다. 둘 모두 추상화에 의존해야 한다.

  • 추상화는 구체적인 사항에 의존해서는 안 된다. 구체적인 사항은 추상화에 의존해야 한다.

재사용될 필요가 없는 클래스는 독립적인 패키지에 모아야 한다.

  • 불필요한 클래스를 같은 패키지에 두면 수정이 생길 때마다 관련이 없는 패키지에 영향이 가서 불필요한 재컴파일이 생길 수 있어 전체적인 빌드 시간을 올리게 된다.

이를 SEPARATED INTERFACE 패턴이라고 부른다.

유연성

유연성의 트레이드오프로 항상 복잡성이 올라간다.

  • e.g., 정적인 클래스의 구조와 실행 시점의 동적인 객체 구조가 다르다.

유연한 설계는 유연성이 필요할 때만 써도 좋다.

협력과 책임

객체의 역할과 책임이 자리를 잡기 전에 너무 성급하게 객체의 생성에 집중하지 말자.

객체를 생성할 책임을 담당할 객체나 객체 생성 메커니즘을 결정하는 시점(e.g., FACTORY)은 책임 할당의 마지막 단계로 미루자.

Last updated