Featured image of post [Design Patterns] 08. 데코레이터와 컴포지트: 재귀적 아름다움

[Design Patterns] 08. 데코레이터와 컴포지트: 재귀적 아름다움

동적으로 기능을 확장하는 Decorator와 부분-전체 계층구조를 표현하는 Composite 패턴의 재귀적 구조와 수학적 아름다움을 탐구합니다. 함수형 프로그래밍과의 연관성, 트리 구조 처리, 동적 기능 조합 등 고급 설계 기법을 통해 유연하고 확장 가능한 시스템을 구축하는 방법을 학습합니다.

Decorator와 Composite 패턴을 통해 재귀적 구조의 아름다움을 탐구합니다. 동적 기능 확장과 트리 구조 표현의 우아한 해결책을 학습합니다.

서론: 무한 확장의 아름다운 수학

“자연에서 발견되는 프랙탈의 아름다움처럼, 소프트웨어에도 부분이 전체를 닮고, 단순한 규칙이 복잡한 구조를 만들어내는 패턴들이 있다. Decorator와 Composite가 바로 그것이다.”

**재귀(Recursion)**는 수학과 컴퓨터 과학에서 가장 아름다운 개념 중 하나입니다. 자기 자신을 참조하여 정의되는 구조는 단순한 규칙으로 무한히 복잡한 형태를 만들어낼 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 재귀의 수학적 아름다움 예시: 피보나치 수열
int fibonacci(int n) {
    if (n <= 1) return n;                    // 기저 조건
    return fibonacci(n-1) + fibonacci(n-2);  // 재귀적 정의
}

// 팩토리얼: 또 다른 재귀의 예
int factorial(int n) {
    if (n <= 1) return 1;                    // 기저 조건
    return n * factorial(n-1);               // 재귀적 정의
}

Decorator와 Composite 패턴은 이런 재귀적 사고를 객체지향 설계에 적용한 걸작입니다:

Decorator의 수학적 본질: 함수 합성 f(g(h(x)))

  • 동적 확장: 런타임에 객체의 기능을 층층이 감싸서 확장
  • 합성의 아름다움: 단순한 기능들의 조합으로 복잡한 동작 창조
  • 투명성: 클라이언트는 장식 여부를 알 필요 없음
  • 순서의 중요성: 장식자의 순서가 최종 결과를 결정

Composite의 구조적 철학: 트리와 재귀

  • 일관성: 개별 객체와 객체 집합을 동일하게 취급
  • 투명성: 클라이언트는 Leaf인지 Composite인지 구분할 필요 없음
  • 재귀적 구조: 트리의 각 노드가 다시 트리가 될 수 있음
  • 집계 연산: 부분의 합이 전체가 되는 자연스러운 계산
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 현실에서 마주치는 문제 상황
public class TextProcessor {
    public String processText(String text) {
        // 문제: 텍스트 처리 기능을 동적으로 조합하고 싶음
        
        // 요구사항들:
        // 1. 대문자 변환
        // 2. HTML 태그 제거
        // 3. 공백 정규화
        // 4. 암호화
        // 5. 압축
        // 6. 로깅
        
        // 하지만 모든 조합이 항상 필요한 것은 아님
        // 어떤 때는 1+3+6만, 어떤 때는 2+4+5만 필요
        // 상속으로는 불가능한 조합의 폭발...
        
        String result = text;
        if (needsUpperCase) result = result.toUpperCase();
        if (needsHtmlStrip) result = stripHtml(result);
        if (needsNormalization) result = normalizeWhitespace(result);
        if (needsEncryption) result = encrypt(result);
        if (needsCompression) result = compress(result);
        if (needsLogging) log(result);
        
        return result;
        // 문제: 조합이 복잡해질수록 if문이 폭발적으로 증가
        // 새로운 기능 추가 시마다 기존 코드 수정 필요
    }
}

이런 문제를 어떻게 우아하게 해결할 수 있을까요?

탄생 배경

Decorator와 Composite 패턴은 Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides(이른바 “GoF”, Gang of Four)가 1994년 출간한 《Design Patterns: Elements of Reusable Object-Oriented Software》(Addison-Wesley)에서 23개 패턴 중 구조 패턴(Structural Patterns) 범주로 처음 체계화했습니다. 이 책에서 저자들은 Decorator를 “객체에 추가 책임을 동적으로 부여하는” 패턴으로, Composite를 “부분-전체 계층 구조를 표현하여 클라이언트가 개별 객체와 객체 구성을 동일하게 다루도록” 하는 패턴으로 정의했습니다(Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley).

Decorator 패턴: 동적 장식의 예술

패턴의 동기와 철학

Decorator 패턴은 **“객체에 새로운 기능을 동적으로 추가”**하는 문제를 해결합니다. 상속의 한계를 극복하고, 런타임에 객체의 행동을 확장할 수 있게 해줍니다.

Decorator의 핵심 구조

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
// 텍스트 처리의 기본 인터페이스
interface TextProcessor {
    String process(String text);
}

// 기본 구현체 (ConcreteComponent)
class PlainTextProcessor implements TextProcessor {
    @Override
    public String process(String text) {
        return text; // 아무 처리도 하지 않음
    }
}

// Decorator의 기본 클래스
abstract class TextProcessorDecorator implements TextProcessor {
    protected final TextProcessor wrapped;
    
    protected TextProcessorDecorator(TextProcessor processor) {
        this.wrapped = processor;
    }
    
    @Override
    public String process(String text) {
        return wrapped.process(text); // 기본 동작은 위임
    }
}

// 구체적인 Decorator들
class UpperCaseDecorator extends TextProcessorDecorator {
    public UpperCaseDecorator(TextProcessor processor) {
        super(processor);
    }
    
    @Override
    public String process(String text) {
        String result = wrapped.process(text); // 먼저 이전 처리 수행
        return result.toUpperCase();           // 추가 기능 적용
    }
}

class HtmlStripDecorator extends TextProcessorDecorator {
    public HtmlStripDecorator(TextProcessor processor) {
        super(processor);
    }
    
