9. 유연한 설계
이전 포스팅에서 다양한 의존성 관리 기법들을 다루어보았다.
이번엔 이 기법들을 원칙의 관점에서 정리해보도록 하자.
9.1. 개방-폐쇄 원칙
로버트 마틴이 정리한 객체지향의 5원칙 중 하나로 개방-폐쇄 원칙(OCPOpen-Closed Principle) 이 있다.
개방-폐쇄 원칙을 한 줄로 정리하면 클래스, 모듈, 함수 등 소프트웨어 개체는 확장에 대해서는 열려있어야하고, 수정에 대해서는 닫혀있어야 한다 는 원칙이다.
확장에 대해 열려있다는 것은 애플리케이션의 동작 관점을 반영한 것으로 요구사항이 변경될 때 새로운 동작을 추가해서 애플리케이션의 기능을 확장한다는 것이며,
수정에 대해 닫혀있다는 것은 애플리케이션의 코드 관점을 반영한 것으로 기존의 코드를 수정하지 않도고 애플리케이션의 동작을 추가하거나 변경할 수 있다는 뜻이다.
어떻게 코드를 수정하지않고 새로운 동작을 추가할 수 있을까?
사실 개방-폐쇄 원칙은 런타임 의존성과 컴파일타임 의존성에 관한 이야기로도 풀이된다.
유연하고 재사용 가능한 설계에서 런타임 의존성과 컴파일타임 의존성은 서로 다른 구조를 가지기 때문에 컴파일타임 의존성을 수정하지 않고, 런타임 의존성을 변경하여 요구사항을 충족할 수 있다.
개방-폐쇄 원칙의 관점에서 생략되지않고 남겨지는 부분은 다양한 상황에서의 공통점을 반영한 추상화의 결과물로,
공통부의 경우 문맥이 바뀌더라도 수정할 필요가 없어야 하기에 추상화 부분은 “닫혀” 있다고 볼 수 있다.
반대로 추상화 부분을 제외한 나머지는 다양한 문맥에 대응하기 위한 확장의 여지를 주기에 “열려” 있다고 볼 수는 것이다.
따라서 개방-폐쇄 원칙은 추상화의 의존하는 것이다.
9.2. 팩토리 - 생성과 사용의 분리
코드 혹은 모듈간 결합도가 높아질 수록 개방-폐쇄 원칙을 지키는 것은 점점 더 어려워진다.
특히 객체 생성에 대한 지식은 과도한 결합도를 초래하며, 객체의 타입과 생성자에 전달해야하는 인자에 대한 과도한 지식은 코드를 특정 문맥에 강결합시키는 원인이된다.
이때의 문맥을 수정하는 유일한 방법은 코드내 명시된 문맥에 대한 정보를 직접 수정하는 것 뿐이다.
하지만 이를 회피하기 위해서 객체를 아예 생성하지 않을수도 없는 없기에 객체에 대한 생성과 사용의 분리(separating use from creation) 를 적용해야 한다.
생성과 사용을 분리하는 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 이전하는 것이다.
만약 생성과 관련된 로직을 클라이언트도 모르게 하려면 객체를 생성하는 책임과 생성하는 방법을 결정할 수 있도록 팩토리를 활용할 수 있다.
위의 팩토리는 단순히 기술적인 결정으로 추가된 것으로, 결합도를 낮추고, 재사용성을 높이기 위한 가공의 객체일 뿐이라 도메인 레이어에 속하지는 않는다.
이런 식으로 시스템을 객체로 분해하는 데는 크게 표면적 분해(representational decomposition) 과 행위적 분해(behavioral decomposition) 이 존재한다.
표변적 분해는 도메인에 존재하는 사물 또는 개념들을 표현하는 객체들을 이용해 시스템을 분해하며, 도메인 모델에 내재된 개념과 관계를 추종하여 도메인과 소프트웨어 사이의 표현적 차이를 최소화하는 방법이다.
다만 도메인 개념을 표현하는 객체에게 책임을 할당하는 것으로는 부족하여, 도메인 개념들을 초월하는 기계적인 개념이 필요해지는 경우가 있다.
이처럼 도메인과 무관하게 책임을 할당하기 위해 만들어진 인공적 객체를 순수 가공물(Pure Facrication) 이라고 부른다.
순수 가공물은 어떤 행위를 추가하는 시점에 이 행동을 책임질 적절한 도메인 개념이 존재하지 않는 경우에 생성하면 된다.
따라서 순수 가공물은 일반적으로 표현적 분해보다 행위적 분해에 의해 생성된다.
9.3. 의존성 역전 원칙
재상용성 측면에서 상위 클래스와 하위 클래스 중 어떤 것이 더 중요할까?
더 볼 필요도 없이 상위 클래스이다.
상위 수준의 변경에 의해 하위 수준이 변경이 발생하는 것은 누구나 납득할 수 있지만,
하위 수준의 변경에 의해 상위 수준이 변경되는 것은 용납하기 힘든 일이기 때문이다.
따라서 하위 수준의 이슈로 인해 상위 수준의 클래스들을 재사용하기 어려워진다면 문제가 있다고 보는 것이다.
이 경우에도 추상화를 통해 해결할 수 있다.
상위 수준의 클래스와 하위 수준의 클래스가 모두 추상화의 의존하면 유연하고 재사용성이 좋은 설계를 달성할 수 있기 때문이다.
결론적으로 구체 클래스는 의존성의 시작점으로 남겨두고, 의존성의 목적지가 되지 않도록 처리해야 한다.
위 원칙을 의존성 역전 원칙(DIP : Dependency Inversion Principle) 이라고 부른다.
9.4. 유연성
유연하고 재사용 가능한 설계를 다시 정의해보자.
유연하고 재사용 가능한 설계란 런타임 의존성과 컴파일타임 의존성의 차이를 인지하고, 동일한 컴파일타임 의존성으로부터 다양한 런타임 의존성을 만들어낼 수 있는 구조를 가진 설계이다.
하지만 이 유연한 설계가 항상 좋은 것은 아니다.
변경하기 쉽고 확장하기 쉬운 구조란 결국 단순성과 명확성이 떨어질 가능성 또한 높이기 때문이다.
유연성은 항상 복잡성과 암시성을 수반하며, 유연한 설계는 클래스 구조와 객체 구조 사이의 거리를 벌리는 역할을 한다.
유연한 설계에 단순성과 명확성을 부여하는 방법은 결국 협업자간의 긴밀한 커뮤니케이션뿐이므로, 복잡성이 필요한 이유를 정확하게 제시하지 않는다면 모두가 만족하는 설계로 나아갈 수 없게 된다.