포스트

9장. 유연한 설계

9장. 유연한 설계

8장에서 배운 기법들을 원칙이라는 관점에서 정리하는 챕터


❐ 1. 개방-패쇄 원칙


1-1. 개방-패쇄 원칙

개방-패쇄 원칙란?

  • 소프트웨어 객체는 확장에 열려있어야 하고, 수정에 대해서는 닫혀 있어야 한다.
  • 여기서 핵심은 확장수정
    • 확장에 대해 열려있어야 한다.
      • 요구사항이 변경될 떄 이 변경에 맞게 새로운 동작을 추가해서 기능을 확장시킬 수 있다.
    • 수정에 대해 닫혀있다.
      • 기존의 코드를 수정하지 않고도 애플리케이션의 동작을 추가하거나 변경할 수 있다.


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

의존성 관점에서 OCP를 다르는 설계란?

  • 컴파일타임 의존성은 유지하면서 런타임 의존성의 가능성을 확장하고 수정할 수 있는 구조


1-3. 추상화가 핵심이다.

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

  • OCP의 관점에서 ‘생략되지 않고 남겨지는 부분’은 다양한 상황에서의 공통점을 반영한 추상화의 결과물
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class DiscountPolicy { 
    private List<DiscountCondition> conditions = new ArrayList<>();
    
    public DiscountPolicy(DiscountCondition... conditions) {
        this.conditions = Arrays.asList(conditions);
    }
    
    public Money calculateDiscountAmount(Screening screening) { 
        for (DiscountCondition each : conditions) {
            if (each.isSatisfiedBy(screening)) { 
                return getDiscountAmount (screening); 
            }
        }
        return screening.getMovieFee(); 
    } 
    
    // 상속을 통해 구체화함으로써 할인 정책을 확장할 수 있다.
    abstract protected Money getDiscountAmount(Screening Screening);
}
  • 언제라도 추상화의 생략된 부분을 채워넣음으로써 새로운 문맥에 맞게 기능을 확장할 수 있다.
  • 따라서 추상화는 설계의 확장을 가능하게 한다.


OCP 원칙에서 폐쇄를 가능하게 하는 것은 의존성의 방향이다.

  • OCP의 “폐쇄”는 변경의 영향이 어떤 경계를 넘지 않도록 막는 것
    • 그걸 가능하게 하는 유일한 수단이 의존성 방향을 뒤집는 것이다.
  • 수정에 대한 영향을 최소화하기 위해서는 모든 요소가 추상화에 의존해야 한다.
    • 이 말이 무조건 인터페이스를 써라!는 아니다.
    • 정확히 말하면, 변경 가능성이 있는 축에 대해 안쪽 정책(상위 모듈)이 바깥 구현(하위 모듈)에 의존하지 말라
  • 의존성 화살표는 항상 안쪽(정책) → 추상화로만 향해야 한다.
    1
    2
    3
    4
    5
    
      비즈니스 규칙
         ↓
      추상화 (인터페이스)
         ↑
      구현 기술
    


추상화를 했다고 해서 모든 수정에 대해 설계가 폐쇄되는 것은 아니다!

  • 변경에 의한 파급효과를 최대한 피하기 위해서는 변하는 것변하지 않는 것이 무엇인지를
    이해하고 이를 추상화의 목적으로 삼아야만 한다.
  • 추상화가 수정에 대해 닫혀 있을 수 있는 이유는 변경되지 않을 부분을 신중하게 결정하고
    올바른 추상화를 주의 깊게 선택했기 때문이다.



❐ 2. 생성 사용 분리


2-1. 엉뚱한 곳에서 생성하는 객체

문제는 부적절한 곳에서 객체를 생성한다는 것.

  • 객체 생성은 필할 수는 없다. 어딘가에서 반드시 객체를 생성해야 한다.
  • 그런데 결합도가 높아지게 객체를 생성하면 OCP 원칙을 따르는 구조를 설계하기가 어려워진다.


생성과 사용을 분리하라.

  • 유연하고 재사용 가능한 설계를 원한다면 객체와 관련된 두 가지 책임을 서로 다른 객체로 분리해야 한다.
  • 하나는 객체를 생성하는 것이고, 다른 하나는 객체를 사용하는 것이다.


2-2. FACTORY 추가하기

FACTORY란?

  • 생성과 사용을 분리하기 위해 객체 생성에 특화된 객체
  • (내 생각)복잡한 객체 생성의 경우엔 Factory가 유의미할 듯.


2-3. 순수한 가공물에게 책임 할당하기

표현적 분해(representation decomposition)

  • 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 것.
  • 목적:
    • 도메인 모델에 담겨 있는 개념과 관계를 따르며 도메인과 소프트웨어 사이의 표현적 차이를 최소화하는 것
    • 따라서 표현적 분해는 객체지향 설계를 위한 가장 기본적인 접근법