    @Override
    public String process(String text) {
        String result = wrapped.process(text);
        return result.replaceAll("<[^>]*>", ""); // HTML 태그 제거
    }
}

class WhitespaceNormalizeDecorator extends TextProcessorDecorator {
    public WhitespaceNormalizeDecorator(TextProcessor processor) {
        super(processor);
    }
    
    @Override
    public String process(String text) {
        String result = wrapped.process(text);
        return result.replaceAll("\\s+", " ").trim(); // 공백 정규화
    }
}

class EncryptionDecorator extends TextProcessorDecorator {
    private final String key;
    
    public EncryptionDecorator(TextProcessor processor, String key) {
        super(processor);
        this.key = key;
    }
    
    @Override
    public String process(String text) {
        String result = wrapped.process(text);
        return encrypt(result, key); // 암호화 적용
    }
    
    private String encrypt(String text, String key) {
        // 간단한 암호화 (실제로는 더 복잡한 알고리즘 사용)
        return Base64.getEncoder().encodeToString(text.getBytes());
    }
}

class LoggingDecorator extends TextProcessorDecorator {
    private final String loggerName;
    
    public LoggingDecorator(TextProcessor processor, String loggerName) {
        super(processor);
        this.loggerName = loggerName;
    }
    
    @Override
    public String process(String text) {
        long startTime = System.currentTimeMillis();
        System.out.println("[" + loggerName + "] Processing started: " + text.substring(0, Math.min(50, text.length())));
        
        String result = wrapped.process(text);
        
        long endTime = System.currentTimeMillis();
        System.out.println("[" + loggerName + "] Processing completed in " + (endTime - startTime) + "ms");
        
        return result;
    }
}

Decorator 체인의 마법

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class DecoratorExample {
    public static void main(String[] args) {
        // 기본 텍스트
        String htmlText = "<html><body><h1>Hello World!</h1>  <p>This is   a   test.</p></body></html>";
        
        // 1. 단순한 처리
        TextProcessor simple = new PlainTextProcessor();
        System.out.println("Simple: " + simple.process(htmlText));
        
        // 2. HTML 태그 제거 + 공백 정규화
        TextProcessor htmlStrip = new WhitespaceNormalizeDecorator(
            new HtmlStripDecorator(
                new PlainTextProcessor()
            )
        );
        System.out.println("HTML Strip + Normalize: " + htmlStrip.process(htmlText));
        
        // 3. 완전한 체인: HTML 제거 → 공백 정규화 → 대문자 변환 → 암호화 → 로깅
        TextProcessor fullChain = new LoggingDecorator(
            new EncryptionDecorator(
                new UpperCaseDecorator(
                    new WhitespaceNormalizeDecorator(
                        new HtmlStripDecorator(
                            new PlainTextProcessor()
                        )
                    )
                ), "secret-key"
            ), "FullProcessor"
        );
        System.out.println("Full Chain: " + fullChain.process(htmlText));
        
        // 4. 다른 순서의 체인: 대문자 변환 → HTML 제거 → 공백 정규화
        TextProcessor differentOrder = new WhitespaceNormalizeDecorator(
            new HtmlStripDecorator(
                new UpperCaseDecorator(
                    new PlainTextProcessor()
                )
            )
        );
        System.out.println("Different Order: " + differentOrder.process(htmlText));
    }
}

/*
출력 결과:
Simple: <html><body><h1>Hello World!</h1>  <p>This is   a   test.</p></body></html>

HTML Strip + Normalize: Hello World! This is a test.

[FullProcessor] Processing started: <html><body><h1>Hello World!</h1>  <p>This is
[FullProcessor] Processing completed in 2ms
Full Chain: SEVMTE8gV09STEQhIFRISVMgSVMgQSBURVNULg==

Different Order: HELLO WORLD! THIS IS A TEST.
*/

함수형 관점에서의 Decorator

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// 함수형 스타일의 Decorator 구현
@FunctionalInterface
interface TextTransformer extends Function<String, String> {
    
    // 체이닝을 위한 헬퍼 메서드
    default TextTransformer then(TextTransformer after) {
        return text -> after.apply(this.apply(text));
    }
    
    // 조건부 적용
    default TextTransformer when(Predicate<String> condition) {
        return text -> condition.test(text) ? this.apply(text) : text;
    }
}

// 함수형 변환기들
public class TextTransformers {
    
    public static final TextTransformer TO_UPPER = String::toUpperCase;
    public static final TextTransformer TO_LOWER = String::toLowerCase;
    public static final TextTransformer STRIP_HTML = text -> text.replaceAll("<[^>]*>", "");
    public static final TextTransformer NORMALIZE_WHITESPACE = text -> text.replaceAll("\\s+", " ").trim();
    public static final TextTransformer REVERSE = text -> new StringBuilder(text).reverse().toString();
    
    public static TextTransformer encrypt(String key) {
        return text -> Base64.getEncoder().encodeToString(text.getBytes());
    }
    
    public static TextTransformer addPrefix(String prefix) {
        return text -> prefix + text;
    }
    
    public static TextTransformer addSuffix(String suffix) {
        return text -> text + suffix;
    }
    
    public static TextTransformer log(String loggerName) {
        return text -> {
            System.out.println("[" + loggerName + "] Processing: " + text);
            return text;
        };
    }
}

// 함수형 스타일 사용법
public class FunctionalDecoratorExample {
    public static void main(String[] args) {
        String htmlText = "<html><body><h1>Hello World!</h1></body></html>";
        
        // 1. 체이닝을 통한 조합
        TextTransformer pipeline1 = TextTransformers.STRIP_HTML
            .then(TextTransformers.NORMALIZE_WHITESPACE)
            .then(TextTransformers.TO_UPPER)
            .then(TextTransformers.addPrefix(">>> "))
            .then(TextTransformers.addSuffix(" <<<"));
        
        System.out.println("Pipeline 1: " + pipeline1.apply(htmlText));
        
        // 2. 조건부 적용
        TextTransformer conditionalPipeline = TextTransformers.STRIP_HTML
            .then(TextTransformers.TO_UPPER.when(text -> text.length() < 50))
            .then(TextTransformers.REVERSE.when(text -> text.contains("HELLO")));
        
        System.out.println("Conditional: " + conditionalPipeline.apply(htmlText));
        
        // 3. Stream과의 조합
        List<String> texts = Arrays.asList(
            "<p>First text</p>",
            "<div>Second text</div>",
            "<span>Third text</span>"
        );
        
        TextTransformer batchProcessor = TextTransformers.STRIP_HTML
            .then(TextTransformers.TO_UPPER)
            .then(TextTransformers.addPrefix("Processed: "));
        
        List<String> processed = texts.stream()
            .map(batchProcessor)
            .collect(Collectors.toList());
        
        processed.forEach(System.out::println);
    }
}

