09 유연한 설계
8장에서는 응집도와 결합도의 개념에 대해 이야기를 하고 중요한 이유, 즉 원론적인 이유에 대해 말하였습니다. 9장에서는 이러한 응집도와 결합도를 좋은 설계로 이끄는 방법들에 대해 이야기하고 패턴이나 원칙들을 위주로 이야기하였습니다.
01 개방-폐쇄 원칙(Open-Closed Principle, OCP)
소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.
...
- 확장에 대해 열여 있다: 애플리케이션의 요구사항이 변경될 때 이 변경에 맞게 새로운 '동작'을 추가해서 애플리케이션의 기능을 확장할 수 있다.
- 수정에 대해 닫혀 있다: 기존의 '코드'를 수정하지 않고도 애플리케이션의 동작을 추가하거나 변경할 수 있다.
이전 포스팅에서 이야기한 개방 폐쇄 원칙에 대해 짚고 넘어가고 있습니다. 저는 과거 SOLID에 대해 공부를 하며 OCP에 대해 접한적이 있었습니다. 그렇기에 해당 대목에서는 복습을 한다는 마음으로 읽었던것 같습니다.
컴파일 의존성을 고정시키고 런타임 의존성을 변경하라
의존성 관점에서 개방-폐쇄 원칙을 따르는 설계란 컴파일타임 의존성의 가능성을 확장하고 수정할 수 있는 구조라고 할 수 있다.
이 문구를 저는 "의존성 문제 해결 != OCP"라고 생각 합니다. OCP는 의존성 문제를 해결하는 방법중에 하나이며 그 방법의 핵심은 추상화라고 생각합니다.
추상화가 핵심이다.
개방-폐쇄 원칙의 핵심은 추상화에 의존하는 것이다.
...
따라서 추상화 부분은 수정에 대해 닫혀 있다. 추상화를 통해 생략된 부분은 확장의 여지를 남긴다. 이것이 추상화가 개방-폐쇄 원칙을 가능하게 만드는 이유다.
...
비록 이런 기법들이 개방-폐쇄 원칙을 따르는 코드를 작성하는 데 중요하지만 핵심은 추상화라는 것을 기억하라. 올바른 추상화를 설계하고 추상화에 대해서만 의존하도록 관계를 제한함으로써 설계를 유연하게 확장할 수 있다.
...
여기서 주의할 점은 추상화를 했다고 해서 모든 수정에 대해 설계가 폐쇄되는 것은 아니라는 것이다. 수정에 대해 닫혀 있고 확장에 대해 열려 있는 설계는 공짜로 얻어지지 않는다.
책의 처음부터 언급되었던 추상화에 대해 이야기를 하고 있습니다. 그러면서 추상화가 좋은 설계를 하는, 높은 코드의 품질을 유지하는 방법중 하나이지만 만능은 아니라고 지적하고 있습니다. 그리고 9장의 마지막에서 나오겠지만 모든 것은 트레이드 오프라는 것을 한번 꼬집어주고 있습니다.
02 생성 사용 분리
결합도가 높아질수록 개방-폐쇄 원칙을 따르는 구조를 설계하기가 어려워진다.
...
동일한 클래스 안에서 객체 생성과 사용이라는 두 가지 이질적인 목적을 가진 코드가 공존하는 것이 문제다.
...
사용으로부터 생성을 분리하는 데 사용되는 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것이다.
생성과 사용의 분리의 중요성을 이야기하고 있습니다. 분리를 하는 방법으로는 클라이언트로 생성의 책임을 옮기는 것을 제시하고 있는데 이후에 다시 언급하지만 저는 이 역시 올바른 방법은 아니라 생각합니다. 클라이언트의 책임은 넓게 보면 객체를 사용하는 것에 있지 이를 생성하는 것은 책임에 비해 기술적인 성격이 강한 문제라 생각합니다.
Factory 추가하기
객체 생성과 관련된 책임만 전담하는 별도의 객체를 추가하고 Client는 이 객체를 사용하도록 만들 수 있다. 이처럼 생성과 사용을 분리하기 위해 객체 생성에 특화된 객체를 FACTORY라고 부른다.
개발 공부를 하다보면 Factory 혹은 Factory패턴에 대해 한 번쯤은 들어보았다고 생각합니다. 이 책은 디자인 패턴에 대해 공부를 하는 책은 아니지만 그래도 챕터의 주제인 유연한 설계를 위해 도움을 주는 패턴이기에 소개를 해주었다 생각합니다.
순수한 가공물에게 책임 할당하기
크레이그 라만은 시스템을 객체로 분해하는 데는 크게 두 가지 방식이 존재한다고 설명한다. 하나는 표현적 분해(representational decomposition)이고 다른 하나는 행위적 분해(behavioral decomposition)다.
...
표현적 분해는 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 것이다. 표현적 분해는 도메인 모델에 담겨 있는 개념과 관계를 따르며 도메인과 소프트웨어 사이의 표현적 차이를 최소화하는 것을 목적으로 한다. 따라서 표현적 분해는 객체지향 설계를 위한 가장 기본적인 접근법이다.
...
모든 책임을 도메인 객체에게 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하와 같은 심각한 문제점에 봉착하게 될 가능성이 높아진다. 이 경우 도메인 개념을 표현한 객체가 아닌 설계자가 편의를 위해 임의로 만들어낸 가공의 객체에게 책임을 할당해서 문제를 해결해야 한다. 크레이그 라만은 이처럼 책임을 할당하기 위해 창조되는 도메인과 무관한 인공적인 객체를 PURE FABRICATION(순수한 가공물)이라고 부른다.
...
PURE FABRICATION은 표현적 분해보다는 행위적 분해에 의해 생성되는 것이 일반적이다.
...
도메인 개념을 표현하는 객체와 순수하게 창조된 가공의 객체들이 모여 자신의 역할과 책임을 다하고 조화롭게 협력하는 애플리케이션을 설계하는 것이 목표여야 한다.
9장에서 해당 대목이 저에게 있어 가장 배울 것이 많은 장이였습니다. 순수한 가공물이란 저만의 표현 방법으로 말하자면 '기술적인 성격이 강한 객체'입니다. 그리고 이러한 기술적인 성격이 강한 객체와 도메인을 나타내는 객체는 서로 배타적인 개념이 아니라 하나로 섞여 프로그램을 만든다고 주장하고 있습니다. 과거 저는 우테코 프리코스에 도전을 하였을때 도메인만으로는 유연한 설계가 불가능하였고 기술적인 객체가 필요하다고 생각을 하였습니다.(이를테면 Factory객체)하지만 이것이 정녕 올바른 설계인지는 몰랐기에 조심스러웠으나 해당 대목 덕분에 제가 염려했던 부분을 해소시킬 수 있었습니다.
03 의존성 주입
이처럼 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 리를 전달해서 의존성을 해결하는 방법을 의존성 주입(Dependency Injection)이라고 부른다.
...
- 생성자 주입: 객체를 생성하는 시점에 생성자를 통한 의존성 해결
- setter 주입: 객체 생성후 setter 메서드를 통한 의존성 해결
- 메서드 주입: 메서드 실행 시 인자를 이용한 의존성 해결
스프링을 공부하였던 저에게 있어 익숙한 단어인 DI, 의존성 주입입니다. 말이 어렵지 이미 오브젝트 책에서 어느정도 의존성 주입을 통해 코드를 개선해나가고 또 설계를 했다 생각합니다. 그렇기에 저자는 이미 코드로 구현하였던 것을 하나의 개념으로 정리하였습니다.
숨겨진 의존성은 나쁘다.
의존성 주입 외에도 의존성을 해결할 수 있는 다양한 방법이 존재한다. 그중에는 가장 널리 사용되는 대표적인 방법은 SERVICE LOCATOR 패턴이다. SERVICE LOCATOR은 의존성을 해결할 객체들을 보관하는 일종의 저장소다. 외부에서 객체에게 외존성을 전달하는 의존성 주입과 달리 SERVICE LOCATOR의 경우 객체가 직접 SERVICE LOCATOR에게 의존성을 해결해줄 것을 요청한다.
...
SERVICE LOCATOR 패턴의 가장 큰 단점은 의존성을 감춘다는 것이다.
...
이야기의 핵심은 의존성 주입이 SERVICE LOCATOR 패턴보다 좋다가 아니라 명시적인 의존성이 숨겨진 의존성보다 좋다는 것이다. 가급적 의존성을 객체의 퍼블릭 인터페이스에 노출하라.
책에서는 의존성을 숨기는 SERVICE LOCATOR가 좋지 않다고 주장하고 있습니다. 그러면서 "의존성을 숨기는"에 방점을 두고 있습니다. 저는 SERVICE LOCATOR가 스프링의 빈 저장소와 유사하다 생각하였으며 만약 SERVICE LOCATOR가 좋지 못한 설계방식이라면 많은 사람들이 사용하는 Spring은 왜 해당 패턴을 채택하였을까에 대해 생각해보았습니다. 그 결과 스프링은 저장소에 @Bean이라는 방식으로 저장하고 있고 이는 사용자가 직접 등록하기에 의존성을 숨기지 않는 방식이라 생각합니다. 그렇기에 스프링은 SERVICE LOCATOR와 같은 방식으로 빈 저장소를 구현하고 있다 생각합니.
04 의존성 역전 원칙
추상화와 의존성 역전
1. 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다. 둘 모두 추상화에 의존해야 한다.
2. 추상화는 구체적인 사항에 의존해서는 안 된다. 구체적인 사항은 추상화에 의존해야 한다.
...
의존성 역전 원칙(Dependency Inversion Principle, DIP)이라고 부른다.
SOLID에 존재하는 의존성 역전 원칙을 이야기하고 있습니다. 즉 상위의(추상화가 더 많이 되어 있는)모듈은 하위의(추상화가 덜 되어 있는 ) 모듈의 변경에 영향을 받으면 안된다는 이야기 입니다. 그리고 이러한 개념은 다양한 프로그램에서 사용중이며(예를 들면 오라클DB가 있습니다. 학부 수업중 해당 부분에 대해 배웠던 기억이 납니다.) 어찌보면 당연한 이야기이기도 합니다.
의존성 역전 원칙과 패키지
따라서 불필요한 클래스들을 같은 패키지에 두는 것은 전체적인 빌드 시간을 가파르게 상승시킨다.
...
함께 재사용될 필요가 없는 클래스들은 별도의 독립적인 패키지에 모아야 한다. 마틴 파울러는 이 기법을 가리켜 SEPARETED INTERFACE이라고 부른다.
...
객체지향 패러다임에서는 상위 수준 모듈과 하위 수준 모듈이 모두 추상화에 의존한다.
여기서는 평소 공부를 할 때 생각치 못한 빌드, 패키징에 대해 이야기를 하고 있습니다. 그리고 이를 객체지향적인 시작으로 바라본다면 관련 없는 객체들을 하나의 패키지로 관리하는 것 역시 문제가 있다 생각합니다.
05 유연성에 대한 조언
유연한 설계는 유연성이 필요할 때만 옳다
유연성은 항상 복잡성을 수반한다. 유연하지 않은 설계는 단순하고 명확하다. 유연한 설계는 복잡하고 암시적이다.
저자는 이 이야기를 하며 유연성과 단순함(이해하기 쉬운 정도)는 트레이드 오프라고 말해주고 있습니다. 저는 과거 오픈소스 컨트리뷰션을 준비하며 모든 DB와 호환하기 위해 추상화가 잘되어 있는 JPA의 소스코드를 본 적이 있었습니다. 그리고 그 코드에서는 추상화의 정도가 높아 쉽게 이야하기 어려웠습니다. 당시는 그저 '이해하기 어렵다'정도로 끝나는 코드였지만 지금와서 다시 생각해보면 높은 추상화이기에 어쩔 수 없이 이해하기 어려웠던것 같습니다.