9장 유연한 설계

이번 장은 앞 8장에서 다룬 기법들을 실제 원칙에 따라 정리하고 메커니즘을 정리해준다.

개방-폐쇄 원칙

개방-폐쇄 원칙(Open-Closed Principle, OCP)은 “소프트웨어 개체는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.” 라는 원칙이다.

SOLID에 등장하지만, 대부분 암기식으로 외우거나 이유를 모른다. 아마 경험적인 지식이 부족하거나 필요성을 못 느낄 도메인만 접해서 그런것이라 생각한다.

여기서 키워드는 확장수정이다. 이 둘은 애플리케이션의 동작코드의 관점을 반영한다.

  • 확장에 대해 열려 있다: 애플리케이션의 요구사항이 변경될 때 이 변경에 맞게 새로운 동작을 추가해서 애플리케이션의 기능을 확장할 수 있다.
  • 수정에 대대해 닫혀 있다: 기존의 코드를 수정하지 않고도 애플리케이션의 동작을 추가하거나 변경할 수 있다.

어떻게 보면 유니티의 컴포넌트 패턴으로 제작된 컴포넌트 단위도 이에 충족한다고 볼 수 있다. 한 오브젝트에 대해서 조합 형식으로 확장에 대해서 열려 있고 수정에 대해서 닫혀 있는다는 코드가 아닌 툴 관점에서는 그러하다. 즉, 객체지향 사고 방식은 꼭 코드뿐만 아니라 유연한 설계 자체에 대한 이점이 있다.

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

사실 개방-폐쇄 원칙은 런타임 의존성과 컴파일 타임 의존성에 관한 이야기다. (쉽게 동적 모델과 정적 모델이라고 생각하면 된다.) 컴파일타임 의존성은 코드에서 드러나는 클래스들 사이의 관계다. 런타임과는 다른 구조를 가지며 이에 대한 이해는 앞장에서 다시 살펴볼 것

앞서 다룬 DiscountPolicy가 개방-폐쇄 원칙을 준수하는 코드라는 것이다. 실제로 새로운 기능을 확장하는 NoneDiscountPolicy를 추가할 때 DiscountPolicyMovie의 코드를 수정하지 않았다. 이를 확장에 대해서는 열려 있고, 수정에 대해서는 닫혀 있다라고 할 수 있다.

추상화가 핵심이다

개방-폐쇄 원칙의 핵심은 추상화에 의존하는 것이다. 여기서 추상화의존이라는 두 개념 모두가 중요하다.

추상화란 핵심적인 부분만 남기고 불필요한 부분은 생략함으로써 복잡성을 극복하는 기법이다. 추상화 과정을 거치면 문맥이 바뀌더라도 변하지 않는 부분만 남게 되고 문맥에 따라 변하는 부분은 생략된다. 추상화를 사용하면 생략된 부분을 문맥에 적합한 내용으로 채워넣음으로써 각 문맥에 적합하게 기능을 구체화하고 확장할 수 있다.

이 부분이 정말 중요하다고 생각한다. 추상화에 대해서 깊게 생각해봐야 하는 내용이며 이는 꼭 코드뿐만 아니라 우리가 쉽게 추상화하는 과정또한 여기에 대입할 수 있다. 그렇다면 우리는 이 글을 읽고 실제 코드에 어떻게 추상화를 나타내는지 생각해보자.

여기서 남는 추상화된 부분이 역할이라고 할 수 있을 것 같다.

개방-폐쇄 원칙의 관점에서 생략되지 않고 남겨지는 부분은 다양한 상황에서의 공통점을 반영한 추상화의 결과물이다. 공통적인 부분은 문맥이 바뀌더라도 변하지 않아야 한다. 다시 말해서 수정할 필요가 없어야 한다.