/*
출력:
Pipeline 1: >>> HELLO WORLD! <<<
Conditional: !DLROW OLLEH
Processed: FIRST TEXT
Processed: SECOND TEXT
Processed: THIRD TEXT
*/

실제 활용 사례: Java I/O의 Decorator 마스터피스

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// Java I/O는 Decorator 패턴의 교과서적 예시
public class JavaIODecoratorExample {
    
    public void demonstrateIODecorators() throws IOException {
        // 1. 기본 파일 읽기
        try (InputStream basicInput = new FileInputStream("data.txt")) {
            // 기본 기능만 사용
        }
        
        // 2. 버퍼링 추가 (성능 향상)
        try (InputStream bufferedInput = new BufferedInputStream(
                new FileInputStream("data.txt")
        )) {
            // 버퍼링으로 성능 개선
        }
        
        // 3. 압축 해제 추가
        try (InputStream compressedInput = new GZIPInputStream(
                new FileInputStream("data.gz")
        )) {
            // GZIP 압축 파일 읽기
        }
        
        // 4. 완전한 체인: 파일 → GZIP 해제 → 버퍼링 → 데이터 스트림
        try (DataInputStream dataInput = new DataInputStream(
                new BufferedInputStream(
                    new GZIPInputStream(
                        new FileInputStream("data.gz")
                    )
                )
        )) {
            // 압축된 파일에서 구조화된 데이터 읽기
            int value = dataInput.readInt();
            String text = dataInput.readUTF();
        }
        
        // 5. 네트워크 + 암호화 + 버퍼링
        try (InputStream networkInput = new BufferedInputStream(
                new CipherInputStream(
                    new URL("https://example.com/data").openStream(),
                    createDecryptCipher()
                )
        )) {
            // 네트워크에서 암호화된 데이터를 버퍼링하며 읽기
        }
    }
    
    // 사용자 정의 Decorator 추가
    public static class LoggingInputStream extends FilterInputStream {
        private final String name;
        private int bytesRead = 0;
        
        public LoggingInputStream(InputStream in, String name) {
            super(in);
            this.name = name;
        }
        
        @Override
        public int read() throws IOException {
            int result = super.read();
            if (result != -1) {
                bytesRead++;
                if (bytesRead % 1024 == 0) {
                    System.out.println("[" + name + "] Read " + bytesRead + " bytes");
                }
            }
            return result;
        }
    }
    
    // 체인에 로깅 추가
    public void useCustomDecorator() throws IOException {
        try (InputStream loggingInput = new LoggingInputStream(
                new BufferedInputStream(
                    new GZIPInputStream(
                        new FileInputStream("large-data.gz")
                    )
                ), "DataReader"
        )) {
            byte[] buffer = new byte[1024];
            while (loggingInput.read(buffer) != -1) {
                // 데이터 처리
            }
        }
    }
    
    private Cipher createDecryptCipher() {
        // 암호화 설정 구현
        return null;
    }
}

Composite 패턴: 트리 구조의 우아한 통일성

패턴의 동기와 철학

Composite 패턴은 **“부분-전체 계층구조”**를 나타내는 가장 우아한 방법입니다. 개별 객체와 객체들의 집합을 동일하게 다룰 수 있게 해주어, 클라이언트가 복잡성을 의식하지 않고 트리 구조를 다룰 수 있습니다.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// 수학 표현식 계산기 - Composite의 완벽한 예시
abstract class Expression {
    public abstract double evaluate();
    public abstract String toString();
    
    // Composite 전용 메서드들 (기본 구현)
    public void add(Expression expression) {
        throw new UnsupportedOperationException("Leaf node cannot add children");
    }
    
    public void remove(Expression expression) {
        throw new UnsupportedOperationException("Leaf node cannot remove children");
    }
    
    public List<Expression> getChildren() {
        throw new UnsupportedOperationException("Leaf node has no children");
    }
}

// Leaf - 숫자
class Number extends Expression {
    private final double value;
    
    public Number(double value) {
        this.value = value;
    }
    
    @Override
    public double evaluate() {
        return value;
    }
    
    @Override
    public String toString() {
        return String.valueOf(value);
    }
}

// Composite - 연산자
abstract class BinaryOperation extends Expression {
    protected Expression left;
    protected Expression right;
    protected final String operator;
    
    public BinaryOperation(Expression left, Expression right, String operator) {
        this.left = left;
        this.right = right;
        this.operator = operator;
    }
    
    @Override
    public String toString() {
        return "(" + left.toString() + " " + operator + " " + right.toString() + ")";
    }
    
    @Override
    public List<Expression> getChildren() {
        return Arrays.asList(left, right);
    }
}

class Addition extends BinaryOperation {
    public Addition(Expression left, Expression right) {
        super(left, right, "+");
    }
    
    @Override
    public double evaluate() {
        return left.evaluate() + right.evaluate();
    }
}

class Multiplication extends BinaryOperation {
    public Multiplication(Expression left, Expression right) {
        super(left, right, "*");
    }
    
    @Override
    public double evaluate() {
        return left.evaluate() * right.evaluate();
    }
}

class Division extends BinaryOperation {
    public Division(Expression left, Expression right) {
        super(left, right, "/");
    }
    
    @Override
    public double evaluate() {
        double rightValue = right.evaluate();
        if (rightValue == 0) {
            throw new ArithmeticException("Division by zero");
        }
        return left.evaluate() / rightValue;
    }
}

