개요
이번 장에서는 8장에서 소개했던 기법들을 원칙이라는 관점에서 설명합니다.
개방-폐쇄 원칙(Open-Closed Principle, OCP)
- 소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해 열려 있어야 하고, 수정에 대해 닫혀 있어야 한다는 원칙입니다.
- '확장에 대해 열려 있다는 의미'는 앱의 요구사항이 변경될 때 이 변경에 맞게 새 동작을 추가해서 앱의 기능을 확장할 수 있다는 의미이며, '수정에 대해 닫혀 있다는 의미'는 기존의 코드를 수정하지 않고도 앱의 동작을 추가하거나 변경할 수 있다는 의미입니다.
어떻게 기존 코드를 수정하지 않고도 새로운 동작을 추가할 수 있을까?
1. 컴파일타임 의존성을 고정시키고 런타임 의존성을 변경하라
- 런타임 의존성은 실행 시에 협력에 참여하는 객체들 사이의 관계이며, 컴파일타임 의존성은 코드에서 드러나는 클래스들 사이의 관계입니다.
- 위 이미지에 보이는 설계는 기존 코드를 수정할 필요 없이 새로운 클래스를 추가하는 것만으로 새로운 할인 정책을 확장할 수 있기에 수정에 대해서는 닫혀 있고 확장에 대해서는 열려 있다고 할 수 있습니다.
2. 추상화가 핵심이다.
- 추상화 과정을 거치면 문맥이 바뀌더라도 변하지 않는 부분만 남게 됩니다. 따라서 문맥에 따라 변하는 부분은 생략되며, 생략된 부분을 문맥에 적합한 내용으로 채워 넣음으로써 각 문맥에 적합하게 기능을 구체화하고 확장할 수 있습니다.
그래서 어떻게 구현해야 할까?
- 알아야 하는 지식(객체 생성에 대한 지식)이 많으면 결합도도 높아지는 경향이 있는데 이 결합도 문제를 해결하려면 객체에 대한 생성과 사용을 분리해야 합니다.
- 사용으로부터 생성을 분리하는 데 사용되는 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것입니다.
- 책에서 제공된 예제를 예시로 들면 Movie의 클라이언트가 적절한 DiscountPolicy 인스턴스를 생성한 후 Movie에게 전달하게 하는 것입니다.
- 왜냐면 Movie에게 금액 할인 정책을 적용할지, 비율 할인 정책을 적용할지를 알고 있는 것은 그 시점에 Movie와 협력할 클라이언트이기 때문입니다.
- 컨텍스트에 관한 결정권을 가지고 있는 클라이언트로 컨텍스트에 대한 지식을 옮김으로써 Movie는 특정한 클라이언트에 결합되지 않고 독립적일 수 있습니다.
- 위 이미지의 설계는 AmountDiscountPolicy의 인스턴스를 생성하는 책임을 클라이언트에게 맡김으로써 구체적인 컨텍스트와 관련된 정보는 클라이언트로 옮기고 Movie는 오직 DiscountPolicy의 인스턴스를 사용하는 데만 주력하고 있습니다.
- Movie의 의존성을 추상화인 DiscountPolicy로만 제한하기 때문에 확장에 대해서는 열려 있으면서도 수정에 대해서는 닫혀 있는 코드를 만들 수 있습니다.
더 나은 방법이 있을까?
- 만약 Movie를 사용하는 Client도 특정한 컨텍스트에 묶이지 않기를 바란다고 가정해 봅니다.
- 책에서 제공하는 예제의 Client의 코드를 살펴보면 Movie의 인스턴스를 생성하는 동시에 getFee 메시지도 함께 전송하고 있습니다. 이는 Client 역시 생성과 사용의 책임을 함께 지니고 있는 것입니다.
- Movie의 문제를 해결했던 방법과 동일한 방법으로 이용해 해결을 할 수 있지만 객체 생성과 관련된 지식이 Client와 협력하는 클라이언트까지 새어나가기를 원치 않는다고 가정해 봅니다.
- 이 경우 객체 생성과 관련된 책임만 전담하는 별도의 객체를 추가하고 Client는 이 객체를 사용하도록 만들 수 있습니다.
- 생성과 사용을 분리하기 위해 객체 생성에 특화된 객체를 Factory라고 부릅니다.
- 이제 Client는 Factory를 사용해서 생성된 Movie의 인스턴스를 반환받아 사용하기만 하면 됩니다.
- Factory를 사용하면 Movie와 AmountDiscountPolicy를 생성하는 책임 모두를 Factory로 이동할 수 있습니다.
- 이제 Client에는 사용과 관련된 책임만 남게 되는데 하나는 Factory를 통해 생성된 Movie 객체를 얻기 위한 것이고 다른 하나는 Movie를 통해 가격을 계산하기 위한 것입니다.
- 이제 Client는 오직 사용과 관련된 책임만 지고 생성과 관련된 어떤 지식도 가지지 않을 수 있게 되었습니다.
객체의 책임 할당을 올바르게 한 걸까?
- 책임 할당의 기본 원칙은 책임을 수행하는 데 필요한 정보를 가진 전문가에게 책임을 부여하는 것입니다.
- 따라서 책임을 할당하려는 경우에는 먼저 도메인 모델 내의 개념 중 적합한 후보를 찾아야 합니다.
적합한 후보를 어떻게 찾아야 할까?
- 후보를 찾기 전에 먼저 객체를 분해하는 방법에 대해 알 필요가 있습니다.
- 시스템을 객체로 분해하는 방법에는 대표적으로 표현적 분해 방식과 행위적 분해 방식이 있습니다.
- 표현적 분해 방식은 도메인 모델의 개념과 관계를 따르며, 도메인과 소프트웨어 간의 표현적 차이를 최소화하려는 목적을 가지고 있습니다.
- 하지만 때때로 도메인 개념을 표현하는 객체에 책임을 할당하는 것만으로는 충분하지 않은 경우가 발생할 수 있습니다.
- 모든 책임을 도메인 객체에 할당하면, 낮은 응집도, 높은 결합도, 재사용성 저하 등의 문제가 발생할 수 있는데, 이런 경우에는 도메인 개념을 표현하는 객체가 아니라 설계자가 편의를 위해 만든 가공의 객체에 책임을 할당해서 문제를 해결해야 합니다.
- 이러한 책임 할당을 위해 창조되는 도메인과 무관한 인공적인 객체를 우리는 '순수한 가공물(PURE FABRICATION)'이라고 부릅니다.
- 어떤 행동을 추가하려 하는데 이 행동을 책임질 마땅한 도메인 개념이 없다면 PURE FABRICATION을 추가하고 이 객체에 책임을 할당해야 합니다.
왜 이렇게 객체를 분리를 해야 할까?
- 설계자로서의 우리의 역할은 도메인 추상화를 기반으로 앱 로직을 설계하는 동시에 품질의 측면에서 균형을 맞추는 데 필요한 객체들을 창조하는 것입니다.
- 따라서 도메인 개념을 표현하는 객체와 순수하게 창조된 가공의 객체들이 모여 자신의 역할과 책임을 다하고 조화롭게 협력하는 앱을 설계하는 것이 목표여야 합니다.
의존성 주입(Dependency Injection)
- 객체가 사용하는 다른 객체를 직접 생성하지 않고 외부에서 생성된 인스턴스를 전달받아 사용하는 방식을 의존성 주입이라 부르며, 의존성 주입에는 네 가지 주요 방법이 있습니다.
- 생성자 주입(Constructor Injection): 객체가 생성될 때 생성자를 통해 의존성을 주입합니다. 이 방법을 사용하면 객체가 필요로 하는 의존성을 명확하게 표현할 수 있습니다. 하지만, 주입된 의존성이 한두 개의 메서드에서만 사용된다면, 이 방법은 비효율적일 수 있습니다.
- Setter 주입(Setter Injection): 객체가 생성된 후 setter 메서드를 통해 의존성을 주입합니다. 이 방법의 단점은 어떤 의존성이 필수적인지 명시적으로 표현할 수 없다는 것입니다.
- 메서드 주입(Method Injection): 메서드를 호출할 때 인자를 통해 의존성을 주입합니다. 이 방법은 메서드가 의존성을 필요로 하는 유일한 경우에 사용됩니다.
- 인터페이스 주입(Interface Injection): 의존성을 명시하기 위해 인터페이스를 사용합니다. 이 방법은 근본적으로 setter 주입이나 프로퍼티 주입과 같으나, 주입 대상을 인터페이스를 통해 명시적으로 선언한다는 점이 다릅니다. 구현적인 관점을 덜어내고 본질적인 측면에서 바라보면, 인터페이스 주입은 setter 주입과 프로퍼티 주입의 변형으로 볼 수 있습니다."
왜 의존성 역전 원칙을 사용할까?
- 상위 수준의 클래스가 하위 수준의 클래스에 의존하면 상위 수준의 클래스를 재사용할 때 하위 수준의 클래스도 필요하기 때문에 재사용하기가 어려워집니다.
- 위 이미지의 경우는 요금을 계산하는 상위 정책이 요금을 계산하는 데 필요한 구체적인 방법에 의존하기에 변경에 취약한데 이 문제를 해결하기 위한 대표적인 방법은 추상화입니다.
- 모두 추상화에 의존하도록 수정하면 하위 수준 클래스의 변경으로 인해 상위 수준의 클래스가 영향을 받는 것을 방지할 수 있습니다.
- 또한 상위 수준을 재사용할 때 하위 수준의 클래스에 얽매이지 않고도 다양한 컨텍스트에서 재사용이 가능합니다.
- 유연하고 재사용 가능한 설계를 원한다면 모든 의존성의 방향이나 추상 클래스나 인터페이스와 같은 추상화를 따라야 합니다.
- 정리하자면 의존성 역전 원칙을 사용하는 이유는 역할, 책임, 협력의 관점에서 설계가 유연하고 재사용 가능하기 때문입니다.
의존성을 설계를 할때 주의해야 할 부분은 무엇일까?
유연한 설계는 유연성이 필요할 때만 옳다
- 유연하고 재사용 가능한 설계가 항상 좋은 것은 아니며, 미래에 변경이 일어날지도 모른다는 막연한 불안감은 불필요하게 복잡한 설계를 낳습니다.
- 따라서 아직 일어나지 않는 변경은 변경이 아닙니다. 즉 불필요한 유연성은 불필요한 복잡성을 낳습니다
- 단순하고 명확한 해법이 그런대로 만족스럽다면 유연성을 제거하면 됩니다.
- 유연성은 코드를 읽는 사람들이 복잡함을 수용할 수 있을 때만 가치가 있습니다.
- 하지만 복잡성에 대한 걱정보다 유연하고 재사용 가능한 설계의 필요성이 더 크다면 코드의 구조와 실행 구조를 다르게 만들어야 합니다.
협력과 책임이 중요하다
- 설계를 유연하게 만들기 위해서는 협력에 참여하는 객체가 다른 객체에게 어떤 메시지를 전송하는지가 중요합니다.
- 다양한 컨테스트에서 협력을 재사용할 필요가 없다면 설계를 유연하게 만들 당위성도 함께 사라집니다.
- 객체들이 메시지 전송자의 관점에서 동일한 책임을 수행하는지 여부를 판단할 수 없다면 공통의 추상화를 도출할 수 없습니다.
- 동일한 역할을 통해 객체들을 대체 가능하게 만들지 않았다면 협력에 참여하는 객체들을 교체할 필요가 없습니다.
- 객체를 생성할 책임을 담당할 객체나 객체 생성 메커니즘을 결정하는 시점은 책임 할당의 마지막 단계로 미뤄야만 합니다.
- 중요 비즈니스 로직을 처리하기 위해 책임을 할당하고 협력의 균형을 맞추는 것이 객체 생성에 관한 책임을 할당하는 것보다 우선입니다.
- 책임 관점에서 객체들 간에 균형이 잡혀 있는 상태라면 생성과 관련된 책임을 지게 될 객체를 선택하는 것은 간단한 작업이 됩니다.
마무리
오늘은 오브젝트 9장에 대한 스터디를 진행하며, 중요하다고 생각되는 부분을 정리해 보았습니다.
이번 장의 내용이 처음에는 잘 이해되지 않아서, 여러 번 읽어야 했고 이로 인해 포스팅이 약간 늦어졌습니다.
이번 장에서 말하는 원칙에 대한 설명이 매끄럽게 이어지지 않는다고 생각이 들어서, 약간 아쉬움이 남았습니다.
그러나, 이번 장을 통해 기존에 애매하게 이해하고 있던 '의존성 주입'에 대한 개념과 '객체를 분리하는 방법'에 대한 기준에 대해 조금 더 이해할 수 있어서 좋았습니다.
이번 포스팅은 마무리하면서 다음 포스팅에서 뵙겠습니다.
참고
http://www.yes24.com/Product/Goods/74219491
'스터디 > 오브젝트' 카테고리의 다른 글
11장 - 합성과 유연한 설계 (0) | 2023.08.28 |
---|---|
10장 - 상속과 코드 재사용 (0) | 2023.08.06 |
8장 - 의존성 관리하기 (0) | 2023.05.22 |
7장 - 객체 분해 (0) | 2023.04.30 |
6장 - 메시지와 인터페이스 (0) | 2023.04.17 |