추상화 과정에서 주의할 점은 모든 수정에 대해서 폐쇄되는 것은 아니라는 점이다. 수정에 대해 닫혀 있고 확장에 대해 열려 있는 설계는 공짜로 얻어지지 않는다. 변경에 의한 파급효과를 최대한 피하기 위해서는 변하는 것과 변하지 않는 것이 무엇인지를 이해하고 이를 추상화의 목적으로 삼아야만 한다. 추상화가 수정에 대해 닫혀 있을 수 있는 이유는 변경되지 않을 부분을 신중하게 결정하고 올바른 추상화를 주의 깊게 선택했기 때문이다.

추가로 SOLID에서 리스코프를 제외한 나머지 원칙들도 같이 준수한다면 이 추상화수준도 같이 높아진다.

생성 사용 분리

Movie가 오직 DiscountPolicy라는 추상화에만 의존하기 위해서는 Movie내부에서 구체 클래스의 인스턴스를 생성하면 안된다. 이 구조는 앞서 다룬 개방-폐쇄 원칙도 위반하지만 다형성 개념 자체를 활용하지 못하게 된다.

결합도가 높아질수록 개방-폐쇄 원칙을 따르는 구조를 설계하기가 어려워진다. 알아야 하는 지식이 많으면 결합도도 높아진다. 특히 객체 생성에 대한 지식은 과도한 결합도를 초래하는 경향이 있다.(new)

물론 객체 생성 자체를 피할 수는 없으므로 어디선가는 반드시 객체를 생성해야 한다. (즉, 의존성을 챙겨야 한다.) 문제는 객체 생성이 아니라 부적절한 곳에서 생성한다는 것이 문제다.

유연하고 재사용 가능한 설계를 원한다면 객체와 관련된 두 가지 책임을 서로 다른 객체로 분리해야 한다. 하나는 객체를 생성하는 것이고, 다른 하나는 객체를 사용하는 것이다. 한마디로 객체에 대한 생성과 사용을 분리(Separating use from creation)해야 한다.

소프트웨어 시스템은 객체를 제작하고 의존성을 서로 연결하는 시작 단계와 이후 실행 단계를 구분해야 한다.

사용으로부터 생성을 분리하는 데 사용되는 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것이다.

여기서 클라이언트를 실제 클라이언트라고 이해하기 보다 게임에서는 한 function이나 모듈이 클라이언트가 될 수 있다. 즉, 어느 협력 관계의 root에 해당되는 부분으로 볼 수 있다. 지금은 단순한 모델이라 그렇지만 대부분은 모델간의 조합과 합성 그리고 협력으로 이뤄진다.

FACTORY 추가하기

생성 책임을 클라이언트로 옮긴 배경에는 Movie는 특정 컨텍스트에 묶여서는 안 되지만 Client는 묶여도 상관이 없다는 전제가 깔려 있다. 하지만 Movie를 사용하는 Client도 특정한 컨텍스트에 묶이지 않기를 바란다고 가정해보자. (위에서 말한 구조가 더 크다면.)

이렇게 되면 클라이언트도 하나의 책임을 지니게 되고 동일한 방법으로 다른 클라이언트로 확장하여 구조를 넓힐 수 있다. 하지만 객체 생성과 관련된 지식이 클라이언트와 협력하는 클라이언트에게까지 새어나가길 원하지 않는다면 생성과 관련된 책임만을 수행하는 별도의 객체를 추가하고 클라이언트가 이 객체를 사용하도록 만들 수 있다.

한 모듈을 벗어나는 것을 선호하지 않고, 생성과 사용을 분리하기 위해 생성이라는 로직을 담당하는 새로운 객체를 만든다.

이처럼 생성과 사용을 분리하기 위해 객체 생성에 특하된 객체를 FACTORY라고 부른다.

// MovieFactory가 더 적합하지 않을까..?
public class MovieFactory
{
    public Movie CreateAvatarMovie()
    {
        return new Movie("아바타", Duration.ofMinutes(120), Money.wons(10000), new AmountDiscountPolicy(...));
    }
}
public class Client
{
    private MovieFactory movieFactory;

    public Client(MovieFactory movieFactory)
    {
        this.movieFactory = movieFactory;
    }

    public Money GetAvatarFee()
    {
        Movie avatar = movieFactory.CreateAvatarMovie();
        return avatar.GetFee();
    }
}

