본문 바로가기
독서 기록

[오브젝트] 9장 - 유연한 설계

by 워냥 2024. 6. 15.


1. 개요

8장에서는 의존성에 대해 살펴보고 여러 의존성 관리 기법들을 알아보았다.
이번 장에서는 의존성을 관리하기 위한 몇 가지 원칙을 배우게 된다.


2. 개방-폐쇄 원칙

개방-폐쇄 원칙

개방-폐쇄 원칙(Open-Closed Principle, OCP)은 '모든 개체는 확장에 대해 열려 있고, 수정에 대해 닫혀 있어야 한다'는 원칙이다.

 

여기서 확장은 애플리케이션에 새로운 동작이 추가되어 기능이 늘어나는 것을 의미한다.
수정은 기존 코드가 수정되는 것을 의미한다.

런타임 의존성, 컴파일타임 의존성과의 관계

앞서 컴파일타임은 코드 레벨과 관련이 깊다는 것을 보았다.
개방-폐쇄 원칙은 '컴파일타임 의존성을 수정하지 않고도 런타임 의존성을 변경할 수 있음'으로도 볼 수 있다.

 

런타임 의존성과 컴파일타임 의존성을 구분하기 위해 추상화를 사용했다.
개방-폐쇄 원칙도 추상화를 사용하여 달성할 수 있다.

 

코드에서 핵심적인 부분을 제외하고는 추상화를 사용하여 생략한다.
생략된 부분은 확장이 가능해지고, 핵심적인 부분은 폐쇄되게 된다.

 

여기서 핵심적인 부분과 생략할 부분은 신중하게 결정해야 한다.


3. 생성과 사용의 분리

객체의 생성과 사용

결합도가 높아지면 개방-폐쇄 원칙을 준수하기 어려워진다.
결합도를 낮추기 위해 객체의 생성과 사용을 분리해야 한다.

public class Movie {
    ...
    public Movie(String title, Duration runningTime, Money fee) {
        ...
        this.discountPolicy = new AmountDiscountPolicy(...); // 객체 생성
    }

    public Money calculateMovieFee(...) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening)); // 객체 사용
    }
}

위 코드에서 한 클래스에 객체 생성과 사용이 모두 일어난다.
이 경우 Movie 객체와 AmountDiscountPolicy 객체 사이의 결합도가 높으므로 변경이 어렵다.
따라서 객체 생성의 책임을 분리해야 한다.

 

객체 생성 책임 분리

객체 생성 책임을 분리하기 위한 첫 번째 방법은 클라이언트가 객체를 생성하는 것이다.
객체를 호출하는 클라이언트 측에서 객체를 생성하고 전달한다.

public class Client {
    public Money getAvatarFee() {
        Movie avatar = new Movie(..., new AmountDiscountPolicy(...));  // 객체 생성
        return avatar.getFee();
    }
}

public class Movie {
    ...
    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy
    }

    public Money calculateMovieFee(...) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening)); // 객체 사용
    }
}

 

두 번째 방법은 Factory 패턴을 사용하는 것이다.
Factory는 객체의 생성에 특화된 객체이다.

public class Factory {
    public Movie createAvatarMovie() {
        return new Movie(..., new AmountDiscountPolicy(...));  // 객체 생성
    }
}

public class Client {
    public Money getAvatarFee() {
        Movie avatar = factory.createAvatarMovie();
        return avatar.getFee(); // 객체 사용
    }
}

 

순수한 가공물

지금까지 시스템을 객체로 분해할 때 도메인에 존재하는 사물, 개념을 기준으로 분해하였다.
하지만 실제로 설계를 진행하다 보면 도메인 내의 개념만으로 책임을 나누기에는 객체의 수가 부족해진다.

 

충분히 객체를 나누기 위해 도메인 내의 개념 외의 기계적 개념이 필요하다.
이런 기계적 개념을 순수한 가공물(Pure Fabrication)이라고 부른다.

 

순수한 가공물은 도메인과는 무관한 인공적인 객체이다.
이 인공적인 객체는 오직 책임을 분산시키기 위해 제작되었다.
보통 도메인의 사물이나 개념이 아닌, 특정한 행위를 표현한다.

 

위에서 살펴본 Factory도 도메인과는 무관한 순수한 가공물이다.

 

표현적 분해와 행위적 분해

기존 도메인 내에 존재하는 사물과 개념을 사용해 시스템을 분해하는 방식을 표현적 분해(representational decomposition)라고 한다.
반대로 순수한 가공물처럼 어떤 행위를 기준으로 시스템을 분해하는 방식을 행위적 분해(behavioral decomposition)라고 한다.

 

객체지향이 실세계의 모방이 아닌 이유가 바로 표현적 분해 외에도 행위적 분해가 존재하기 때문이다.
기존 도메인 개념과 책임을 적절히 분산시키기 위한 인공적인 객체가 모두 모인 것이 객체지향이다.


3. 의존성 주입

유연한 설계를 위해서 생성과 사용을 분리해야 한다고 했다.
의존성 주입(Dependency Injection) 은 생성과 사용의 분리를 달성하기 위해 외부의 독립적인 객체가 인스턴스를 생성한 뒤 이를 전달해 주는 행위를 의미한다.

 

8장에서 살펴본 의존성 해결 방법처럼 의존성 주입도 세 가지 방법이 존재한다.

  • 생성자 주입: 객체 생명 주기 전체에 걸쳐 관계 유지
  • Setter 주입: 의존성 대상을 런타임에 변경 가능
  • 메서드 주입: 필요한 경우에만 의존

4. 의존성 역전 원칙

의존성 역전 원칙(Dependency Injection) 은 다음과 같다.

  1. 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다.
  2. 추상화는 구체적인 사항에 의존해서는 안 된다. 구체적인 사항은 추상화에 의존해야 한다.

추상화에 의존하도록 하면 불필요한 클래스가 영향을 받는 것을 방지할 수 있다.


5. 느낀 점

이번 장에서는 의존성을 관리하기 위한 몇 가지 원칙을 배웠다.
그동안 개념적인 부분이 강했지만 이제는 실제 패턴을 몇 가지 배우고 있는 것 같다.

 

이번 장 마지막 파트인 '유연성에 대한 조언'이 특히 인상 깊었다.

유연한 설계는 곧 복잡한 설계다.
미래에 변경이 일어날지 모른다는 막연한 불안감은 불필요하게 복잡한 설계를 낳는다.
변경은 예상이 아니라 현실이어야 한다.

 

객체지향 원칙을 준수하기 위한 다양한 패턴을 배울수록, 이를 실제 코드에 적용하고 싶다는 욕구가 생긴다.
하지만 저자는 이러한 패턴도 복잡함이라는 trade-off가 생긴다는 사실을 강조하고 있다.

 

본질은 패턴이 아닌 유연한 설계에 있음을 항상 명심해야 한다.
무작정 여러 원칙을 적용하는 것이 아닌, 이 원칙이 어떤 득과 실을 줄 지 확인하고 꼭 필요할 때만 적용하도록 하자.

댓글