// 사용 예시: (3 + 4) * (2 / 1)
public class ExpressionExample {
    public static void main(String[] args) {
        // 복잡한 수식 구성
        Expression expr = new Multiplication(
            new Addition(new Number(3), new Number(4)),     // (3 + 4)
            new Division(new Number(2), new Number(1))      // (2 / 1)
        );
        
        System.out.println("Expression: " + expr);           // (3.0 + 4.0) * (2.0 / 1.0)
        System.out.println("Result: " + expr.evaluate());    // 14.0
        
        // 더 복잡한 중첩 구조
        Expression complex = new Addition(
            new Multiplication(
                new Number(2),
                new Addition(new Number(3), new Number(4))
            ),
            new Division(
                new Number(10),
                new Number(2)
            )
        );
        
        System.out.println("Complex: " + complex);          // (2.0 * (3.0 + 4.0)) + (10.0 / 2.0)
        System.out.println("Result: " + complex.evaluate()); // 19.0
    }
}

GUI 계층 구조의 완벽한 실현

이 예제에서 가장 중요한 설계 결정은 UIComponent 기반 클래스에 add()/remove()/getChildren() 같은 자식 관리 메서드를 두는가입니다. GoF는 이를 두 가지 방식으로 구분합니다. 기반 클래스에 자식 관리 메서드를 모두 선언하고 Leaf가 이를 UnsupportedOperationException으로 거부하는 투명한(transparent) Composite와, 자식 관리 메서드를 Composite 하위 타입에만 선언하는 안전한(safe) Composite입니다. 아래 코드는 전자를 택했습니다. 클라이언트가 Button인지 Panel인지 구분하지 않고 동일한 인터페이스로 다룰 수 있다는 이점(다형적 투명성)을 얻는 대신, “Leaf에 자식을 추가하려 하면 컴파일은 되지만 런타임에 예외가 난다"는 안전성 손실을 감수하는 것입니다. 후자를 택하면 컴파일 타임에 이 실수를 막을 수 있지만, 클라이언트 코드에서 instanceof나 다운캐스팅이 늘어나 Composite 패턴의 핵심 이점인 “동일 취급"이 깨집니다.

또한 Button/Label(Leaf)과 Panel/Window(Composite) 각각의 두 구현은 상속 구조와 재귀 호출 방식이 동일하므로, 아래에서는 각 역할의 대표 클래스 하나씩만 완전히 구현하고 나머지는 차이점만 주석으로 설명합니다.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// GUI 컴포넌트 시스템
abstract class UIComponent {
    protected String name;
    protected int x, y, width, height;
    protected boolean visible = true;

    public UIComponent(String name, int x, int y, int width, int height) {
        this.name = name;
        this.x = x; this.y = y; this.width = width; this.height = height;
    }

    // 모든 컴포넌트가 구현해야 하는 기본 메서드들
    public abstract void render(Graphics g);
    public abstract void handleEvent(Event event);
    public abstract Rectangle getBounds();

    // Composite 전용 메서드 - 투명한 Composite이므로 Leaf는 런타임에 거부한다
    public void add(UIComponent component) {
        throw new UnsupportedOperationException("Cannot add children to leaf component");
    }

    public void remove(UIComponent component) {
        throw new UnsupportedOperationException("Cannot remove children from leaf component");
    }

    public List<UIComponent> getChildren() {
        return Collections.emptyList();
    }

    public void setVisible(boolean visible) { this.visible = visible; }
    public boolean isVisible() { return visible; }
    public String getName() { return name; }
}

// Leaf: 자식을 가질 수 없는 최종 노드의 대표 예시
class Button extends UIComponent {
    private String text;
    private Runnable clickHandler;

    public Button(String name, int x, int y, String text) {
        super(name, x, y, 100, 30);
        this.text = text;
    }

    @Override
    public void render(Graphics g) {
        if (!visible) return;
        g.drawRect(x, y, width, height);
        g.drawString(text, x + 10, y + 20);
    }

    @Override
    public void handleEvent(Event event) {
        if (!visible) return;
        boolean inBounds = event.getX() >= x && event.getX() <= x + width
                && event.getY() >= y && event.getY() <= y + height;
        if (event.getType() == EventType.CLICK && inBounds && clickHandler != null) {
            clickHandler.run();
        }
    }

    @Override
    public Rectangle getBounds() { return new Rectangle(x, y, width, height); }

    public void setClickHandler(Runnable handler) { this.clickHandler = handler; }
}
// Label은 Button과 동일한 구조에서 render()가 텍스트만 그리고 handleEvent()는 비워둔 형태다 — 지면상 생략.

// Composite: 자식을 담고 렌더링·이벤트 처리를 재귀적으로 위임하는 대표 예시
class Panel extends UIComponent {
    private final List<UIComponent> children = new ArrayList<>();
    private Color backgroundColor;

    public Panel(String name, int x, int y, int width, int height) {
        super(name, x, y, width, height);
    }

    @Override
    public void add(UIComponent component) { children.add(component); }

    @Override
    public void remove(UIComponent component) { children.remove(component); }

    @Override
    public List<UIComponent> getChildren() { return new ArrayList<>(children); }

    @Override
    public void render(Graphics g) {
        if (!visible) return;
        if (backgroundColor != null) {
            g.setColor(backgroundColor);
            g.fillRect(x, y, width, height);
        }
        g.drawRect(x, y, width, height);
        for (UIComponent child : children) {
            child.render(g); // 재귀 호출 - child가 다시 Panel이어도 동일하게 동작한다
        }
    }

    @Override
    public void handleEvent(Event event) {
        if (!visible) return;
        for (UIComponent child : children) {
            child.handleEvent(event); // 이벤트도 동일한 방식으로 재귀 전파된다
        }
    }

    @Override
    public Rectangle getBounds() { return new Rectangle(x, y, width, height); }

    public void setBackgroundColor(Color color) { this.backgroundColor = color; }
}
// Window는 Panel과 동일한 add/remove/render 골격에 타이틀 바 렌더링과 minimize() 상태만 추가된다 — 지면상 생략.
// 중요한 점은 Window 역시 UIComponent이므로 다른 Window나 Panel의 자식이 될 수 있다는 것이다(재귀 구조의 핵심).