이렇게 되면 MovieAmountDiscountPolicy를 생성하는 책임 모두를 FACTORY로 이동할 수 있다. 이제 Client에는 사용과 관련된 책임만 남게 되는데 하나는 FACTORY를 통해 생성된 Movie 객체를 얻기 위한 것이고 다른 하나는 Movie를 통해 가격을 계산하기 위한 것이다.

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

앞서 다룬 정보 전문가에게 책임을 할당하는 것을 생각해보면 도메인 모델은 이런 정보 전문가를 찾기 위해 참조할 수 있는 일차적인 재료이다. 하지만 Factory는 도메인 모델에 속하지 않으며 이를 추가한 이유는 순수하게 기술적인 결정이다. 전체적으로 결합도를 낮추고 재사용성을 높이기 위해 도메인 개념에게 할당돼 있던 객체 생성 책임을 도메인 개념과는 아무런 상관이 없는 가공의 객체로 이동시킨 것이다.

이 개념을 이해하기 위해선 쉽게 게임 제작에서 내리는 기술적 결정을 생각해보면 좋다. 예를 들어 게임 로직에 같이 들어가게 되는 사운드를 예로 코드 로직에 전혀 관련 없는 사운드 재생 함수가 들어가는 것에 대해서 전혀 상관없는 SoundManager가 생겨서 최소한의 코드로 동작하게 만드는 경우도 종종 있다.

크레이그 라만은 시스템을 분해하는 데는 크게 두 가지 방식이 존재한다고 설명한다. 하나는 표현적 분해(representational decomposition)이고 다른 하나는 행위적 분해(behavioral decomposition)다.

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

그러나 종종 도메인 개념을 표현하는 객체에게 책임을 할당하는 것만으로는 부족한 경우가 발생한다. 도메인 모델은 설계를 위한 중요한 출발점이지만 종착지는 아니다. 단지 출발점이라는 사실을 명심해야 한다. 실제로 동작하는 애플리케이션은 데이터베이스 접근을 위한 객체나, 서버와 통신을 위한 객체등 도메인 개념을 초월하는 기계적인 개념들을 필요로 한다.

모든 책임을 도메인 객체에게 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하와 같은 심각한 문제점에 봉착하게 될 가능성이 높아진다. 이 경우 도메인 개념을 표현한 객체가 아닌 설계자가 편의를 위해 임의로 만들어낸 가공의 객체에게 책임을 할당하여 문제를 해결해야 한다.

이처럼 책임을 할당하기 위해 창조되는 도메인과 무관한 인공적인 객체를 PURE FABRICATION(순수한 가공물)이라고 부른다. 어떤 행동을 추가하려고 하는데 이 행동을 마땅한 도메인 개념이 존재하지 않는다면 PURE FABRICATION을 추가하고 이 객체에게 책임을 할당하라.

유니트는 아마 GameManager의 형태로 이런 PURE FABRICATION을 모아두는 것 같고, 언리얼은 애초에 다 만들어뒀다. (게임 모드, 스테이트 등등)

이런 측면에서 객체지향의 사실과 오해에서 강조한 ‘실세계의 모방’이라는 말은 옳지 않으며 ‘실세계의 은유’가 적합하다. 실제 객체지향 애플리케이션들은 대부분 설계자들이 임의적으로 창조한 인공적인 추상화들이 포함되어 있다.

먼저 도메인의 본질적인 개념을 표현하는 추상화를 이용해 애플리케이션을 구축하기 시작하라. 만약 도메인 개념이 만족스럽지 못하다면 주저하지 말고 인공적인 객체를 창조하라. 객체지향이 실세계를 모방해야 한다는 헛된 주장에 현혹될 필요가 없다.

의존성 주입

“의존성 주입”은 5센트 개념을 25달러로 표현한 용어입니다.

사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법을 의존성 주입이라고 한다. 이 기법을 의존성 주입이라고 부르는 이유는 외부에서 의존성의 대상을 해결한 후 이를 사용하는 객체 쪽으로 주입하기 때문이다.

