데코레이터 패턴(Decorator Pattern) 이해하기
데코레이터 패턴은 객체에 새로운 기능을 추가할 때 기존 클래스를 변경하지 않고 유연하게 확장할 수 있도록 하는 구조적 디자인 패턴입니다. 이 패턴은 기존 객체를 감싸는 방식으로 기능을 추가하며, 여러 데코레이터를 조합하여 다양한 기능을 객체에 동적으로 추가할 수 있습니다. 특히 데코레이터 패턴은 상속 대신 조합을 통해 기능을 확장하기 때문에 코드의 유연성과 재사용성을 높입니다.
이번 포스트에서는 데코레이터 패턴의 개념과 구조, 구현 방법, 장단점 및 사용 사례를 구체적인 예제와 함께 살펴보겠습니다.
데코레이터 패턴이란?
데코레이터 패턴은 기존 객체를 감싸서 기능을 동적으로 추가하거나 확장하는 구조적 디자인 패턴입니다. 데코레이터 패턴은 상속을 사용하지 않고도 객체의 기능을 확장할 수 있어, 필요할 때 원하는 기능만을 선택적으로 추가할 수 있습니다. 이를 통해 개방-폐쇄 원칙(Open-Closed Principle)을 준수하면서 코드의 확장성을 높일 수 있습니다.
데코레이터 패턴의 핵심은 객체를 감싸는 래퍼(wrapper) 역할을 하는 데코레이터 객체를 사용하는 것입니다. 이 데코레이터 객체는 감싸고 있는 객체의 메서드를 호출하거나, 여기에 추가적인 기능을 덧붙입니다.
데코레이터 패턴의 구조
데코레이터 패턴은 다음과 같은 구성 요소로 이루어집니다.
- Component (구성 요소): 객체에 추가될 기능의 기본 인터페이스를 정의합니다.
- ConcreteComponent (구체적인 구성 요소): Component 인터페이스를 구현하는 기본 객체입니다. 데코레이터가 이 객체에 새로운 기능을 추가합니다.
- Decorator (데코레이터): Component 인터페이스를 구현하는 추상 클래스입니다. Component 객체를 감싸고, 기존 메서드를 호출하거나 추가적인 기능을 제공할 수 있습니다.
- ConcreteDecorator (구체적인 데코레이터): Decorator 클래스를 상속하여 구체적인 기능을 추가하는 클래스입니다. 각 ConcreteDecorator는 감싸고 있는 Component에 새로운 기능을 제공합니다.
데코레이터 패턴의 구현 방법
예제 코드: 커피 주문 시스템 예제
커피 주문 시스템에서 커피 종류에 추가 옵션(예: 우유, 설탕, 시럽 등)을 선택적으로 추가하는 예제를 통해 데코레이터 패턴을 구현해보겠습니다.
// Component 인터페이스
public interface ICoffee
{
string GetDescription();
double GetCost();
}
// ConcreteComponent 클래스 (기본 커피)
public class BasicCoffee : ICoffee
{
public string GetDescription()
{
return "Basic Coffee";
}
public double GetCost()
{
return 2.0;
}
}
// Decorator 클래스
public abstract class CoffeeDecorator : ICoffee
{
protected ICoffee _coffee;
public CoffeeDecorator(ICoffee coffee)
{
_coffee = coffee;
}
public virtual string GetDescription()
{
return _coffee.GetDescription();
}
public virtual double GetCost()
{
return _coffee.GetCost();
}
}
// ConcreteDecoratorA 클래스 (우유 추가)
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription()
{
return base.GetDescription() + ", Milk";
}
public override double GetCost()
{
return base.GetCost() + 0.5;
}
}
// ConcreteDecoratorB 클래스 (설탕 추가)
public class SugarDecorator : CoffeeDecorator
{
public SugarDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription()
{
return base.GetDescription() + ", Sugar";
}
public override double GetCost()
{
return base.GetCost() + 0.2;
}
}
// Client 코드
class Program
{
static void Main()
{
// 기본 커피 생성
ICoffee coffee = new BasicCoffee();
Console.WriteLine($"{coffee.GetDescription()} - Cost: ${coffee.GetCost()}");
// 우유 추가
coffee = new MilkDecorator(coffee);
Console.WriteLine($"{coffee.GetDescription()} - Cost: ${coffee.GetCost()}");
// 설탕 추가
coffee = new SugarDecorator(coffee);
Console.WriteLine($"{coffee.GetDescription()} - Cost: ${coffee.GetCost()}");
}
}
위 코드에서 ICoffee는 기본 커피의 인터페이스로, GetDescription()과 GetCost() 메서드를 정의합니다. BasicCoffee는 기본 커피 클래스이며, 데코레이터들이 추가 기능을 제공할 기본 객체입니다.
MilkDecorator와 SugarDecorator는 CoffeeDecorator를 상속받아 각각 우유와 설탕을 추가하는 데코레이터입니다. 각 데코레이터는 GetDescription()과 GetCost() 메서드를 오버라이딩하여 감싸고 있는 커피 객체에 추가 기능을 제공합니다.
출력 결과는 다음과 같습니다.
Basic Coffee - Cost: $2.0
Basic Coffee, Milk - Cost: $2.5
Basic Coffee, Milk, Sugar - Cost: $2.7
각 데코레이터는 기존 커피에 우유와 설탕을 추가할 수 있으며, 이를 통해 클라이언트는 객체의 원래 기능을 수정하지 않고도 동적으로 기능을 확장할 수 있습니다.
데코레이터 패턴의 장단점
장점
- 기능의 유연한 추가: 객체의 기능을 변경하지 않고도, 다양한 기능을 동적으로 추가할 수 있어 코드의 유연성을 높입니다.
- 상속 대신 조합 사용: 상속이 아닌 조합(Composition)을 사용하여 기능을 추가하므로, 더 많은 조합을 만들 수 있고 클래스 수를 줄일 수 있습니다.
- 기능 추가의 개방-폐쇄 원칙 준수: 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어 개방-폐쇄 원칙을 준수합니다.
단점
- 복잡성 증가: 여러 데코레이터가 중첩될 경우, 코드의 복잡성이 증가하고 디버깅이 어려워질 수 있습니다.
- 기능 간의 상호 의존성: 데코레이터 간의 순서나 의존성에 따라 기능이 다르게 동작할 수 있어, 기능 간 상호작용을 고려해야 할 때가 있습니다.
- 객체 수 증가: 데코레이터가 많아지면 그만큼 객체가 많아지므로, 메모리 사용량이 증가할 수 있습니다.
언제 데코레이터 패턴을 사용해야 할까?
데코레이터 패턴은 동적으로 객체에 새로운 기능을 추가해야 하며, 상속으로 해결하기 어려운 경우에 적합합니다. 다음과 같은 상황에서 유용하게 사용될 수 있습니다.
- 기능 추가가 빈번한 경우: 기본 객체의 기능을 자주 변경해야 하거나, 추가적인 기능이 필요할 때 유용합니다.
- 상속이 제한적일 때: 상속을 사용하기 어려운 경우(예: 상속을 통해 모든 조합을 구현하기 어렵거나, 클래스 계층이 너무 많아질 경우) 데코레이터 패턴이 대안이 될 수 있습니다.
- 기능이 독립적이고 조합 가능한 경우: 다양한 기능을 독립적으로 구현하여, 여러 데코레이터를 조합해 객체에 동적으로 기능을 추가해야 할 때 적합합니다.
데코레이터 패턴과 관련 패턴 비교
데코레이터 패턴은 기존 객체에 새로운 기능을 추가하는 데 중점을 둔 패턴으로, 특히 어댑터 패턴 및 프록시 패턴과 유사하지만 목적이 다릅니다.
- 어댑터 패턴: 주로 서로 다른 인터페이스를 맞추기 위해 사용되며, 객체의 인터페이스를 변환하여 호환성을 제공합니다.
- 프록시 패턴: 객체에 대한 접근을 제어하는 데 사용되며, 객체의 동작을 감시하거나 접근을 제한하는 용도로 사용됩니다.
- 데코레이터 패턴: 객체의 기능을 확장하기 위한 용도로, 원래 인터페이스를 유지하면서 추가 기능을 덧붙이는 데 중점을 둡니다.
따라서, 호환성을 위한 변환이 필요한 경우에는 어댑터 패턴, 접근 제어가 필요한 경우에는 프록시 패턴, 기능 확장이 필요한 경우에는 데코레이터 패턴을 사용하는 것이 적절합니다.
마무리하며
데코레이터 패턴은 객체의 기능을 동적으로 확장할 수 있어, 상속 대신 조합을 통해 유연한 코드 구조를 제공합니다. 특히, 기능을 추가할 때 기존 클래스의 변경 없이 데코레이터를 통해 기능을 확장할 수 있어, 개방-폐쇄 원칙을 준수하며 유연하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
복잡한 객체 조합이나 기능 확장이 필요한 시스템에서 데코레이터 패턴을 사용해보세요. 이를 통해 필요한 기능을 손쉽게 추가하고, 객체 지향 설계의 장점을 극대화할 수 있습니다.
'Thinking > Concept' 카테고리의 다른 글
플라이웨이트 패턴(Flyweight Pattern) 이해하기 (0) | 2024.11.15 |
---|---|
퍼사드 패턴(Facade Pattern) 이해하기 (0) | 2024.11.15 |
컴포지트 패턴(Composite Pattern) 이해하기 (2) | 2024.11.15 |
브리지 패턴(Bridge Pattern) 이해하기 (0) | 2024.11.15 |
어댑터 패턴(Adapter Pattern) 이해하기 (1) | 2024.11.15 |