행위적 분해(behavioral decomposition)

  • 모든 책임을 도메인 객체에게 할당하면 아래와 같은 심각한 문제점에 봉착하게 될 가능성이 높아진다.
    • 낮은 응집도
    • 높은 겹합도
    • 재사용성 저하
  • 이런 경우 도메인 개념을 표현한 객체가 아닌 설계자가 편의를 위해 임의로 만들어낸
    가상의 객체에게 책임을 할당해서 문제를 해결해야 한다. → 순수한 가공물(Pure Fabrication)
  • 어떤 행동을 추가하려고 하는데 이 행동을 책임질 마땅한 도메인 개념임 존재하지 않는다면
    Pure Fabrication을 추가하고 이 객체에게 책임을 할당하라.
  • 따라서 Pure Fabrication은 표현적 분해보다는 행위적 분해에 의해 생성되는 것이 일반적이다.



❐ 3. 의존성 주입


3-1. 의존성 주입

의존성 주입이란?

  • 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법
  • 의존성 주입은 의존성을 해결하기 위해 의존성을 객체의 퍼블릭 인터페이스에 명시적으로 드러내서
    외부에서 필요한 런타임 의존성을 전달할 수 있도록 만드는 방법을 표괄하는 명칭


의존성 해결

  • 의존성 해결은 컴파일타임 의존성과 런타임 의존성의 차이점을 해소하기 위한 다양한 메커니즘을 포괄한다.
  • 왜 ‘차이점을 해소’라고 하나?
    • 코드에는 “인터페이스 A를 쓴다”라고만 적혀 있지만, 실제로는 “구현체 A1인지 A2인지”를
      런타임에 선택해야 하므로, 컴파일타임과 런타임 사이에 ‘빈칸’이 생긴다.
    • 이 빈칸을 생성자 주입, 팩토리, DI 컨테이너 같은 메커니즘으로 채워서, 추상적인 타입 의존을
      구체적인 객체 의존으로 이어 주는 행위를 “컴파일타임·런타임 의존성의 차이를 해소하는 것”
    • 즉 의존성 해결이라고 부를 수 있다.


의존성을 해결하는 3가지 방법

  1. 생성자 주입(constructor injection)
    • 객체를 생성하는 시점에 생성자를 통한 의존성 해결
  2. setter 주입
    • 객체 생성 후 setter 메서드를 통한 의존성 해결
  3. 메서드 주입
    • 메서드 실행 시 인자를 이용한 의존성 해결


3-2. 숨겨진 의존성은 나쁘다.,

Service Locator 패턴

  • 의존성을 해결할 객체들을 보관하는 일종의 저장소다
  • 외부에서 객체에게 의존성을 전달하는 의존성 주입과 달리
    객체가 직접 Service Locator에게 의존성을 해결해줄 것을 요청한다.
  • 가장 큰 단점
    • 의존성을 감춘다는 것.


숨겨진 의존성의 문제점

  • 의존성을 이해하기 위해 코드의 내부 구현을 이해할 것을 강요한다.
  • 의존성의 대상을 설정하는 시점과 의존성이 해결되는 시점을 멀리 떨어트려 놓는다.
  • 따라서 숨겨진 의존성은 캡슐화를 위반한다.


의존성을 숨기는 코드의 단점

  • 디버깅이 어려움.
  • 테스트 작성 어려움.


의존성 주입은..

  • 필요한 의존성을 클래스의 퍼블릭 인터페이스에 명시적으로 드러낸다.
  • 의존성을 이해하기 위해 코드 내부를 읽을 필요가 없기 때문에 의존성 주입은 객체의 캡슐을 단단한게 보호한다.
  • 의존성과 관련된 문제도 최대한 컴파일타임에 잡을 수 있다.
    • 필요한 의존성을 인자에 추가하지 않을 경우 컴파일 에러가 발생하기 때문이다.


그럼 언제 Service Locator 패턴을 쓸 수 있을까

  • 의존성 지원 프레임워크를 사용하지 못하는 경우
  • 깊은 호출 계층에 걸쳐 동일한 객체를 계속해서 전달해야 하는 고통을 견디기 어려운 경우



❐ 4. 의존성 역전 원칙


4-1. 추상화와 의존성 역전

상위 수준의 클래스가 하위 수준의 클래스에 의존하게 되면..

  • 상위 수준의 클래스를 재사용할 때 하위 수준의 클래스도 변경이 필요하기 때문에 재사용이 어려움.
  • 결국 가장 중요한건 추상화에 의존하라.


의존성 역전(DIP, Dependency Inversion Principal)이란?

  • 상위 수준의 정책(비즈니스)이 하위 수준의 구현에 의존하지 않고, 둘 다 추상화에 의존하도록 의존성의 방향을 바꾸는 것


4-2. 의존성 역전 원칙과 패키지

인터페이스의 소유권

  • 객체지향 프로그래밍 언어에서 어떤 구성 요소의 소유권을 결정하는 것은 모듈이다.
    • 자바는 패키지를 이용해 모듈을 구현한다.