C++은 헤더에서 전방선언과 포인터 객체로 이를 비슷하게 보여준다.

앞 장에서도 말했지만 디폴트가 되는 기본 객체를 생성자 주입으로 활용하고 이후 Setter를 통해 다형성을 열어두는 것이 좋은 방법이다.

역시 트레이드 오프이지만 다형성이 필요없다면 DI자체도 고려 대상이다. 유니티 툴은 자율적인 라이프서클을 가지기에 오히려 Init을 계층적으로 따라가는 것이 좋지 못할 수 있다.

숨겨진 의존성은 나쁘다

의존성 주입 외에도 의존성을 해결할 수 있는 다양한 방법이 존재한다. 그중에서도 널리 사용되는 대표적인 방법은 SERVICE LOCATOR패턴이다. 서비스 로케이터 패턴은 의존성을 해결할 객체들을 보관하는 일종의 저장소다. 외부에서 객체에게 의존성을 전달하는 의존성 주입과 달리 직접 의존성을 해결해줄 것을 요청한다.

이 패턴에 대해서 많은 논의가 있다. 싱글톤 vs 서비스 로케이터 vs 의존성 주입이렇게 3가지를 가지고 대부분 뭐가 더 적합하냐에 대한 논의가 많이 일어난다.

하지만 서비스로케이터 패턴은 앞 장에서 말한 의존성을 감추기 때문에 충분히 유연하지 못하다는 것이다. 이를 수정하거나 확인하기 위해선 해당 객체를 찾아가야 하기 때문에 의존성을 명확하게 알 수 없다.

이런 코드는 단위 테스트 작성도 어렵고 정적 변수가 들어가는 순간 테스트 코드의 작성 난이도는 어려워지고, 쉽게 말하면 라이프서클이나 경합현상에 대해서 머리가 아파진다.

이는 꼭 서비스로케이터에 국한된 것이 아닌 싱글톤 패턴의 고질적인 문제점으로도 이어진다. 테스트와 디버깅을 어렵게 만들고 시간이 지날수록 굳어지는 코드를 만들어낸다.

반면에 의존성 주입은 이런 문제를 깔끔하게 해결한다. 필요한 의존성은 클래스의 퍼블릭 인터페이스에 명시적으로 드러나며, 의존성을 이해하기 위해 코드 내부를 읽을 필요가 없다. 즉, 단단하게 보호된다.

이 이야기의 핵심은 서비스 로케이터 패턴이 안좋다거나 좋다는 것이 아닌 명시적인 의존성이 숨겨진 의존성보다 좋다는 것이다. 가급적 의존성을 객체의 퍼블릭 인터페이스에 노출하자.

모든 패턴은 사용가치가 있기 때문에 존재하는 것이다. 그것이 안좋다면 진작에 이름조차 지어지지 못했을 것이기에 어딘가에선 쓸모가 있고 필요로 하기 때문이다. 서비스로케이터 패턴도 깊은 뎁스를 가져서 계속해서 전달해야 하는 고충이 있다면 사용하는 것도 고려해야 한다.

유니티에선 이런 뎁스의 전달은 툴 자체의 리플렉션 기능인 시리얼라이즈로 해결할 수 있다. 이는 유니티의 특징이기도 하다. 하지만 게임에 관련된 하나의 로직을 다수의 객체가 참조하여 활용해야 할 때는 마찬가지로 싱글톤도 틀리지 않았다.

의존성 역전 원칙

추상화와 의존성 역전

객체 사이의 협력이 존재할 때 그 협력의 본질을 담고 있는 것은 상위 수준의 정책이다. MovieAmountDiscountPolicy 사이의 협력이 가지는 본질은 영화의 가격을 계산하는 것이다. (Movie라는 객체가 협력을 위해 메시지를 전달할 때의 과정을 떠올리자.) 다시 말해서 어떤 협력에서 중요한 정책이나 의사결정, 비즈니스의 본질을 담고 있는 것은 상위 수준의 클래스다.