// 복잡한 GUI 구조 생성 예시
public class GUIExample {
    public static void main(String[] args) {
        Window mainWindow = new Window("mainWindow", 100, 100, 400, 300, "My Application");

        Panel topPanel = new Panel("topPanel", 10, 35, 380, 50);
        topPanel.add(new Button("saveBtn", 10, 10, "Save"));
        topPanel.add(new Button("loadBtn", 120, 10, "Load"));

        Panel centerPanel = new Panel("centerPanel", 10, 95, 380, 150);
        centerPanel.add(new Label("titleLabel", 10, 10, "Document Title:"));

        mainWindow.add(topPanel);
        mainWindow.add(centerPanel);

        Graphics mockGraphics = new MockGraphics();
        mainWindow.render(mockGraphics);                                   // 트리 전체를 재귀적으로 렌더링
        mainWindow.handleEvent(new Event(EventType.CLICK, 120, 110));      // 이벤트도 트리 전체에 재귀적으로 전파
    }
}

// 재귀적 구조 순회 유틸리티 - countComponents/maxDepth 모두 "자신 + 자식들의 결과를 합산"하는 동일한 재귀 골격을 따른다.
// findByName, printTree 같은 다른 순회도 이 골격에서 종료 조건과 결합 방식만 바꾸면 된다.
public class CompositeUtils {
    public static int countComponents(UIComponent root) {
        return 1 + root.getChildren().stream()
                .mapToInt(CompositeUtils::countComponents)
                .sum();
    }

    public static int maxDepth(UIComponent root) {
        if (root.getChildren().isEmpty()) {
            return 1;
        }
        return 1 + root.getChildren().stream()
                .mapToInt(CompositeUtils::maxDepth)
                .max()
                .orElse(0);
    }
}

패턴의 수학적 본질과 현대적 진화

함수 합성으로서의 Decorator

Decorator 패턴의 수학적 본질은 **함수 합성(Function Composition)**입니다. 함수형 프로그래밍의 관점에서 보면 더욱 명확해집니다.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
// 순수 함수형 Decorator 구현
@FunctionalInterface
public interface Processor<T> extends Function<T, T> {
    
    // 함수 합성 (g ∘ f)(x) = g(f(x))
    default Processor<T> compose(Processor<T> before) {
        return input -> this.apply(before.apply(input));
    }
    
    // 체이닝 (f ∘ g)(x) = f(g(x))
    default Processor<T> andThen(Processor<T> after) {
        return input -> after.apply(this.apply(input));
    }
    
    // 조건부 적용
    default Processor<T> when(Predicate<T> condition) {
        return input -> condition.test(input) ? this.apply(input) : input;
    }
    
    // 로깅 기능 추가
    default Processor<T> withLogging(String description) {
        return input -> {
            System.out.println("Before " + description + ": " + input);
            T result = this.apply(input);
            System.out.println("After " + description + ": " + result);
            return result;
        };
    }
}

// 함수형 프로세서들의 라이브러리
public class Processors {
    
    // 기본 변환들
    public static final Processor<String> TRIM = String::trim;
    public static final Processor<String> TO_UPPER = String::toUpperCase;
    public static final Processor<String> TO_LOWER = String::toLowerCase;
    public static final Processor<String> REVERSE = s -> new StringBuilder(s).reverse().toString();
    
    // 파라미터화된 변환들
    public static Processor<String> replace(String target, String replacement) {
        return s -> s.replace(target, replacement);
    }
    
    public static Processor<String> addPrefix(String prefix) {
        return s -> prefix + s;
    }
    
    public static Processor<String> addSuffix(String suffix) {
        return s -> s + suffix;
    }
    
    public static Processor<String> truncate(int maxLength) {
        return s -> s.length() > maxLength ? s.substring(0, maxLength) + "..." : s;
    }
    
    // 고차 함수를 이용한 조합
    public static <T> Processor<T> repeat(Processor<T> processor, int times) {
        return input -> {
            T result = input;
            for (int i = 0; i < times; i++) {
                result = processor.apply(result);
            }
            return result;
        };
    }
    
    // 병렬 처리
    public static <T> Processor<List<T>> parallel(Processor<T> processor) {
        return list -> list.parallelStream()
                .map(processor)
                .collect(Collectors.toList());
    }
}

// 아름다운 함수 합성 예시
public class FunctionalCompositionExample {
    public static void main(String[] args) {
        // 복잡한 텍스트 처리 파이프라인
        Processor<String> pipeline = Processors.TRIM
            .andThen(Processors.TO_LOWER)
            .andThen(Processors.replace("  ", " "))
            .andThen(Processors.addPrefix("📝 "))
            .andThen(Processors.addSuffix(" [processed]"))
            .andThen(Processors.truncate(50))
            .withLogging("text-processing");
        
        String input = "  HELLO   WORLD  WITH   SPACES  ";
        String result = pipeline.apply(input);
        System.out.println("Final result: " + result);
        
        // 조건부 처리
        Processor<String> conditionalPipeline = Processors.TRIM
            .andThen(Processors.TO_UPPER.when(s -> s.length() < 10))
            .andThen(Processors.REVERSE.when(s -> s.contains("HELLO")));
        
        System.out.println("Conditional: " + conditionalPipeline.apply("hello"));
        
        // 반복 적용
        Processor<String> doubleReverse = Processors.repeat(Processors.REVERSE, 2);
        System.out.println("Double reverse: " + doubleReverse.apply("hello")); // "hello"
        
        // 리스트 처리
        List<String> texts = Arrays.asList("  hello  ", "  WORLD  ", "  java  ");
        Processor<String> itemProcessor = Processors.TRIM.andThen(Processors.TO_UPPER);
        List<String> processed = Processors.parallel(itemProcessor).apply(texts);
        System.out.println("Processed list: " + processed);
    }
}

React HOC: 현대적 Decorator의 진화

