Decorator
Decorator Pattern이란?
데코레이터Decorator 패턴은 객체에 동적으로 새로운 기능을 추가할 수 있는 패턴으로, 기존 클래스의 기능을 확장하는 데 주로 사용됩니다. 이 패턴은 상속을 통해 기능을 확장하는 것이 아니라, 구성Composition을 통해 객체에 추가적인 행동을 부여합니다. 데코레이터 패턴은 여러 기능을 조합하여 복잡한 객체를 만들 때 매우 유용하며, 기존 클래스의 수정 없이도 새로운 기능을 추가할 수 있는 유연성을 제공합니다.
Decorator 패턴 구성 요소
데코레이터 패턴은 주로 다음과 같은 구성 요소로 이루어져 있습니다:
컴포넌트Component
기본 인터페이스나 추상 클래스입니다. 이 인터페이스는 데코레이터와 구체적인 컴포넌트가 구현해야 하는 공통 메서드를 정의합니다.
구체적인 컴포넌트Concrete Component
기본 컴포넌트 인터페이스를 구현하는 클래스입니다. 이 클래스는 기본적인 행동을 정의하며, 데코레이터는 이 클래스에 새로운 기능을 추가합니다.
데코레이터Decorator
컴포넌트 인터페이스를 구현하는 클래스이며, 기본 컴포넌트를 감싸고 추가적인 기능을 제공합니다. 데코레이터는 원래 컴포넌트의 인터페이스를 구현하고, 새로운 행동을 추가하거나 기존 행동을 수정할 수 있습니다.
구체적인 데코레이터Concrete Decorator
데코레이터 클래스를 상속하여, 추가적인 기능을 실제로 구현하는 클래스입니다.
Decorator 패턴 구조
- Client → Component (Request 호출)
- 클라이언트는
Component
인터페이스를 통해Request()
를 호출합니다.
- 클라이언트는
- Client → ConcreteDecoratorB (Request 호출)
- 클라이언트가
Component
인터페이스를 호출하면, 가장 바깥에 위치한 데코레이터(ConcreteDecoratorB
)가 요청을 받습니다.
- 클라이언트가
- ConcreteDecoratorB → ConcreteDecoratorA (Request 위임)
ConcreteDecoratorB
는 자신의 동작을 처리한 후, 내부의 다음 구성 요소인ConcreteDecoratorA
객체로 요청을 전달합니다.
- ConcreteDecoratorA → ConcreteComponent (Request 위임)
ConcreteDecoratorA
도 자신의 동작을 처리한 후,ConcreteComponent
객체로 요청을 전달합니다.
- ConcreteComponent → 실제 작업 수행
ConcreteComponent
는 요청을 처리하는 실제 작업을 수행합니다.- 데코레이터들은 작업을 위임하거나 추가 동작을 수행했을 뿐, 최종 작업은
ConcreteComponent
에서 이루어집니다.
- Decorator → Component
Decorator
는Component
를 구현하여 동일한 인터페이스를 제공합니다.- 내부적으로
ConcreteComponent
나 다른Decorator
객체를 참조합니다.
- ConcreteDecoratorA, ConcreteDecoratorB → Decorator
ConcreteDecoratorA
와ConcreteDecoratorB
는Decorator
를 확장하여, 기본 동작에 추가 기능을 구현합니다.
Decorator 패턴 동작 원리
객체 래핑Object Wrapping
- 기존 객체를 감싸는 방식으로 동작을 확장합니다.
- 새로운 객체를 생성하는 대신 기존 객체를 감싸 추가 기능을 제공합니다.
추상화와 구체화의 분리
- 데코레이터는 기능 확장을 위한 공통 추상화Decorator를 제공하고, 구체적인 동작은 ConcreteDecorator에서 정의됩니다.
재귀적 호출과 확장
- 데코레이터 패턴은 재귀적 호출을 통해 각 데코레이터가 감싸진 객체의 작업을 호출하거나 확장합니다.
Decorator 패턴 적용
잘못된 기능 추가 방식
데코레이터 패턴을 사용하지 않고 기능을 확장하려고 하면, 일반적으로 상속을 통해 기능을 추가하거나 기존 클래스에 새로운 기능을 직접 추가하게 됩니다. 이러한 방법은 코드의 유연성을 떨어뜨리고, 유지보수와 확장을 어렵게 만듭니다. 아래는 도서관 관리 시스템에서 책에 여러 가지 기능(예: 대출 가능 여부, 예약 상태, 인기 순위)을 추가하는 데코레이터 패턴을 적용하지 않은 잘못된 코드 예시입니다.
// 기본 책 클래스
public class Book
{
public string Title { get; private set; }
public Book(string title)
{
Title = title;
}
public string GetDetails() => Title;
}
// 대출 가능 여부를 포함한 책 클래스
public class AvailableBook : Book
{
public AvailableBook(string title) : base(title) { }
public string GetAvailability() => $"{GetDetails()}.Available";
}
// 예약 상태를 포함한 책 클래스
public class ReservedBook : Book
{
public ReservedBook(string title) : base(title) { }
public string GetReservation() => $"{GetDetails()}.Reserved";
}
// 인기 순위를 포함한 책 클래스
public class PopularBook : Book
{
public PopularBook(string title) : base(title) { }
public string GetPopularity() => $"{GetDetails()}.Popular";
}
// 대출 가능 여부와 인기 상태를 모두 포함한 책 클래스
public class AvailablePopularBook : Book
{
public AvailablePopularBook(string title) : base(title) { }
public string GetAvailabilityAndPopularity() => $"{GetDetails()}.Available.Popular";
}
// 다양한 조합의 책 클래스를 계속 추가해야 함
public class Program
{
public static void Main(string[] args)
{
// 대출 가능 상태의 책
AvailableBook availableBook = new AvailableBook("C# Programming");
Console.WriteLine(availableBook.GetAvailability());
// 예약된 책
ReservedBook reservedBook = new ReservedBook("Design Patterns");
Console.WriteLine(reservedBook.GetReservation());
// 인기 있는 대출 가능한 책 (조합된 기능을 추가)
AvailablePopularBook book = new AvailablePopularBook("NB");
Console.WriteLine(book.GetAvailabilityAndPopularity());
}
}
클래스 폭발 문제
위와 같이 기능을 추가할 때마다 새로운 클래스를 생성하게 되면, 가능한 모든 기능의 조합을 다루기 위해 매우 많은 클래스가 필요하게 됩니다. 예를 들어, 책이 “대출 가능” 상태이면서 “예약됨” 상태인 경우, 이 둘을 모두 포함한 AvailableReservedBook
과 같은 클래스를 새로 만들어야 합니다. 이러한 방식으로 기능을 확장하면, 클래스의 수가 기하급수적으로 증가하게 되어 클래스 폭발Class Explosion 문제가 발생합니다.
유지보수 어려움
새로운 기능을 추가할 때마다, 기존 클래스들을 수정하거나 새 클래스를 추가해야 합니다. 이로 인해 코드의 유지보수가 매우 어려워집니다. 또한, 하나의 클래스를 수정할 때 다른 클래스들에 미치는 영향을 고려해야 하므로, 수정이 더욱 복잡해집니다.
재사용성 부족
각 클래스는 특정 기능 조합에만 사용될 수 있습니다. 예를 들어, AvailableBook
은 대출 가능 여부만 추가한 상태에서 사용되며, 다른 조합이 필요할 때는 또 다른 클래스를 만들어야 합니다. 이는 코드의 재사용성을 크게 저해합니다.
단일 책임 원칙SRP 위반
하나의 클래스가 여러 기능을 담당하게 되면서 단일 책임 원칙이 무너지게 됩니다. 예를 들어, AvailablePopularBook
클래스는 대출 가능 여부와 인기 여부를 모두 처리하는 책임을 가지게 됩니다. 이로 인해 클래스의 역할이 복잡해지고, 코드의 가독성과 유지보수성이 저하됩니다.
데코레이터 패턴의 필요성
데코레이터 패턴을 사용하면 이러한 기능을 동적으로 조합할 수 있으며, 각 기능은 별도의 클래스로 구현되어 필요에 따라 객체에 추가될 수 있습니다. 이렇게 하면 클래스의 복잡성을 줄이고, 유연한 설계를 유지할 수 있습니다.
// 컴포넌트 인터페이스
public interface IBook
{
string GetDetails();
}
// 구체적인 컴포넌트 클래스
public class Book : IBook
{
private string _title;
public Book(string title)
{
_title = title;
}
public string GetDetails() => _title;
}
// 데코레이터 추상 클래스
public abstract class BookDecorator : IBook
{
protected IBook _book;
public BookDecorator(IBook book)
{
_book = book;
}
public virtual string GetDetails() => _book.GetDetails();
}
// 구체적인 데코레이터 클래스: 대출 가능 여부 추가
public class AvailableDecorator : BookDecorator
{
public AvailableDecorator(IBook book) : base(book) { }
public override string GetDetails() => $"{base.GetDetails()}.Available";
}
// 구체적인 데코레이터 클래스: 예약 상태 추가
public class ReservedDecorator : BookDecorator
{
public ReservedDecorator(IBook book) : base(book) { }
public override string GetDetails() => $"{base.GetDetails()}.Reserved";
}
// 구체적인 데코레이터 클래스: 인기 순위 추가
public class PopularDecorator : BookDecorator
{
public PopularDecorator(IBook book) : base(book) { }
public override string GetDetails() => $"{base.GetDetails()}.Popular";
}
public class Program
{
public static void Main(string[] args)
{
// 기본 책 객체
IBook book = new Book("C# Programming");
// 대출 가능 상태 추가
book = new AvailableDecorator(book);
Console.WriteLine(book.GetDetails());
// 예약 상태 추가
book = new ReservedDecorator(book);
Console.WriteLine(book.GetDetails());
// 인기 순위 추가
book = new PopularDecorator(book);
Console.WriteLine(book.GetDetails());
// 출력:
// C# Programming.Available
// C# Programming.Available.Reserved
// C# Programming.Available.Reserved.Popular
}
}
IBook 인터페이스
책의 기본 정보(예: 제목)를 제공하는 메서드를 정의합니다.
Book 클래스
IBook
인터페이스를 구현하는 기본 클래스입니다.
BookDecorator 클래스
IBook
인터페이스를 구현하는 데코레이터의 추상 클래스입니다. 이 클래스는 기본적으로 IBook
객체를 감싸고, 추가적인 기능을 제공할 수 있도록 합니다.
AvailableDecorator, ReservedDecorator, PopularDecorator 클래스
각각의 데코레이터 클래스는 책에 대출 가능 여부, 예약 상태, 인기 순위와 같은 추가적인 기능을 동적으로 추가합니다.
Decorator Pattenr의 장단점
장점
기능 확장의 유연성
데코레이터 패턴을 사용하면, 기존 클래스의 코드를 변경하지 않고도 기능을 동적으로 추가할 수 있습니다. 이는 다양한 기능의 조합을 필요로 하는 시스템에서 매우 유용합니다.
단일 책임 원칙의 준수
각 데코레이터는 하나의 책임만 가지므로, 클래스가 단일 책임 원칙을 준수할 수 있습니다. 예를 들어, AvailableDecorator
는 대출 가능 여부만 추가하고, 다른 책임은 가지지 않습니다.
기능의 재사용성
기능을 별도의 데코레이터 클래스로 구현하면, 이 기능을 다양한 클래스에서 재사용할 수 있습니다.
단점
복잡성 증가
데코레이터 패턴을 사용하면 많은 데코레이터 클래스를 생성 하게 되어, 클래스 수가 증가하고, 전체 시스템이 복잡해질 수 있습니다.
객체 생성의 비용 증가
여러 개의 데코레이터를 중첩하여 사용할 경우, 객체 생성의 비용이 증가할 수 있습니다. 이는 성능에 민감한 시스템에서는 주의가 필요합니다.
디버깅과 테스트의 어려움
데코레이터 패턴은 여러 개의 객체가 중첩되어 있는 구조이기 때문에, 디버깅이나 테스트가 복잡해질 수 있습니다. 특히, 어떤 데코레이터가 어느 시점에 적용되었는지 추적하는 것이 어려울 수 있습니다.
순서 의존성
데코레이터의 실행 순서에 따라 동작이 달라질 수 있으므로, 순서를 잘 관리해야 합니다.
개선 방안
조직화된 클래스 구조
데코레이터 클래스들을 적절하게 조직화하여 관리하면 복잡성을 줄일 수 있습니다. 예를 들어, 데코레이터 클래스들을 패키지 또는 네임스페이스로 묶어, 관련된 데코레이터들이 함께 그룹화 되도록 할 수 있습니다.
간단한 기능부터 적용
처음부터 모든 기능을 데코레이터 패턴으로 구현하지 말고, 복잡성을 점진적으로 증가시키면서 필요할 때마다 데코레이터를 추가합니다. 이렇게 하면 패턴의 장점을 유지하면서도 복잡성을 제어할 수 있습니다.
Object Pool 사용
객체를 재사용할 수 있도록 Object Pool을 사용하면 빈번한 객체 생성과 소멸로 인한 오버헤드를 줄일 수 있으므로 성능 저하를 방지할 수 있습니다.
Decorator 중첩 최소화
꼭 필요한 경우에만 데코레이터를 중첩하여 사용하고, 불필요한 데코레이터 사용을 줄입니다. 특정 기능의 결합이 빈번하게 사용된다면, 이를 최적화하여 데코레이터를 덜 사용하는 방식으로 설계를 변경할 수 있습니다.
로그와 트레이싱 도구 사용
각 데코레이터 클래스에서 중요한 작업을 수행할 때 로그를 남기도록 하여, 디버깅 시 어떤 데코레이터가 어떻게 적용되었는지 쉽게 추적할 수 있습니다.
// 기본 인터페이스 및 클래스 정의는 동일합니다.
// 로깅 데코레이터
public class LoggingDecorator : BookDecorator
{
public LoggingDecorator(IBook book) : base(book) { }
public override string GetDetails()
{
string details = base.GetDetails();
Console.WriteLine($"Logging: {details}");
return details;
}
}
// 다른 데코레이터들과 함께 사용하는 예시
public class Program
{
public static void Main(string[] args)
{
// 기본 책 객체
IBook book = new Book("C# Programming");
// 로깅 데코레이터 추가
book = new LoggingDecorator(book);
// 대출 가능 상태 추가
book = new AvailableDecorator(book);
// 예약 상태 추가
book = new ReservedDecorator(book);
// 인기 순위 추가
book = new PopularDecorator(book);
// 최종 결과 출력 및 로그 확인
Console.WriteLine(book.GetDetails());
// 출력:
// Logging: C# Programming
// Logging: C# Programming.Available
// Logging: C# Programming.Available.Reserved
// C# Programming.Available.Reserved.Popular
}
}
단위 테스트 강화
각 데코레이터에 대해 독립적인 단위 테스트를 작성하고, 데코레이터들이 조합될 때의 시나리오도 테스트하도록 합니다. 이를 통해 데코레이터들이 예상대로 작동하는지 확인할 수 있습니다.
Decorator Chain의 시각화
디버깅 도구나 커스텀 디버깅 기능을 통해 현재 객체의 데코레이터 체인을 시각화하여, 어떤 데코레이터가 어떤 순서로 적용되었는지 확인할 수 있도록 합니다.
맺음말
데코레이터 패턴은 객체에 동적으로 새로운 기능을 추가하고, 상속을 사용하지 않으면서도 클래스의 기능을 확장할 수 있는 유연한 방법을 제공합니다. 이 패턴은 다양한 기능을 조합하여 복잡한 객체를 만들 때 매우 유용하며, 특히 유지보수성과 재사용성이 중요한 시스템에서 강력한 도구가 될 수 있습니다. 다만, 데코레이터 클래스를 과도하게 사용하면 복잡도가 증가할 수 있으므로, 필요한 경우에 신중하게 적용하는 것이 중요합니다.