데코레이터 패턴
처음에는 본문만 출력하던 보고서에 제목이 필요해졌다. 어떤 화면에는 줄 번호도 붙이고, 다른 화면에는 둘 다 붙이고 싶다. 조합마다 하위 클래스를 만들면 옵션을 하나 더 붙일 때마다 보고서 종류까지 늘어난다.
같은 호출 약속을 가진 객체로 원래 객체를 감싸고, 그 안팎에 동작을 보태는 구조가 데코레이터(Decorator) 패턴이다. Design Patterns의 네 저자를 가리키는 GoF의 분류에서는 구조 패턴에 속한다.1
같은 약속을 유지하는 이유
IReport는 Render()를 호출하면 보고서 문자열을 돌려준다는 약속이다. 본문만 있는 보고서와 두 종류의 포장 객체가 모두 이 약속을 제공하므로, 호출부는 어떤 조합이든 Render()로 사용할 수 있다. 포장 객체는 내부의 IReport를 inner로 보관하고 실제 작업을 그 객체에 맡긴 뒤 자기 결과를 보탠다. 다른 객체에 일을 맡기는 것을 위임이라고 한다.
inner 자리에 다른 포장 객체가 들어갈 수도 있다. 이 연결 덕분에 제목·줄 번호 조합을 위한 TitledNumberedReport 클래스를 따로 만들지 않아도 된다.
실제로 연결되는 객체
이 UML 객체 다이어그램은 아래 첫 번째 조합을 보여 준다. 호출은 왼쪽에서 오른쪽으로 들어가고 결과는 반대로 돌아온다. TextReport가 본문을 돌려주면 NumberedReport가 줄 번호를 붙이고, TitleReport가 그 결과 앞에 제목을 더한다.
작은 예제부터
using System;
using System.Linq;
IReport plain = new TextReport("Revenue: 100\nCost: 40");
IReport first = new TitleReport(new NumberedReport(plain), "October");
IReport second = new NumberedReport(new TitleReport(plain, "October"));
Console.WriteLine(first.Render());
Console.WriteLine("---");
Console.WriteLine(second.Render());
public interface IReport
{
string Render();
}
public sealed class TextReport : IReport
{
private readonly string text;
public TextReport(string text) => this.text = text;
public string Render() => text;
}
public sealed class TitleReport : IReport
{
private readonly IReport inner;
private readonly string title;
public TitleReport(IReport inner, string title)
{
this.inner = inner;
this.title = title;
}
public string Render() => title + "\n" + inner.Render();
}
public sealed class NumberedReport : IReport
{
private readonly IReport inner;
public NumberedReport(IReport inner) => this.inner = inner;
public string Render() => string.Join("\n", inner.Render().Split('\n')
.Select((line, index) => $"{index + 1}. {line}"));
}October
1. Revenue: 100
2. Cost: 40
---
1. October
2. Revenue: 100
3. Cost: 40첫 번째 조합은 본문에 줄 번호를 붙인 뒤 October를 더하므로 제목에는 번호가 없다. 두 번째는 제목부터 붙이고 전체에 번호를 매기므로 October가 1번 줄이 된다. 호출부는 두 경우 모두 Render()를 쓴다.
두 조합은 같은 plain 객체를 참조한다. 여기의 포장 객체는 새 문자열을 만들 뿐 그 객체가 가진 본문을 바꾸지 않는다.
약속이 유지되는 범위
메서드 모양이 같아도 약속의 의미가 달라지면 호출부가 안전하게 교체할 수 없다. 여기의 두 포장 객체는 모두 보고서 텍스트를 돌려주므로 IReport에 맞는다. Render()가 몰래 외부 서비스로 보고서를 전송하게 만들면 호출부는 약속에 드러나지 않은 효과까지 떠안게 된다.
함수로 충분한 경우
고정된 변환 한 번이라면 함수로 쓰면 된다. 호출 약속은 유지한 채 객체 연결마다 다른 책임을 붙여야 할 때 데코레이터가 유용하다. 전략 패턴이 처리 규칙을 교체하는 쪽이라면, 데코레이터는 감싼 객체의 작업을 계속 호출하면서 동작을 보탠다.
같이 보기
전체 예제는 .NET 10 콘솔 프로그램에서 실행할 수 있다.
각주
- Gamma, Helm, Johnson, Vlissides. Design Patterns, Decorator. 출판사와 패턴 목록. ↩