인터페이스가 서버 모듈 쪽에 위치하는 전통적인 모듈 구조

  • 위 그림은 OCP 원칙과 의존성과 DIP를 모두 준수하고 있음.
  • 그럼 문제가 뭐냐?
    • 🔥Movie를 다양한 컨텍스트에서 재사용할 때, 불필요한 클래스들이 Movie와 함께 배포되어야 한다!
    • Movie → DiscountPolicy 이기 때문에 Movie를 정상적으로 컴파일하기 위해서, DiscountPolicy가 필요하다.


이렇게 되면 어떤 기술적 문제가 생기는가

  1. DiscountPolicy가 포함된 패키지 안의 어떤 클래스가 수정됨.
  2. 패키지 전체가 재배포
  3. 이로 인해 이 패키지를 의존하는 Movie 클래스가 포함된 패키지도 재컴파일 대상
  4. 만약 Movie에 의존하는 또 다른 패키지가 있다면, 컴파일은 의존성을 타고 전파됨.
  5. 결과적으로 불필요한 클래스들을 같은 패키지에 두게되면 빌드 시간을 가파르게 상승 시킴


인터페이스의 소유권을 이전시킨 객체지향적인 모듈 구조

  • 추상화를 별도의 독립적인 패키지가 아니라 클라이언트가 속한 패키지에 포함시켜야 한다.
  • 의존성 역전 원칙에 따라 상위 수준의 협력 흐름을 재사용하기 위해서는
    추상화가 제공하는 인터페이스의 소유권 역시 역전시켜야 한다.


정리

  • 유연하고 재사용 가능하면 컨텍스트에 독립적인 설계는 전통적인 패러다임이 고수하는 의존성의 방향을 역전시킨다.
  • 객체지향 패러다임에서는
    • 상위 수준 모듈과 하위 수준 모듈이 모두 추상화에 의존한다.
    • 인터페이스가 상위 수준 모듈에 속한다.
  • 훌륭한 객체지향 설계를 위해서는 의존성을 역전시켜야 한다.

의존성 역전은

  • “구현을 바꾸기 쉬운 구조”가 아니라
  • “상위 정책의 협력을 구현 변화로부터 완전히 독립시키는 구조”를 만드는 것이다.


인터페이스 소유권 역전은

  • 의존성 역전 원칙을 패키지·빌드 단위까지 일관되게 적용한 결과다.



❐ 5. 유연성에 대한 조언


5-1. 유연한 설계는 유연성이 필요할 때만 옳다.

유연하고 재사용 가능한 설계란?

  • 런타임 의존성과 컴파일타임 의존성의 차이를 인식하고 동일한 컴파일타임 의존성으로부터
    다양한 런타임 의존성을 만들 수 있는 코드 구조를 가지는 설계를 의미한다.


유연하고 재사용 가능한 설계가 항상 좋은건 아니다.

  • 설계의 미덕은 단순함과 명료함이다.
  • 오히려 변경하기 쉽고 확장하기 쉬운 구조를 만들다가 단순함과 명료함을 버리게될 수 있다.


변경은 예상이 아니라 현실이어야 한다.

  • 정보가 제한적인 상황에서 아래의 질문에 대답하는 것은 심리에 가깝다.
    • 이 설계가 복잡한 이유는 무엇일까?
    • 어떤 변경에 대비하기 위해 설계를 복잡하게 만들었는가?
    • 정말 유연성이 필요한가?
  • 미래에 변경이 일어날지도 모른다는 막연한 불안감은 불필요하게 복잡한 설계를 낳는다.
    • 아직 일어나지 않은 변경은 변경이 아니다.


유연성은 항상 복잡성을 수반한다.

  • 유연하지 않은 설계는 단순하고 명확하다.
  • 유연한 설계는 복잡하고 암시적이다.
    • 설계가 유연할 수록 클래스 구조와 객체 구조사이의 거리는 점점 멀어진다.
  • 유연한 설계를 단순하고 명확하게 만드는 유일한 방법 → 긴밀한 커뮤니케이션 뿐.
  • 불필요한 유연성은 불필요한 복잡성을 낳는다.
    • 단순하고 명확한 해법이 그런대로 만족스럽다면 유연성을 제거하라.


5-2. 협력과 책임이 중요하다.

객체의 협력과 책임이 중요하다.

  • 설계를 유연하게 만들기 위해서는 협력에 참여하는 객체가 다른 객체에게 어떤 메시지를 전송하는지가 중요하다.
  • 중요한 비즈니스 로직을 처리하기 위해 책임을 할당하고 협력의 균형을 맞추는 것이
    객체 생성에 관한 책임을 할당하는 것보다 우선이다.
  • 객체를 생성하는 방법에 대한 결정은 모든 책임이 자리를 잡은 후 가장 마지막 시점에 내리는 것이 적절하다.


정리

  • 의존성을 관리해야 하는 이유는 역할, 책임, 협력의 관점에서 설계가 유연하고 재사용 가능하게 해야하기 때문이다.
  • 따라서 역할, 책임, 협력에 먼저 집중하라.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.