주로 Refactoring Guru의 structural-pattern을 참고하여 번역 및 추가로 정리한 글이다.
출처: https://refactoring.guru/design-patterns/structural-patterns
3. Composite (복합체 패턴)
복합체 패턴은 객체들을 트리 구조로 합성하여 하나의 객체처럼 작업할 수 있는 구조 패턴이다.
Problem
복합체 패턴은 앱의 핵심 모델을 트리로 표현할 수 있을 때만 의미가 있다.
예를 들어 제품과 상자라는 두 유형의 객체가 있다고 가정하자. 하나의 상자에는 여러 제품과 여러 개의 작은 상자가 포함될 수 있다. 이러한 작은 상자에도 일부 제품 또는 더 작은 상자를 담을 수 있다.
이러한 클래스를 사용하는 주문 시스템을 만들어보자. 주문에는 포장이 없는 간단한 제품 뿐만 아니라 제품이 가득 들어있는 상자가 포함될 수 있다. 그렇다면 이러한 주문의 총 가격을 어떻게 계산할 것인가?
모든 상자의 포장을 풀고 내부의 모든 제품을 살펴본 다음 총합을 계산하는 직접적인 방법이 있지만 덧셈 loop를 도는 것만큼 쉬운 일이 아니다. 그 이유는 제품과 상자의 종류와 같이 기타 복잡한 세부 사항들을 미리 알고 있어야 하기 때문이다. 이 모든 것들은 직접적인 접근 방식을 어렵게 만든다.
Solution
복합체 패턴은 총 가격을 계산하는 메서드를 선언하는 공통 인터페이스를 통해 제품 및 상자 클래스로 작업할 것을 제안한다.
제품의 경우 이 메서드는 단순히 제품 가격을 반환한다. 상자의 경우 이 메서드는 상자 안에 포함된 항목의 가격을 살펴 해당 상자의 총 가격을 반환한다. 만약 이 항목들 중 하나가 더 작은 상자라면, 메서드는 해당 상자의 모든 내부 구성 요소의 가격이 계산될 때까지 내용물 등을 살펴본다.
이 방식의 가장 큰 이점은 트리를 구성하는 개체들의 구상 클래스들에 대해 신경쓸 필요가 없다는 것이다. 단순히 공통 인터페이스를 통해 같은 방식으로 처리하면 된다. 메서드를 호출하면 객체 자체가 트리 아래로 요청을 전달한다.
실제 상황 적용
대부분의 국가에서 군대는 계층 구조로 구성되어 있다. 군대는 여러 사단으로 구성되며, 사단은 여러 여단으로, 여단은 여러 소대로, 소대는 여러 분대로 나누어 진다. 마지막은 분대는 실제 군인들의 작은 집합이다. 명령은 위계 질서에서 최상위에서 내려와 모든 병사가 자신의 임무를 알게 될 때까지 하위로 전달된다.
구조
- 컴포넌트 인터페이스는 트리의 단순 요소와 복잡한 요소 모두에 공통적인 작업을 설명한다.
- leaf는 하위 요소가 없는 트리의 기본 요소이다.
보통 leaf 컴포넌트들은 작업을 위임을 하위 요소가 없어 대부분의 실제 작업들을 수행한다. - Composite는 leaf 또는 기타 컨테이너와 같은 하위 요소를 가진 요소이다. 컨테이너는 자식들의 구상 클래스를 알 수 없으며 컴포넌트 인터페이스를 통해서만 모든 하위 요소와 함께 작동한다.
요청을 전달받으면 컨테이너틑 작업을 하위 요소에 위임하고 중간 결과를 처리한 다음 클라이언트에게 최종 결과를 반환한다. - 클라이언트는 컴포넌트 인터페이스를 통해 모든 요소와 함께 작동한다. 그 결과 클라이언트는 트리의 단순하거나 복잡한 요소 모두에 대한 같은 방식으로 작업할 수 있다.
적용
- 트리와 같은 객체 구조를 구현해야 할 때 사용한다.
- 클라이언트 코드가 단순 요소와 복잡 요소 모두 균일하게 처리하도록 하고 싶을 때 사용한다.
장점
- 다향성과 재귀를 사용하여 복잡한 트리 구조를 보다 편리하게 사용할 수 있다.
- 개방/폐쇄 원칙
- 객체 트리와 함께 작동하는 기존 코드를 훼손하지 않고 새로운 요소 유형들을 도입할 수 있다.
단점
- 기능이 너무 다른 클래스에는 공통 인터페이스를 제공하기 어려울 수 있다. 어떤 경우에는 컴포넌트 인터페이스를 과도하게 일반화해야 하며 이해하기 어려울 수 있다.
4. Decorator (데코레이터 패턴)
새로운 동작들을 포함하는 특수 래퍼 객체 안에 넣어서 객체에 필요한 추가 기능을 동적으로 생성할 수 있는 구조적 디자인 패턴이다.
즉, 각 추가 기능을 데코레이터 클래스로 정의한 후 필요한 객체에 배치함으로써 동적으로 기능을 확장할 수 있게 해준다.
Problem
다른 프로그램이 사용자에게 중요한 이벤트를 알릴 수 있는 알림 라이브러리를 만들고 있다고 가정해보자.
이 라이브러리의 초기 버전은 Notifier(알림자) 클래스를 기반으로 두었으며, 이 클래스에는 몇 개의 필드, 하나의 생성자 그리고 단일 send(전송) 메서드만 있다. 메서드는 클라이언트로부터 메세지 인수를 받은 후 그 메세지를 생성자를 통해 사용자에게 전달된 이메일 목록으로 메세지를 보낼 수 있다. 또한 클라이언트 역할을 하는 서드 파티 앱은 알림 객체를 한번 생성하고 설정한 후 중요한 일이 발생할 때마다 사용하도록 되어 있다.
어느 시점에서 라이브러리 사용자들이 이메일 알림 이상을 기대할 수 있다. 그들 중 많은 사람들은 중요한 사안에 대한 SMS를 받고 싶어한다. 다른 사람들은 페이스북 알림을, 기업 사용자들은 슬랙 알림을 받고 싶어한다.
표면상 어렵지 않아보인다. 당신은 Notifier(알림자) 클래스를 확장하고 추가 알림 메서드들을 새 하위 클래스에 넣어 클라이언트가 원하는 알림 클래스를 인스턴스화 하고 모든 추가 알림에 사용하도록 앱을 설계 했다.
그런데 누군가 "한 번에 여러 유형의 알림을 사용할 수 없나요? 집에 불이라도 난다면 사용자들은 모든 채널에서 정보를 받고 싶어 할 겁니다." 라고 물어볼 수도 있다.
이 문제를 해결하기 위해 하나의 클래스 내에서 여러 알림 메서드를 합성한 특수 하위 클래스를 만들었으나, 이 접근 방식은 라이브러리 코드 뿐만 아니라 클라이언트 코드도 엄청나게 부풀릴 것이라는 게 명백했다.
Solution
객체의 동작을 변경해야 할 때 가장 먼저 고려되는 방법은 클래스의 확장이다. 그러나 상속에는 몇가지 주의사항들이 있다.
- 상속은 정적이다. 런타임에 기존 객체의 동작을 변경할 수 없다. 전체 객체를 다른 하위 클래스에서 생성된 다른 객체로만 바꿀 수 있다.
- 하위 클래스는 부모 클래스를 하나만 가질 수 있다. 대부분 언어에서의 상속은 클래스가 여러 클래스를 동시에 상속하도록 허용하지 않는다.
이러한 주의사항을 극복하는 방법 중 하나는 상속 대신 집합관계(Aggregation) 또는 조합(Composite)을 사용하는 것이다. 두 방법 모두 거의 같은 방식으로 동작한다. 집합 관계에서는 한 객체가 다른 객체에 대한 참조를 가지고 일부 작업을 위임하는 반면, 상속을 사용하면 객체 자체는 슈퍼 클래스의 동작을 상속받으며 해당 작업을 수행할 수 있다.
이 새로운 접근 방식을 사용하면 연결된 '헬퍼' 객체를 다른 객체로 쉽게 대체하여 런타임에 컨테이터 동작을 변경할 수 있다. 객체는 여러 클래스의 동작을 사용할 수 있고, 여러 객체를 참조하고 모든 종류의 작업을 위임한다. 집합관계/조합은 데코레이터를 포함한 많은 디자인 패턴의 핵심 원칙이다.
Wrapper(래퍼)는 패턴의 주요 아이디어를 명확하게 표현하는 데코레이터 패턴의 별명이다. 래퍼는 일부 대상 객체와 연결할 수 있는 객체이다. 래퍼에는 대상과 동일한 매서드 집합이 포함되어 있으며 자신이 받는 모든 요청을 대상 객체에게 위임한다. 그러나 래퍼는 이 요청을 대상에 전달하기 전이나 후에 무언가를 수행하여 결과를 바꿀 수 있다.
그러나 간단한 Wrapper(래퍼)는 언제 진정한 Decorator(데코레이터)가 될 수 있을까? 앞서 말했듯, 래퍼는 래핑된 객체와 동일한 인터페이스를 구현한다. 따라서 클라이언트의 관점에서 이러한 객체들은 같다. 래퍼의 참조 필드가 해당 인터페이스를 따르는 모든 객체를 허용하도록 한다. 이렇게 하면 여러 래퍼로 객체를 포장해서 모든 래퍼들의 결합된 동작을 객체에 추가할 수 있다.
이제 알림 라이브러리에서 기초 Notifier(알림자) 클래스 내에 있는 간단한 이메일 알림 동작은 그대로 두고 다른 알림 매서드들을 데코레이터로 바꿔보자.
클라이언트 코드는 기초 알림자 객체를 클라이언트의 요구사항들과 일치하는 데코레이터들의 집합으로 래핑해야 한다. 위 결과 객체가 스택으로 구성된다.
스택의 마지막 데코레이터는 실제로 클라이언트와 작업하는 객체이다. 모든 데코레이터들은 기초 알림자와 같은 인터페이스를 구현하므로 나머지 클라이언트 코드는 자신이 '순수한' 알림자 객체와 작동하든 데코레이터로 장식된 알림자 객체와 함께 작동하든 상관하지 않는다.
메세지 형식 지정이나 수신자 목록 구성과 같은 행동에서도 같은 접근법을 적용할 수 있다. 클라이언트는 동일한 인터페이스를 따르는 어떤 사용자 지정 데코레이터로도 객체를 장식할 수 있다.
실제 상황 적용
옷을 입는 것은 데코레이터 패턴을 사용하는 예이다. 추울 때는 스웨터를 입고, 그래도 춥다면 위에 재킷을 입을 수 있다. 비가 온다면 우비를 입을 수 있다. 이 모든 옷은 우리의 기본적인 행동을 "확장"시켜주지만 우리의 일부가 아니기 때문에 필요하지 않을 때마다 쉽게 옷을 벗을 수 있다.
구조
- 컴포넌트는 래퍼와 래핑된 객체 모두에 대한 공통 인터페이스를 선언한다.
- 구체적인 컴포넌트는 래핑되는 객체의 클래스이며, 이는 기본 행동들을 정의하고 해당 기본 행동들은 데코레이터들이 변경할 수 있다.
- 기본 데코레이터 클래스에는 래핑된 객체를 참조하기 위한 필드가 있다. 필드의 유형은 구체적인 컴포넌트들과 데코레이터들을 모두 포함할 수 있도록 컴포넌트 인터페이스로 선언되어야 한다.
기본 데코레이터는 모든 작업을 래핑된 객체에 위임한다. - 구체적인 데코레이터는 구성요소에 동적으로 추가할 수 있는 추가 동작들을 정의한다. 구체적인 데코레이터는 기본 데코레이터의 메서드를 재정의하고 부모 매서드를 호출하기 전이나 후에 동작을 실행한다.
- 클라이언트는 컴포넌트 인터페이스를 통해 모든 객체와 함께 작동하는 한 컴포넌트들을 여러 층의 데코레이터들로 래핑할 수 있다.
적용가능성
- 객체를 사용하는 코드를 훼손하지 않고 런타임에 추가 동작들을 객체에 할당할 수 있는 경우 사용한다.
- 객체의 동작을 확장하는 것이 어색하거나 불가능할 경우 사용한다.
장점
- 새 하위 클래스를 만들지 않고 객체를 확장할 수 있다.
- 런타임에 객체들로부터 책임을 추가하거나 제거할 수 있다.
- 객체를 여러 데코레이터로 래핑하여 여러 동작을 결합할 수 있다.
- 단일 책임 원칙
- 다양한 동작 변형을 구현하는 단일 클래스를 여러 개의 작은 클래스로 나눌 수 있다.
단점
- 래퍼 스택에서 특정 래퍼를 제거하기가 어렵다.
- 데코레이터 스택 내의 순서에 의존하지 않는 방식으로 구현하기가 어렵다.
- 계층들의 초기 구성 코드가 보기 흉할 수 있다.