React의 Higher-Order Components(HOC)는 Decorator 패턴의 현대적 진화형입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// React HOC 예시 (JavaScript)
const withAuth = (WrappedComponent) => {
    return class extends React.Component {
        componentDidMount() {
            if (!this.props.isAuthenticated) {
                this.props.history.push('/login');
            }
        }
        
        render() {
            if (!this.props.isAuthenticated) {
                return <div>Please login...</div>;
            }
            return <WrappedComponent {...this.props} />;
        }
    };
};

const withLoading = (WrappedComponent) => {
    return (props) => {
        if (props.isLoading) {
            return <div>Loading...</div>;
        }
        return <WrappedComponent {...props} />;
    };
};

const withErrorBoundary = (WrappedComponent) => {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = { hasError: false };
        }
        
        static getDerivedStateFromError(error) {
            return { hasError: true };
        }
        
        componentDidCatch(error, errorInfo) {
            console.error('Error caught by boundary:', error, errorInfo);
        }
        
        render() {
            if (this.state.hasError) {
                return <div>Something went wrong.</div>;
            }
            return <WrappedComponent {...this.props} />;
        }
    };
};

// HOC 체이닝 (Decorator 패턴의 완벽한 구현)
const EnhancedUserProfile = withErrorBoundary(
    withAuth(
        withLoading(UserProfile)
    )
);

// 함수형 스타일로 더 우아하게
const enhance = compose(
    withErrorBoundary,
    withAuth,
    withLoading
);

const EnhancedUserProfile2 = enhance(UserProfile);

Java의 Stream API: Composite + Decorator의 조화

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Stream API는 Composite와 Decorator 패턴의 완벽한 조합
public class StreamCompositeExample {
    
    public static void main(String[] args) {
        List<String> words = Arrays.asList(
            "functional", "programming", "is", "beautiful",
            "decorator", "composite", "patterns", "rock"
        );
        
        // 복잡한 변환 파이프라인 (Decorator 체인)
        List<String> result = words.stream()
            .filter(word -> word.length() > 2)           // 필터링
            .map(String::toUpperCase)                     // 대문자 변환
            .map(word -> "*** " + word + " ***")          // 장식 추가
            .sorted()                                     // 정렬
            .limit(5)                                     // 제한
            .collect(Collectors.toList());                // 수집
        
        result.forEach(System.out::println);
        
        // 그룹화와 집계 (Composite 구조)
        Map<Integer, List<String>> groupedByLength = words.stream()
            .collect(Collectors.groupingBy(String::length));
        
        // 재귀적 구조 처리
        groupedByLength.forEach((length, wordList) -> {
            System.out.println("Length " + length + ":");
            wordList.forEach(word -> System.out.println("  - " + word));
        });
        
        // 복잡한 집계 연산 (Fold 연산)
        String concatenated = words.stream()
            .filter(word -> word.length() > 4)
            .map(String::toUpperCase)
            .reduce("", (acc, word) -> acc.isEmpty() ? word : acc + " | " + word);
        
        System.out.println("Concatenated: " + concatenated);
    }
}

AOP(Aspect-Oriented Programming)와의 관계

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// Spring AOP의 Decorator 패턴 구현
@Component
public class UserService {
    
    @Transactional              // 트랜잭션 Decorator
    @Cacheable("users")         // 캐싱 Decorator
    @LogExecutionTime           // 로깅 Decorator
    @ValidateParams             // 검증 Decorator
    public User createUser(CreateUserRequest request) {
        // 비즈니스 로직
        return new User(request.getName(), request.getEmail());
    }
}

// 커스텀 Aspect (Decorator) 구현
@Aspect
@Component
public class ExecutionTimeAspect {
    
    @Around("@annotation(LogExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        try {
            Object result = joinPoint.proceed();  // 원본 메서드 실행
            return result;
        } finally {
            long endTime = System.currentTimeMillis();
            String methodName = joinPoint.getSignature().getName();
            System.out.println(methodName + " executed in " + (endTime - startTime) + "ms");
        }
    }
}

@Aspect
@Component
public class ValidationAspect {
    
    @Before("@annotation(ValidateParams)")
    public void validateParams(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg == null) {
                throw new IllegalArgumentException("Parameter cannot be null");
            }
            // 추가 검증 로직
        }
    }
}

// 동적 프록시를 이용한 Decorator 구현
public class DynamicDecoratorExample {
    
    public interface Calculator {
        int add(int a, int b);
        int multiply(int a, int b);
    }
    
    public static class SimpleCalculator implements Calculator {
        @Override
        public int add(int a, int b) {
            return a + b;
        }
        
        @Override
        public int multiply(int a, int b) {
            return a * b;
        }
    }
    
    // 로깅 Decorator
    public static Calculator withLogging(Calculator calculator) {
        return (Calculator) Proxy.newProxyInstance(
            Calculator.class.getClassLoader(),
            new Class[]{Calculator.class},
            (proxy, method, args) -> {
                System.out.println("Calling " + method.getName() + " with args: " + Arrays.toString(args));
                Object result = method.invoke(calculator, args);
                System.out.println("Result: " + result);
                return result;
            }
        );
    }
    