그러나 이런 상위 수준의 클래스가 하위 수준의 클래스에 의존한다면 하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받게 될 것이다. 정리하면 하위 수준의 AmountDiscountPolicyPercentageDiscountPolicy로 변경한다고 해서 상위 수준인 Movie가 영향을 받으면 안된다. 상위 수준의 Movie의 변경에 따라 하위 수준의 AmountDiscountPolicy가 영향을 받아야 한다.

대부분의 경우 우리가 재사용하려는 대상은 상위 수준의 클래스라는 점을 기억하자. 상위 수준의 클래스가 하위 수준의 클래스에 의존하면 상위 수준의 클래스를 재사용할 때 하위 수준의 클래스도 필요하기 때문에 재사용하기가 어려워진다.

중요한 것은 상위 수준의 클래스다. 상위 수준의 변경에 의해 하위 수준의 변경되는 것은 납득할 수 있지만 하위 수준의 변경으로 인해 상위 수준이 변경돼서는 곤란하다. 하위 수준의 이슈로 인해 상위 수준에 위치하는 클래스들은 재사용하는 것이 어렵다면 이것 역시 문제가 된다.

이 경우에도 해결사는 추상화다. 추상화에 의존하도록 수정하면 하위 수준 클래스의 변경으로 인해 상위 수준의 클래스가 영향을 받는 것을 방지할 수 있다. 또한 상위 수준을 재사용할 때 하위 수준의 클래스에 얽매이지 않고도 다양한 컨텍스트에서 재사용이 가능하다.

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

이 내용을 읽어보니 저번 논의 때 이야기 나눈 1대1 인터페이스 매칭에 대해서 객체 간의 관계가 수평적이지 않고 하위와 상위 모듈로 나뉜다면 추상화 수준을 올리기 위해 1대1 인터페이스 매칭도 올바른 형태가 될 수 있다고 생각하게 된 것 같다. ex) PlayerController -> PlayerJump

이를 의존성 역전 원칙(Dependency Inversion Principle, DIP)이라고 한다. 의존성 역전 원칙은 상위 수준의 모듈이 하위 수준의 모듈에 의존하지 않도록 하기 위해 추상화를 도입하라는 원칙이다. 역전이라는 단어가 들어간 이유는 의존성 방향이 전통적인 절차형 프로그래밍과는 반대 방향으로 나타나기 때문이다.

의존성 역전 원칙과 패키지

