03. Decorator Pattern
데코레이터 패턴
들어가면서
커피 주문을 예시로 데코레이터 패턴을 시작하였다.
여태 개발하면서 직접적으로 상품에 대한 옵션을 선택하고
주문으로 이어지는 시스템을 개발한 기억이 없었다.
그래서 이번 예시는 큰 기대감을 가져왔다.
주된 내용
카페에서 메뉴는..
생각해보면 카페에서 음료에 대한 가짓수를 생각하면 무수히 많다.
추가할 수 있는 옵션이 많아서인데, 이런 메뉴를 고려해서 각각의 옵션 조합마다
클래스를 만드는 것은 사실상 불가능 할 것이다.
나 같으면 옵션에 해당하는 영역들을 따로 클래스를 두어 개발하면 좋지 않을까 생각한다.
하지만 특정 메뉴에서만 적용되는 특정 옵션에 대해서는 지금 당장 떠오르는 생각은 없다.
책에서는 데코레이터 패턴이 이에 적합한 패턴이라고 한다.
이것을 가능하게 해주는 개방-패쇄 원칙
SOLID 원칙 중 하나인 OCP ( 개방-패쇄 원칙 )에 대해서 알아보면
"소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만,
변경에 대해서는 닫혀 있어야 한다.”
이는 새로운 기능이 필요할 때 기존 코드를 수정하지 않고 확장이 가능해야 하지만
이미 안정적으로 작동하는 기존 코드는 변경하지 않아야 한다는 의미이다.
해당 특징을 최대한 활용한 것이 데코레이터 패턴이라고 할 수 있다
개방-패쇄와 데코레이터 패턴
왜 갑자기 개방-패쇄 원칙을 알아보았을까? 거기에 대한 이유는
우리가 앞으로 알아볼 데코레이터 패턴이 개방-패쇄 원칙을 아주 잘 지키는 디자인 패턴이기 때문이다.
데코레이터 패턴을 아주 간단히 말하면 기능 확장에 있어 상속 대신 조합을 사용하는 디자인 패턴이다.
하지만 단순히 조합만 사용한다고 모두 데코레이터 패턴이 되는 것은 아니기에
(혹은 '단순히 조합을 사용하는 것을 넘어 OCP를 효과적으로 달성하는 방법이기에')
데코레이터 패턴에 대해서 다시 알아보자.
데코레이터 패턴
정의
객체에 추가적인 기능을 동적으로 부여하기 위해
기존 객체를 Wrapping 하여 사용하는 방식이다.
조금 더 이해하기 쉽게 코드를 붙여서 구성 요소 및 구조를 설명해 보면
컴포넌트 ( Coffee )
→ 데코레이터와 실제 컴포넌트가 구현 해야할 공통 인터페이스
// Coffee.java
public interface Coffee {
// 커피의 총 비용을 반환합니다.
double getCost();
// 커피의 설명을 반환합니다.
String getDescription();
}
데코레이터 패턴에서 기능 확장의 대상이 되는 객체(구체 컴포넌트)와 추가 기능을 부여하는 객체(데코레이터) 모두가 따라야 하는 최상위 계약(Contract)
구현 컴포넌트 ( Concrete Component )
→ Simple Coffee : 인터페이스를 구현하는 기본 커피 ( 순수 커피 )
// SimpleCoffee.java
public class SimpleCoffee implements Coffee {
@Override
public double getCost() {
return 5.0; // 기본 커피 비용
}
@Override
public String getDescription() {
return "Simple Coffee"; // 기본 커피 설명
}
}
최상위 데코레이터 패턴의 구현체이며 가장 기본적인 형태를 띈다.
데코레이터 ( Decorator ) / 추상 데코레이터
→ Coffee 인터페이스를 구현하며, Coffee 객체를 참조하는 필드를 가진다.
// CoffeeDecorator.java
public abstract class CoffeeDecorator implements Coffee {
// 래핑할 Coffee 객체에 대한 참조
protected Coffee decoratedCoffee;
// 생성자를 통해 래핑할 Coffee 객체를 주입받습니다.
public CoffeeDecorator(Coffee decoratedCoffee) {
this.decoratedCoffee = decoratedCoffee;
}
// 기본적으로 래핑된 객체의 메서드를 호출합니다.
// 구체 데코레이터에서 이 메서드들을 오버라이드하여 기능을 추가합니다.
@Override
public double getCost() {
return decoratedCoffee.getCost();
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
}
Coffee 인터페이스를 구현
하며, 동시에 내부에 Coffee 객체(기본 컴포넌트 또는 다른 데코레이터)를
참조(래핑)하는 필드를 가진다.
구현된 데코레이터 ( Concrete Decorator ) : Milk, Suger
→ 데코레이터를 구현한 클래스이며, 실제 추가 기능을 구현한다.
// Milk.java
public class Milk extends CoffeeDecorator {
public Milk(Coffee decoratedCoffee) {
super(decoratedCoffee); // 부모 클래스(CoffeeDecorator)의 생성자 호출
}
@Override
public double getCost() {
return super.getCost() + 1.5; // 기존 비용에 우유 비용 추가
}
@Override
public String getDescription() {
return super.getDescription() + ", Milk"; // 기존 설명에 우유 설명 추가
}
}
CoffeeDecorator
추상 클래스를 상속받아 실제 추가 기능을 구현하는 클래스. 이 클래스들이 기존 컴포넌트에 새로운 옵션(기능)을 동적으로 덧붙이는 역할을 한다.
// Main.java
public class Main {
public static void main(String[] args) {
System.out.println("--- 커피 주문 데모 ---");
// 1. 일반 커피 주문
Coffee myCoffee = new SimpleCoffee();
System.out.printf("주문 1: %s - $%.2f%n", myCoffee.getDescription(), myCoffee.getCost());
// 출력: 주문 1: Simple Coffee - $5.00
// 2. 우유 추가 커피 주문
Coffee milkCoffee = new Milk(new SimpleCoffee());
System.out.printf("주문 2: %s - $%.2f%n", milkCoffee.getDescription(), milkCoffee.getCost());
// 출력: 주문 2: Simple Coffee, Milk - $6.50
// 3. 우유와 설탕 추가 커피 주문
Coffee milkSugarCoffee = new Sugar(new Milk(new SimpleCoffee()));
System.out.printf("주문 3: %s - $%.2f%n", milkSugarCoffee.getDescription(), milkSugarCoffee.getCost());
// 출력: 주문 3: Simple Coffee, Milk, Sugar - $7.00
// 4. 설탕만 추가 커피 주문
Coffee sugarCoffee = new Sugar(new SimpleCoffee());
System.out.printf("주문 4: %s - $%.2f%n", sugarCoffee.getDescription(), sugarCoffee.getCost());
// 출력: 주문 4: Simple Coffee, Sugar - $5.50
System.out.println("--- 데모 종료 ---");
}
}
그렇다면 데코레이터 패턴의 장단점 ?
장점
1.유연한 기능 확장
런타임에 객체에 새로운 기능을 동적으로 추가하거나 제거하기 쉽다.
2.단일 책임 원칙 준수
데코레이터 클래스는 각각의 특정 추가 기능에 대해서만 책임을 진다.
3.조합이 많아져도 클래스가 폭발적으로 늘지 않는다.
상속에 비해서 옵션이 증가해도 증가된 상태에서 Wrapping 만 하면 되기 때문에
클래스가 폭발적으로 늘진 않는다.
4.개방-패쇄 원칙 준수
기존 컴포넌트 클래스를 수정하지 않고 새로운 데코레이터 클래스를 추가하여
기능 확장이 쉽다.
단점
1.옵션에 따른 복잡성 증가
옵션에 따라 수많은 데코레이터들이 래핑하기 때문에 복잡성이 증가한다.
2.잦은 객체 생성
옵션에 따라 객체를 생성해야함으로 새로운 객체가 기하 급수적으로 생성이 된다
데코레이터 패턴에 대해서 마치며
간략하게 데코레이터 패턴에 대해서 알아보았다.
항상 드는 생각이지만 상속은 계층 분리 정도로만 사용하는 것이 좋고,
코드 재사용에는 적합하지 않다고 생각이든다.
데코레이터 패턴도 처음에는 왜 이게 데코레이터라는 이름이 붙었을까 싶었지만
이해를 하니 적합한 명칭인 것 같다.