brunch

제9장 데코레이터 패턴

객체에 새로운 의미를 더하다

by jeromeNa

데코레이터(Decorator)는 말 그대로 '장식가'라는 의미를 가지고 있습니다. 음식을 보기 좋게 세팅하는 것도 데코레이션이고, 공간을 아름답게 꾸미는 것도 데코레이션입니다. 이처럼 데코레이터 패턴은 기본 구조에 의미를 확장하고 새로운 가치를 부여하는 개념입니다.


커피숍에서의 주문 과정을 생각해 보면 이해하기 쉽습니다. 기본 에스프레소에 우유를 추가하거나, 휘핑크림을 올리거나, 시럽을 넣어 자신만의 커피를 만들 수 있죠. 각각의 추가 요소는 기본 커피에 새로운 맛과 향, 그리고 비용을 더합니다. 데코레이터 패턴은 이처럼 객체에 동적으로 새로운 책임과 기능을 추가할 수 있게 해주는 디자인 패턴입니다.


1980년대 후반부터 객체지향 프로그래밍 커뮤니티에서 사용되던 기법이었습니다. 특히 항상 나오는 언어인 Smalltalk언어와 함께 발전했습니다. 1988년경 Smalltalk-80의 사용자 인터페이스 프레임워크인 MVC(Model-View-Controller) 아키텍처에서 뷰 컴포넌트의 기능을 동적으로 확장하는 방식으로 사용되었습니다. MVC 아키텍처를 간단히 설명하자면, Model(데이터베이스의 데이터)과 View(화면에 보이는 것)를 Controller가 중간에서 제어하는 개념입니다. 데이터베이스의 Model은 비교적 정적이지만, 사용자에게 보이는 View는 다양한 형태로 자주 변경되어야 합니다. 이렇게 자주 변화하는 View의 유연한 확장을 위해 데코레이터 패턴이 활용되었습니다.


데코레이터 패턴은 GOF의 책에서 공식적으로 문서화되기 전에는 ‘Wrapper’ 패턴이라고도 불렸습니다. 이는 기존의 객체를 새로운 기능으로 ‘감싸는’것에 있기 때문이었습니다.


이 패턴은 총 4가지의 구조로 구성됩니다. 컴포넌트(Component), 구체 컴포넌트(Concrete Component), 데코레이터(Decorator), 구체 데코레이터(Concrete Decorator)이며, 구체(Concrete)라고 되어 있는 건 구체적인 구현이 된 객체를 말합니다.


// 컴포넌트: 모든 커피의 공통 인터페이스
interface Coffee {
String getDescription(); // 커피 설명
double getCost(); // 커피 가격
}

// 구체(구현된) 컴포넌트: 기본 커피
class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple Coffee";
}

@Override
public double getCost() {
return 1.0;
}
}

// 데코레이터: 모든 첨가물의 기본 클래스
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee; // 장식할 커피를 정의

// class와 method 이름이 같은 것은 생성자라고 해서 객체로 만들어지만 바로 실행되는 method.
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}

@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}

@Override
public double getCost() {
return decoratedCoffee.getCost();
}
}

// 구체 데코레이터: 우유 추가
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}

@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", Milk";
}

@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.5;
}
}

// 구체 데코레이터: 휘핑크림 추가
class WhipDecorator extends CoffeeDecorator {
public WhipDecorator(Coffee coffee) {
super(coffee);
}

@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", Whip";
}

@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.7;
}
}

// 구체 데코레이터: 초콜릿 시럽 추가
class ChocolateDecorator extends CoffeeDecorator {
public ChocolateDecorator(Coffee coffee) {
super(coffee);
}

@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", Chocolate";
}

@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.6;
}
}


커피 주문 시스템을 간단히 예로 들었습니다. 커피 컴포넌트를 정의하고 기본 커피를 정의합니다. 그리고 커피에서 데코레이션 할 데코레이터 클래스를 만들고, 테코레이터 클래스를 이용한 각 메뉴들을 정의하고 있습니다. 이렇게 정의된 클래스를 객체로 활용하는 방법은 아래 코드와 같습니다.


public class CoffeeShop {
public static void main(String[] args) {
// 기본 커피 생성
Coffee simpleCoffee = new SimpleCoffee();
System.out.println("Cost: $" + simpleCoffee.getCost() + ", Description: " + simpleCoffee.getDescription());

// 우유 추가
Coffee milkCoffee = new MilkDecorator(simpleCoffee);
System.out.println("Cost: $" + milkCoffee.getCost() + ", Description: " + milkCoffee.getDescription());

// 우유와 휘핑크림 추가
Coffee whipMilkCoffee = new WhipDecorator(milkCoffee);
System.out.println("Cost: $" + whipMilkCoffee.getCost() + ", Description: " + whipMilkCoffee.getDescription());

// 우유, 휘핑크림, 초콜릿 추가
Coffee specialCoffee = new ChocolateDecorator(whipMilkCoffee);
System.out.println("Cost: $" + specialCoffee.getCost() + ", Description: " + specialCoffee.getDescription());

// 다른 조합: 초콜릿과 휘핑크림만 추가
Coffee anotherCoffee = new WhipDecorator(new ChocolateDecorator(new SimpleCoffee()));
System.out.println("Cost: $" + anotherCoffee.getCost() + ", Description: " + anotherCoffee.getDescription());
}
}


이를 출력하면, 아래처럼 커피에 첨가된 가격들이 노출됩니다.

Cost: $1.0, Description: Simple Coffee
Cost: $1.5, Description: Simple Coffee, Milk
Cost: $2.2, Description: Simple Coffee, Milk, Whip
Cost: $2.8, Description: Simple Coffee, Milk, Whip, Chocolate
Cost: $2.3, Description: Simple Coffee, Chocolate, Whip


데코레이터 패턴은 상속(extends) 보다 유연하게 기능을 확장할 수 있습니다. 또한 여러 데코레이터를 조합하여 다양한 동작 구현이 가능하고, 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있습니다. 다만, 작은 클래스들이 많이 생성되어 구조가 복잡해질 우려가 있고, 데코레이터를 적용하는 순서가 중요해질 수 있습니다. 데코레이터들끼리 연결이 복잡할 경우 하나의 데코레이터를 제거하기가 어려울 수 있습니다.


데코레이터 패턴은 기능을 완전히 변경하는 것이 아니라 확장하는 것입니다. 기본 객체의 본질은 유지하면서 추가적인 기능을 장식처럼 더해 다양한 조합을 만들어냅니다. 그러나 장식이 너무 많아지면 기본 객체의 본질을 찾기 어려워질 수 있습니다.


데코레이터 패턴의 장단점과 같이 ‘자신’의 본질을 해치지 않는 선에서 적절히 '장식'을 해야 합니다. 적당한 장식은 본질을 더욱 빛나게 하지만, 너무 많거나 어울리지 않는 장식은 오히려 정체성을 모호하게 만들 수 있습니다.




keyword