이번 장에서는 이전에 배웠던 개념들을 정리하는 시간을 가졌다. OCP(Open-Closed Principle), DIP(Dependency Inversion Principle) 등의 개념을 복습하면서 현재 진행 중인 프로젝트 설계가 떠올랐다. 나는 이를 적절하게 적용하고 있는가?
현재 구조는 하나의 컨트롤러가 서비스 구현체를 의존하고, 서비스 구현체는 인터페이스로 정의된 서비스를 의존하며, 각 인터페이스의 구현체가 핵심 비즈니스 로직을 담고 있다.
GroupController → GroupService → (interface)GroupCreateService → GroupCreateServiceImpl설계 단계에서 중간에 인터페이스를 두어 OCP를 적용했지만, 앞으로 트러블슈팅과 유지보수를 진행하면서 내가 설계한 방식이 변경에 용이한지, 그리고 팀원이 내 코드를 쉽게 이해할 수 있는지 피드백을 거치며 개선해볼 생각이다.
이번 챕터에서 특히 인상 깊었던 부분은 유연함이었다. 유연한 설계는 복잡도를 높이는 결과를 초래할 수 있기 때문에, 유연성을 도입할 때 효용을 신중하게 분석해야겠다고 느꼈다. 무조건적인 유연한 설계가 반드시 좋은 것은 아니라는 점을 다시 한번 유념하게 되었다.
로버트 마틴은 확장 가능하고 변화에 유연하게 대응할 수 있는 설계를 만들 수 있는 원칙 중 하나로 개방-폐쇄 원칙Open-Closed Principle, OCP을 고안했다.
여기서 키워드는 '확장'과 '수정'이다. 이 둘은 순서대로 애플리케이션의 '동작'과 '코드'의 관점을 반영한다.
개방-폐쇄 원칙은 유연한 설계란 기존의 코드를 수정하지 않고도 애플리케이션의 동작을 확장할 수 있는 설계라고 이야기한다.
OCP 원칙의 핵심은 추상화에 의존하는 것이다. 여기서 '추상화'와 '의존'이라는 두 개념 모두가 중요하다.
추상화란 핵심적인 부분만 남기고 불필요한 부분은 생략함으로써 복잡성을 극복하는 기법이다. 추상화 과정을 거치면 문맥이 바뀌더라도 변하지 않는 부분만 남게 되고 문맥에 따라 변하는 부분은 생략된다. 추상화를 사용하면 생략된 부분을 문맥에 적합한 내용으로 채워넣음으로써 각 문맥에 적합하게 기능을 구체화하고 확장할 수 있다.
OCP원칙의 관점에서 생략되지 않고 남겨지는 부분은 다양한 상황에서의 공통점을 반영한 추상화의 결과물이다. 공통적인 부분은 문맥이 바뀌더라도 변하지 않아야 한다. 다시 말해서 수정할 필요가 없어야 한다. 따라서 추상화 부분은 수정에 대해 닫혀 있고 추상화를 통해 생략된 부분은 확장의 여지를 남긴다. 이것이 추상화가 OCP 원칙을 가능하게 만드는 이유다.
단순히 어떤 개념을 추상화했다고 해서 수정에 대해 닫혀 있는 설계를 만들 수 있는 것은 아니다. OCP 원칙에서 폐쇄를 가능하게 하는 것은 의존성의 방향이다. 수정에 대한 영향을 최소화하기 위해서는 모든 요소가 추상화에 의존해야 한다.
결합도가 높아질수록 OCP 원칙을 따르는 구조를 설계하기 어려워진다. 알아야 하는 지식이 많으면 결합도도 높아진다. 특히 객체 생성에 대한 지식은 과도한 결합도를 초래하는 경향이 있다. 객체의 타입과 생성자에 전달해야 하는 인자에 대한 과도한 지식은 특정한 컨텍스트에 강하게 결합시킨다. 컨텍스트를 바꾸기 위한 유일한 방법은 코드 안에 명시돼 있는 컨텍스트에 대한 정보를 직접 수정하는 것뿐이다.
유연하고 재사용 가능한 설계를 원한다면 객체와 관련된 두 가지 책임을 서로 다른 객체로 분리해야 한다. 하나는 객체를 생성하는 것이고, 다른 하나는 객체를 사용하는 것이다. 한 마디로 말해서 객체에 대한 생성과 사용을 분리separating use from creation 해햐 한다.
사용으로부터 생성을 분리하는 데 사용되는 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것이다.
생성과 사용을 분리하기 위해 객체 생성에 특화된 객체를 FACTORY라고 부른다.
크레이그 라만은 시스템을 객체로 분해하는 데 크게 두 가지 방식이 존재한다고 설명한다. 하나는 표현적 분해representational decomposition이고 다른 하나는 행위적 분해behavioral decomposition다.
표현적 분해는 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 것이다. 표현적 분해는 도메인 모델에 담겨 있는 개념과 관계를 따르며 도메인과 소프트웨어 사이의 표현적 차이를 최소화하는 것을 목적으로 한다. 따라서 표현적 분해는 객체지향 설계를 위한 가장 기본적인 접근법이다.
그러나 종종 도메인 개념을 표현하는 객체에게 책임을 할당하는 것만으로는 부족한 경우가 발생한다. 도메인 모델은 설계를 위한 중요한 출발점이지만 단지 출발점이라는 사실을 명심해야 한다. 실제로 동작하는 애플리케이션은 데이터베이스 접근을 위한 객체와 같이 도메인 개념들을 초월하는 기계적인 개념들을 필요로 할 수 있다.
모든 책임을 도메인 객체에게 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하와 같은 심각한 문제점에 봉착하게 될 가능성이 높아진다. 이 경우 도메인 개념을 표현한 객체가 아닌 설계자가 편의를 위해 임의로 만들어낸 가공의 객체에게 책임을 할당해서 문제를 해결해야 한다. 크레이그 라만은 이처럼 책임을 할당하기 위해 창조되는 도메인과 무관한 인공적인 객체를 PURE FABRICATION순수한 가공물이라고 부른다.
먼저 도메인의 본질적인 개념을 표현하는 추상화를 이용해 애플리케이션을 구축하기 시작하라. 만약 도메인 개념이 만족스럽지 못한다면 주저하지 말고 인공적인 객체를 창조하라. 객체지향이 실세계를 모방해야 한다는 헛된 주장에 현혹될 필요가 없다. 우리가 애플리케이션을 구축하는 것은 사용자들이 원하는 기능을 제공하기 위해서지 실세계를 모방하거나 시뮬레이션하기 위한 것이 아니다. 도메인을 반영하는 애플리케이션의 구조라는 제약 안에서 실용적인 창조성을 발휘할 수 있는 능력은 훌륭한 설계자가 갖춰야 할 기본적인 자질이다.
생성과 사용을 분리하면 Movie에는 오로지 인스턴스 사용하는 책임만 남게 된다. 이것은 외부의 다른 객체가 Movie에게 생성된 인스턴스를 전달해야 한다는 것을 의미한다. 이처럼 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법을 의존성 주입 Dependency Injection이라고 부른다. 이 기법을 의존성 주입이라고 부르는 이유는 외부에서 의존성의 대상을 해결한 후 이를 사용하는 객체 쪽으로 주입하기 때문이다.
constructor injection : 객체를 생성하는 시점에 생성자를 통한 의존성 해결setter injection : 객체 생성 후 setter 메서드를 통한 의존성 해결method injection : 메서드 실행 시 인자를 이용한 의존성 해결의존성 주입 외에도 의존성을 해결할 수 있는 다양한 방법이 존재한다. 그중에서 가장 널리 사용되는 대표적인 방법은 SERVICE LOCATOR 패턴이다. SERVICE LOCATOR는 의존성을 해결할 객체들을 보관하는 일종의 저장소다. 외부에서 객체에게 의존성을 전달하는 의존성 주입과 달리 SERVICE LOCATOR의 경우 객체가 직접 SERVICE LOCATOR에게 의존성을 해결해줄 것을 요청한다.
의존성을 숨기는 코드는 단위 테스트 작성도 어렵다. 일반적인 단위 테스트 프레임워크는 테스트 케이스 단위로 테스트에 사용될 객체들을 새로 생성하는 기능을 제공한다. 하지만 위에서 구현한 ServiceLocator는 내부적으로 정적 변수를 사용해 객체들을 관리하기 때문에 모든 단위 테스트 케이스에 걸쳐 ServiceLocator의 상태를 공유하게 된다. 이것은 각 단위 테스트는 서로 고립돼야 한다는 단위 테스트의 기본 원칙을 위반한 것이다.
이야기의 핵심은 의존성 주입이 SERVICE LOCATOR 패턴보다 좋다가 아니라 명시적인 의존성이 숨겨진 의존성보다 좋다는 것이다. 가급적 의존성을 객체의 퍼블릭 인터페이스에 노출하라. 의존성을 구현 내부에 숨기면 숨길수록 코드를 이해하기도, 수정하기도 어려워진다.
어떤 협력에서 중요한 정책이나 의사결정, 비즈니스의 본질을 담고 있는 것은 상위 수준의 클래스다.
그러나 이런 상위 수준의 클래스가 하위 수준의 클래스에 의존한다면 하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받게 될 것이다.
중요한 것은 상위 수준의 클래스다. 상위 수준의 변경에 의해 하위 수준이 변경되는 것은 납득할 수 있지만 하위 수준의 변경으로 인해 상위 수준이 변경돼서는 곤란하다. 하위 수준의 이슈로 인해 상위 수준에 위치하는 클래스들을 재사용하는 것이 어렵다면 이것 역시 문제가 된다. 이 경우에도 해결사는 추상화다.
이제 지금까지 살펴본 내용들을 정리해보자.
이를 의존성 역전 원칙Dependency Inversion Principle, DIP 이라고 부른다.