역전은 의존성의 방향뿐만 아니라 인터페이스의 소유권에도 적용된다는 것이다. 객체지향 프로그래밍 언어에서 어떤 구성 요소의 소유권을 결정하는 것은 모듈이다. (C#은 네임스페이스가 모듈이라고 생각하면 된다. 유니티에선 어셈블리)

Movie가 정상적으로 컴파일 되기 위해서는 DiscountPolicy 클래스가 필요하다. 사실 코드의 컴파일이 성공하기 위해 함께 존재해야 하는 코드를 정의하는 것이 바로 컴파일타임 의존성이다. 문제는 DiscountPolicy가 포함돼 있는 패키지안에 AmountDiscountPolicy, PercentageDiscountPolicy가 포함되어 있다는 것이다. 이것은 DiscountPolicy클래스에 의존하기 위해서는 반드시 같은 패키지에 포함된 AmountDiscountPolicy, PercentageDiscountPolicy 클래스도 함께 존재해야 한다는 것이다.

이런 패키지 구조에 따라 전체적인 빌드 시간이 가파르게 상승한다. 따라서 MovieDiscountPolicy을 같은 패키지로 모으고, AmountDiscountPolicy, PercentageDiscountPolicy를 다른 패키지로 분리하는 것이 좋다.

이렇게 패키지를 분리하면 특정한 컨텍스트로 부터 완전히 독립하게 되고, 다른 컨텍스트에서 재사용하기 위해서는 단지 MovieDiscountPolicy가 포함된 패키지만 재사용하면 된다.

따라서 의존성 역전 원칙에 따라 상위 수준의 협력 흐름을 재사용하기 위해서는 추상화가 제공하는 인터페이스의 소유권 역시 역전시켜야 한다.

정리하자면, 유연하고 재사용 가능하며 컨텍스트에 독립적인 설계는 전통적인 패러다임이 고수하는 의존성의 방향을 역전시킨다. 전통적인 패러다임에서는 상위 수준 모듈이 하위 수준 모듈에 의존했다면 객체지향 패러다임에서는 상위 수준 모듈과 하위 수준 모듈이 모두 추상화에 의존한다.

유연성에 대한 조언

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

유연하고 재사용 가능한 설계란 런타임 의존성과 컴파일타임 의존성의 차이를 인식하고 동일한 컴파일 타임 의존성으로부터 다양한 런타임 의존성을 만들 수 있는 코드 구조를 가지는 설계를 의미한다. 하지만 유연하고 재사용 가능한 설계가 항상 좋은 것은 아니며, 설계의 미덕은 단순함과 명확함으로부터 나온다.

단순하고 명확한 설계를 가진 코드는 읽기 쉽고 이해하기도 편하다. 유연한 설계는 이와는 다른 길을 걷는다. 변경하기 쉽고 확장하기 쉬운 구조를 만들기 위해서는 단순함과 명확함의 미덕을 버리게 될 가능성이 높다.

이 책을 관통하는 말이며, 단순한 도메인에서는 오히려 객체지향이 더 좋지 않을 수 있다.

유연한 설계라는 말의 이면에는 복잡한 설계라는 의미가 숨어 있다. 유연한 설계의 이런 양면성은 객관적으로 설계를 판단하기 어렵게 만든다. 이 설계가 복잡한 이유는 무언인지, 어떤 변경에 대비하기 위해 설계를 복잡하게 만들었는지, 유연성이 정말로 필요한지, 이런 질문들은 제한적인 상황에서 공학적인 판단보다는 심리학적 판단에 더 가깝다.

실제로 사람마다 다른 관점을 가지기에 소프트웨어 품질은 스스로밖에 높이지 못한다.

미래에 변경이 일어날지도 모른다는 막연한 불안감은 불필요하게 복잡한 설계를 가져온다. 아직 일어나지 않은 변경은 변경이 아니다.

유연성은 항상 복잡함을 가져온다. 유연하지 않은 설계는 단순하고 명확하다. 유연한 설계는 복잡하고 암시적이다. 객체지향에 입문하게 된 개발자들이 가장 이해하기 어려워하는 부분이 바로 코드 상에 표현된 정적인 클래스의 구조와 실행 시점의 동적인 객체 구조가 다르다는 사실이다.

절차적인 프로그래밍 방식으로 작성된 코드는 코드에 표현된 정적인 구조가 곧 실행 시점의 동적인 구조를 의미한다. 객체지향 코드에서 클래스 구조는 발생 가능한 모든 객체를 담는 틀일 뿐이다. 특정 시점의 객체 구조를 파악하는 유일한 방법은 클래스를 사용하는 클라이언트 코드 내에서 객체를 생성하거나 변경하는 부분을 직접 찾아보는 것이다.

설계가 유연할수록 클래스 구조와 객체 구조 사이의 거리는 점점 멀어진다. 따라서 유연함은 단순성과 명확성의 희생 위에서 자라난다. 유연한 설계를 단순하고 명확하게 만드는 유일한 방법은 사람들의 커뮤니케이션뿐이다. 복잡성이 필요한 이유와 합리적인 근거를 제시하지 않는다면 누구도 설계를 만족스러운 해법으로 받아들이지 않을 것이다.

이 문단도 이 책에서 가장 중요한 부분이라고 생각한다.

불필요한 유연성은 불필요한 복잡성을 낳는다. 단순하고 명확한 해법이 그런대로 만족스럽다면 유연성을 제거하라. 유연성은 코드를 읽는 사람들이 복잡함을 수용할 수 있을 때만 가치가 있다. 하지만 복잡성에 대한 걱정보다 유연하고 재사용 가능한 설계의 필요성이 더 크다면 코드의 구조와 실행 구조를 다르게 만들어라.

협력과 책임이 중요하다

마지막은 결국 객체의 협력과 책임이라는 것이다. 지금까지 클래스를 중심으로 구현 메커니즘 관점에서 설명했지만 설계를 유연하게 만들기 위해서는 협력에 참여하는 객체가 다른 객체에게 어떤 메시지를 전송하는지가 중요하다.

Movie가 다양한 할인 정책과 협력할 수 있는 이유는 모든 할인 정책이 Movie에게 전송하는 CalculateDiscountAmount메시지를 이해할 수 있기 때문이다. 이들 모두 요금을 계산하기 위한 협력에 참여하면서 할인 요금을 계산하는 책임을 수행할 수 있으며 Movie의 입장에서 동일한 역할을 수행할 수 있다.

설계를 유연하게 만들기 위해서는 먼저 역할, 책임, 협력에 초점을 맞춰야 한다. 다양한 컨텍스트에서 협력을 재사용할 필요가 없다면 설계를 유연하게 만들 당위성도 함께 사라진다. 객체들이 메시지 전공자의 관점에서 동일한 책임을 수행하는지 여부를 판단할 수 없다면 공통의 추상화를 도출할 수 없다.

대부분의 초보자가 저지르는 실수 중 하나는 객체의 역할과 책임이 자리를 잡기 전에 너무 성급하게 객체 생성에 집중하는 것이다. 이것은 객체 생성과 관련된 불필요한 세부사항에 객체를 결합시킨다. 객체를 생성할 책임을 담당할 객체나 객체 생성 메커니즘을 결정하는 시점은 책임 할당의 마지막 단계로 미뤄야만 한다. 중요한 비즈니스 로직을 처리하기 위해 책임을 할당하고 협력의 균형을 맞추는 것이 객체 생성에 관한 책임을 할당하는 것보다 우선이다.

책임의 불균형이 심화되고 있는 상태에서 객체의 생성 책임을 지우는 것은 설계를 하부의 특정한 메커니즘에 종속적으로 만들 확률이 높다. 불필요한 싱글톤은 객체 생성에 관해 너무 이른 시기에 고민하고 결정할 때 도입되는 경향이 있다.

의존성을 관리해야 하는 이유는 역할, 책임, 협력의 관점에서 설계가 유연하고 재사용 가능해야 하기 때문이다. 따라서 역할, 책임, 협력에 먼저 집중하라. 다양한 기법들을 적용하기 전에 역할, 책임, 협력의 모습이 선명하게 그려지지 않는다면 의존성을 관리하는 데 들이는 모든 노력이 물거품이 될 수도 있다는 사실을 명심하라.

느낀점

8장의 내용을 원칙에 맞게 설명하는 내용으로 이러한 기법이나 원칙들에 대해서 자세하게 설명해준다.

단순하고 명확한 설계를 가진 코드는 읽기 쉽고 이해하기도 편하다. 유연한 설계는 이와는 다른 길을 걷는다. 변경하기 쉽고 확장하기 쉬운 구조를 만들기 위해서는 단순함과 명확함의 미덕을 버리게 될 가능성이 높다.

가장 중요하게 생각해야 하는 부분이다. 이 책을 읽고 쉽게 객체지향무새가 되지 않도록 명심해야 한다.

논의사항

싱글톤에 대해서 논의를 하고 싶지만.. 너무 오래걸릴 것 같아서.. 다음에..

  • 의존성 주입에 대한 논의가 있으면 좋을 것 같습니다. 의존성 주입에서 가장 중요하다고 판단되는 것은 어떤것일까요?
    • 저는 의존성 주입의 가장 중요한 부분은 책에 나온 컴파일타임 의존성을 런타임 의존성으로 변경하는 것처럼 동적인 형태에서 DI가 더 중요한 부분을 보이는 것 같습니다. 같은 맥락으로 ReadOnly에 대한 내용으로 연결이 되네요

댓글남기기