유연한 설계

1. Open-Closed Principal, OCP

  • 키워드는 확장, 수정
  • 확장에 대해서 열려 있다는 말은 요구 사항 변경에 따라 새로운 동작을 추가해서 애플리케이션 확장을 할 수 있다는 의미
  • 수정에 대해서 닫혀 있다는 말은 기존 코드 수정 없이 새로운 동작을 추가/변경할 수 있다는 의미

추상화

  • OCP의 핵심은 추상화에 의존하는 것이다.
  • 추상화는 문맥에 바뀌더라도 변하지 않는 부분만 남으므로 문맥에 따라 변하는 부분은 생략된다.
  • OCP 관점에서 생략되지 않고 남겨지는 부분은 다양한 상황에서 공통점을 반영한 추상화의 결과물이다.
  • 공통적인 부분은 문맥에 바뀌더라도 변하지 않아야 한다.
  • 물론 개념 추상화로 수정에 대해서 닫혀 있는 설계를 만들 수 있는 것은 아니다.
  • OCP에서 폐쇄를 가능하게 하는 것은 의존성의 방향이다.
  • 수정에 대한 영향을 최소화하기 위해서는 모든 요소가 추상화에 의존해야 한다.
  • 또한 변하는 것과 변하지 않는 것이 무엇인지를 이해하고 이를 추상화 목적으로 삼아야 한다.

생성과 사용의 분리

  • 결합도가 높아질수록 OCP를 따르는 구조를 설계하기 어려워진다.
  • 객체 생성 자체는 피할 수 없다. 보통의 문제는 부적절한 곳에서 생성하는 것이 문제다.
  • 예를 들어 동일한 클래스 안에서 객체 생성과 사용이라는 이질적인 두 가지 목적을 가진 코드가 공존하면 문제가 된다.
  • 즉, 객체 생성과 사용을 분리(seperating use from creation)을 해야 한다.
  • 이를 위한 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것이다.
    • 혹은 Factory를 추가하는 방법이 있다.
    • 객체 생성과 관련된 책임만 전담하는 별도의 객체를 추가하고 Client는 이를 사용하도록 만들 수 있다.

순수한 가공물에 책임 할당

  • 책임 할당의 기본은 InformationExpert에게 책임을 할당하는 것이다.
  • 어떤 책임을 할당하고 싶다면 제일 먼저 도메인 모델 안에서 개념 중에서 적절한 후보가 존재하는 지 찾아야 한다.
  • 위의 Factory는 순수 기술적인 이유다. 전체적인 결합도를 낮추고 재사용성을 높이기 위해서 도메인에 할당되어 있던 객체 생성 책임을 도메인 개념과는 아무런 상관 없는 가공의 객체로 이동시킨 것이다.
  • 객체 분해에는 두 가지 방식이 존재 한다.
    1. 표현적 분해(representational decomposition)
      • 도메인에 존재하는 사물 도는 개념을 표현하는 객체들을 이용해서 시스템을 분리
      • 도메인 모델에 담겨 있는 개념과 관계를 따르며 SW와 도메인간의 간극을 메우는 것을 목적으로 한다.
    2. 행위적 분해(behavioral decomposition) :
      • 물론 표현적 분해에 한계가 있을 수 있다.
      • 설계자 편의를 위해서 만드는 순수 기술적인 가공물인 PureFacbrication이 생기기도 한다.
      • 예를 들어 어떤 행동을 추가하려고 하는데 이를 책임질 마땅한 도메인이 없다면 PureFacbrication를 만들고 이 객체에 책임을 준다. 이는 표현적 분해보다 행위적 분해에 가깝다.

        PureFabrication

        • 객체 지향은 도메인 상의 개념을 SW로 구현하고 책임을 할당
        • 만약 도메인이 이런 책임을 할당하여 응집도, 결합도 문제가 생긴다면?
        • 도메인 개념을 표현하지 않는, 인위적으로 또는 편의상 만든 클래스에 매우 응집된 책임을 할당하자
        • 도메인 상에 존재하지는 않지만 순수하게 설계 품질을 높이기 위해서 생성되는 가공물이다.

의존성 주입

  • 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법을 DependencyInjection이라고 한다.
  • constructor injection
  • setter injection
  • method injection

숨겨진 의존성 지양

  • 일전에 숨겨진 의존성을 지양해야 한다고 했다.
  • 이를 해결할 수 있는 방법은 ServiceLocator 패턴이 있다. 이는 의존성을 해결할 객체들을 보관하는 일종의 저장소다.
  • 의존성이 필요할 경우 ServiceLocator에 의존성 해결을 요청하는 방식으로 진행된다.
  • 그러나 이 역시 문제가 있을 수 있다. 의존성을 구현 내부로 감추고 이는 컴파일이 아닌 런타임에 발견되는 문제가 있다.

DIP

  • 객체 사이 협력이 존재할 때 그 협력의 본질을 담고 있는 것은 상위 수준의 정책이다.
  • 상위 수준의 변경에 의해 하위 수준에 변경되는 것은 문제가 없지만 반대는 문제가 된다.
  • 추상화를 통해서 하위의 변경으로 상위에 영향이 가는 것을 막을 수 있다.
    • 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다. 둘 다 추상화에 의존해야 한다.
    • 추상화는 구체적인 사항에 의존해서는 안된다. 구체적인 사항은 추상화에 의존해야 한다.

DIP와 소유권

  • DIP와 함께 논의할 내용은 인터페이스 소유권에 대해서다.
  • 과연 구현하는 객체 사이에 인터페이스가 있어야 하는가? 아니면 사용하는 객체 사이에 있어야 하는가?
  • 정답은 후자다. 추상화를 별도 독립적인 패키지가 아니라 클라이언트가 속한 패키지에 포함시켜야 한다. - 이를 Seperated interface 패턴이라고 한다.
  • 추상화가 제공하는 인터페이스 소유권을 역전 시켜야 한다.

유연성

  • 재사용 가능한 설계가 항상 정답은 아니다.
  • 설계는 단순함과 명확함으로부터 나오고 이는 가독성으로 이어진다.
  • 유연한 설계는 필연적으로 복잡해진다. 설계가 유연할수록 클래스 구조와 객체 구조 사이의 거리가 멀어진다.
  • 유연함은 단순성과 명확성을 희생할 수밖에 없다.
  • 복잡성이 필요한 이유와 합리적 근거가 없다면 굳이 복잡성을 감수하면서 유연한 설계를 할 필요는 없다.
  • 돌아가서 뭐 어찌됐든 협력과 책임이 가장 주용하다. 설계를 유연하게 하기 위해서는 역할, 책임, 협력에 초점을 맞추고 들어가야 한다.
  • 책임을 할당하고 협력의 균형을 맞추는 것이 객체 생성에 관한 책임을 할당하는 것보다 우선이다.
  • 책임 관점에서 객체들 간에 균형이 잡혀 있는 상태라면 생성과 관련된 책임을 지게 될 객체를 선택하는 것이 간단한 작업이 된다.
  • 결과적으로 객체 생성 방법에 대한 결정은 모든 책임이 자리잡은 후에 내리는 것이 좋다.
  • “객체가 무엇이 되고 싶은지 알게 될 때까지 객체들을 어떻게 인스턴스화 할 것인지에 대해서 전혀 신경쓰지 말자”