    // 캐싱 Decorator
    public static Calculator withCaching(Calculator calculator) {
        Map<String, Object> cache = new ConcurrentHashMap<>();
        
        return (Calculator) Proxy.newProxyInstance(
            Calculator.class.getClassLoader(),
            new Class[]{Calculator.class},
            (proxy, method, args) -> {
                String key = method.getName() + Arrays.toString(args);
                return cache.computeIfAbsent(key, k -> {
                    try {
                        return method.invoke(calculator, args);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                });
            }
        );
    }
    
    public static void main(String[] args) {
        Calculator calculator = new SimpleCalculator();
        
        // Decorator 체이닝
        Calculator enhanced = withLogging(withCaching(calculator));
        
        System.out.println(enhanced.add(2, 3));      // 캐시 미스, 계산 후 저장
        System.out.println(enhanced.add(2, 3));      // 캐시 히트
        System.out.println(enhanced.multiply(4, 5)); // 새로운 계산
    }
}

성능 분석과 실무 고려사항

성능 특성 분석

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 성능 측정 결과 (마이크로초/operation)
/*
Decorator 체인 성능:
체인 길이    | 평균 실행시간 | 메모리 오버헤드 | 복잡도
1개         |     10μs    |      +5%     |  낮음
3개         |     15μs    |      +15%    |  중간
5개         |     22μs    |      +25%    |  중간
10개        |     45μs    |      +50%    |  높음
20개        |     95μs    |      +100%   |  매우높음

Composite 구조 성능:
트리 깊이    | 순회 시간    | 메모리 사용량  | 스택 깊이
3단계       |     50μs    |     500KB    |   낮음
5단계       |     120μs   |     1.2MB    |   중간
10단계      |     300μs   |     3.5MB    |   높음
15단계      |     650μs   |     8.2MB    |   위험

결론:
- Decorator: 체인이 길어질수록 선형적 성능 저하
- Composite: 깊이가 깊어질수록 메모리와 스택 사용량 증가
- 실무에서는 적절한 깊이/길이 제한 필요

※ 위 수치는 특정 환경에서 관찰될 수 있는 예시 값이며, JVM 워밍업·하드웨어·JIT 최적화 여부에 따라 실제 측정치는 크게 달라질 수 있습니다. 절대값보다 "체인이 길어지거나 트리가 깊어질수록 비용이 커진다"는 경향성에 주목하세요.
*/

// 최적화된 Composite 구현
public abstract class OptimizedComponent {
    private static final int MAX_DEPTH = 10;
    private final int depth;
    
    protected OptimizedComponent(int depth) {
        if (depth > MAX_DEPTH) {
            throw new IllegalArgumentException("Maximum depth exceeded: " + depth);
        }
        this.depth = depth;
    }
    
    // 꼬리 재귀 최적화를 위한 반복적 순회
    public void traverseIteratively(Consumer<OptimizedComponent> visitor) {
        Stack<OptimizedComponent> stack = new Stack<>();
        stack.push(this);
        
        while (!stack.isEmpty()) {
            OptimizedComponent current = stack.pop();
            visitor.accept(current);
            
            // 자식들을 역순으로 스택에 추가 (원래 순서 유지)
            List<OptimizedComponent> children = current.getChildren();
            for (int i = children.size() - 1; i >= 0; i--) {
                stack.push(children.get(i));
            }
        }
    }
}

메모리 관리와 최적화

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 메모리 효율적인 Composite 구현
public class MemoryEfficientComposite {
    
    // Flyweight 패턴과 결합한 최적화
    private static final Map<String, Component> COMPONENT_CACHE = new ConcurrentHashMap<>();
    
    public static Component getCachedComponent(String type, String name) {
        String key = type + ":" + name;
        return COMPONENT_CACHE.computeIfAbsent(key, k -> createComponent(type, name));
    }
    
    // 지연 로딩을 통한 메모리 절약
    public static class LazyComposite extends Component {
        private final Supplier<List<Component>> childrenSupplier;
        private List<Component> cachedChildren;
        
        public LazyComposite(String name, Supplier<List<Component>> childrenSupplier) {
            super(name);
            this.childrenSupplier = childrenSupplier;
        }
        
        @Override
        public List<Component> getChildren() {
            if (cachedChildren == null) {
                cachedChildren = childrenSupplier.get();
            }
            return cachedChildren;
        }
    }
    
    // 약한 참조를 이용한 메모리 누수 방지
    public static class WeakReferenceComposite extends Component {
        private final List<WeakReference<Component>> weakChildren = new ArrayList<>();
        
        public void addChild(Component child) {
            // 가비지 컬렉션된 참조들 정리
            weakChildren.removeIf(ref -> ref.get() == null);
            weakChildren.add(new WeakReference<>(child));
        }
        
        @Override
        public List<Component> getChildren() {
            return weakChildren.stream()
                    .map(WeakReference::get)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList());
        }
    }
}

한눈에 보는 Decorator & Composite 패턴

Decorator vs Composite 핵심 비교

비교 항목Decorator 패턴Composite 패턴
핵심 목적동적 기능 확장부분-전체 계층 표현
수학적 본질함수 합성 f(g(h(x)))트리 재귀 구조
구조선형 체인 (래퍼 스택)트리 구조 (노드-리프)
재귀 방향안쪽으로 위임자식 노드로 전파
클라이언트 관점장식 여부 투명Leaf/Composite 동일 취급
확장 방식새 Decorator 추가새 노드 추가

적용 시나리오 비교

시나리오DecoratorComposite
음료 토핑 시스템OX
파일/폴더 구조XO
I/O 스트림 래핑OX
GUI 컴포넌트 계층XO
텍스트 필터 체인OX
조직도/메뉴 트리XO
로깅/모니터링 추가OX

구현 특성 비교

특성DecoratorComposite
공통 인터페이스필수 (Component)필수 (Component)
래핑/포함 관계1:1 (단일 래핑)1:N (다중 자식)
순서 중요성높음 (체이닝 순서)낮음 (순회 순서만)
메모리 패턴객체당 래퍼 오버헤드트리 깊이에 비례
성능 특성체인 길이에 선형트리 깊이에 의존

성능 벤치마크 가이드

지표Decorator 한계Composite 한계
권장 최대 깊이체인 5-7단계트리 10-15레벨
권장 깊이에서 호출 1회당 시간(참고)~15-30μs~200-400μs
메모리 오버헤드단계당 ~8-16바이트노드당 ~24-48바이트
스택 위험낮음깊은 트리에서 주의

※ “권장 깊이에서 호출 1회당 시간"은 위 “성능 특성 분석” 표에서 Decorator 5개(22μs)7개 수준, Composite 1015단계(300~650μs) 구간을 그대로 가져온 것으로, 척도(1회 호출 기준 마이크로초)를 위 표와 통일했습니다. 두 표 모두 같은 방식(단일 호출 마이크로벤치마크)으로 측정한 예시 값이며, 환경에 따라 달라지므로 실제 도입 전 대상 시스템에서 직접 벤치마크해 재설정하세요.

패턴 조합 가이드

조합효과사용 예
Decorator + Strategy동적 기능 + 알고리즘 교체압축 방식 선택 가능한 래퍼
Composite + Iterator트리 + 순회 추상화파일 시스템 탐색
Decorator + Factory동적 기능 + 생성 캡슐화설정 기반 필터 체인
Composite + Visitor트리 + 연산 분리문서 분석/변환

적용 체크리스트

Decorator 체크 항목Composite 체크 항목
런타임에 기능 추가/제거 필요?부분-전체 계층 구조인가?
상속 없이 기능 확장 필요?단일 객체와 그룹을 동일 취급?
조합 가능한 기능들인가?트리 구조로 표현 가능한가?
기존 클래스 수정 불가?재귀적 집계 연산 필요?

평가 기준

독자가 이 글을 읽은 후 달성해야 할 목표:

  • Decorator를 함수 합성 f(g(h(x)))로, Composite를 재귀적 트리 구조로 각각 설명할 수 있다
  • 투명한(transparent) Composite와 안전한(safe) Composite의 차이, 그리고 각각의 트레이드오프를 구별할 수 있다
  • “적용 시나리오 비교” 표를 근거로 실제 문제가 Decorator/Composite 중 어느 쪽에 해당하는지 판단할 수 있다
  • 체인 길이·트리 깊이가 늘어날 때 성능이 어떻게 저하되는지, 그리고 그 수치가 예시값임을 이해한다

흔한 오해

“Decorator와 상속은 완전히 다른 개념이다” — 절반만 맞습니다. Decorator는 상속을 아예 안 쓰는 게 아니라, 상속은 “공통 인터페이스를 정의"하는 데만 쓰고 “기능 조합"은 컴포지션으로 옮긴 것입니다. UIComponent를 상속하는 Button/Panel처럼, Decorator 구현체도 대개 공통 Component 인터페이스를 상속(구현)합니다. 다만 여러 기능의 조합을 위해 클래스를 늘리는 대신 객체를 층층이 감싸는 것이 핵심 차이입니다.

“Composite는 항상 트리를 명시적으로 순회해야 한다” — Stream API 예시에서 보듯, reducecollect 같은 고차 연산은 재귀 순회를 라이브러리 내부로 숨깁니다. 트리를 직접 방문(visit)하는 코드를 짜지 않아도 Composite의 “부분-전체 동일 취급” 이점은 그대로 유지됩니다.

“Decorator 체인 순서는 상관없다” — 순서가 결과에 영향을 주지 않는 경우(예: 로깅만 추가하는 두 Decorator)도 있지만, 일반적으로는 순서가 중요합니다. withCaching(withLogging(calculator))withLogging(withCaching(calculator))는 동작이 다릅니다. 전자는 캐시된 결과에 대해서도 매번 로그를 남기고, 후자는 캐시 히트 시 로그가 아예 찍히지 않습니다. 어떤 순서가 옳은지는 각 Decorator가 부수효과(side effect)를 갖는지에 달려 있습니다.

결론: 재귀적 아름다움의 현대적 의미

Decorator와 Composite 패턴을 깊이 탐구한 결과, 이들은 단순한 구현 기법을 넘어서 소프트웨어 설계의 수학적 본질을 드러내는 패턴들임을 확인했습니다.

패턴의 핵심 가치와 적용 가이드라인

Decorator 패턴은 f(g(h(x)))로 요약되는 함수 합성을 객체지향적으로 구현하여, 런타임에 객체 기능을 조합하는 유연성과 각 장식자가 단일 책임을 갖는 깔끔한 관심사 분리를 제공합니다. 이런 특성 덕분에 객체에 동적으로 기능을 추가해야 하거나, 상속만으로는 해결하기 어려운 다중 기능 조합, 로깅·보안 같은 횡단 관심사(Cross-cutting Concerns) 처리에 적합합니다. React HOC & Hooks, Java Stream API, Spring AOP가 이 아이디어의 현대적 구현입니다. 다만 체인이 너무 길어지면 성능과 가독성이 함께 저하되므로, 실무에서는 5~7단계 이내로 제한하는 것이 안전합니다.

Composite 패턴은 부분과 전체를 동일하게 다루는 투명성을 바탕으로 트리 구조를 자연스럽게 표현하고, 재귀적 집계 연산을 간결하게 구현할 수 있게 해줍니다. 부분-전체 계층구조를 표현해야 하거나, 개별 객체와 컬렉션을 동일한 인터페이스로 다루고 싶을 때, 파일 시스템·조직도·Virtual DOM·AST(추상 구문 트리)처럼 트리 형태의 데이터를 설계할 때 적합합니다. 다만 깊은 재귀는 스택 오버플로우 위험을 동반하므로, 순환 참조 탐지·방지 메커니즘과 필요시 반복적(iterative) 순회로의 전환을 함께 고려해야 합니다.

두 패턴이 보여주는 가장 큰 가치는 함수 합성과 재귀라는 수학적 개념을 실용적 코드로 번역하는 능력입니다. 이 번역 능력은 여전히 유효해서, 오늘날의 함수형 파이프라인이나 컴포넌트 기반 프레임워크 상당수가 이 두 패턴의 사고방식을 그대로 계승하고 있습니다.

Decorator와 Composite 패턴은 재귀적 사고의 아름다움을 보여주는 동시에, 복잡한 현실 문제를 우아하게 해결하는 실용적 도구입니다. 이들을 이해하고 활용함으로써, 우리는 더 유연하고 확장 가능한 소프트웨어를 설계할 수 있습니다.

다음 글에서는 Proxy 패턴을 탐구하겠습니다. 객체에 대한 접근을 제어하고, 지연 로딩과 보안을 우아하게 구현하는 이 패턴의 다면적 활용법을 살펴보겠습니다.


핵심 메시지: “Decorator와 Composite는 소프트웨어에서 수학적 아름다움을 구현한 패턴들이다. 재귀적 구조를 통해 무한한 확장성을 제공하면서도, 일관된 인터페이스로 복잡성을 숨긴다. 현대의 함수형 프로그래밍과 컴포넌트 기반 개발의 철학적 기초가 되는 패턴들이다.”