dh_0e

[Design Pattern] Structural Design Patterns II (Composite Pattern, Decorator Pattern) 본문

Software Analysis & Design/Design Patterns

[Design Pattern] Structural Design Patterns II (Composite Pattern, Decorator Pattern)

dh_0e 2025. 11. 11. 15:46

Structural Design Patterns

 

Composite Pattern(복합체 패턴)

  • 전체-부분 관계의 트리 구조로 표현되는 객체들을 단일 객체(Interface)처럼 취급할 수 있게 해주는 구조 패턴
    • 단일 객체(Leaf)복합 객체(Composite)를 동일한 인터페이스를 사용하여 처리
    • Leaf는 하위 요소가 없는 단일 객체를 의미하며, Composite는 Leaf나 다른 Composite 객체들을 포함하는 복합 객체

 

Composite Pattern 필요 상황 예시

  • 주문 시스템에서 여러 상품을 담은 상자의 가격을 계산하려고 할 때
    • 한 box에 여러 product와 작은 box들을 담을수 있음
    • 작은 box들도 여러 product를 담을 수 있음
  • Origin: 상자의 가격을 계산하기 위해서는 내부 제품을 모두 살펴보면서 가격을 합산해야 함
  • 트리 전체 순회를 하면서 box와 product의 타입을 구분해주는 코드를 구성해야 하므로 코드가 복잡해질 수 있음
  • Composite Pattern Idea
    • 총가격을 계산하는 메서드($getPrice()$)를 가지는 공통 인터페이스(Component)를 통해서 products와 boxes에 대해 동일하게 작업하게 할 수 있음
      • Product의 경우 단순히 제품의 가격을 반환
      • Box의 경우 Box내 products의 총 가격을 반환
    • 트리를 구성하는 객체들의 구체적인 클래스 타입에 대해 신경 쓸 필요 없음(현재 보고있는 객체가 product인지 box인지 알 필요가 없음)
    • 복합체 패턴은 그릇과 내용물을 동일 시 해서 재귀적인 구조를 편하게 다룰 수 있게 해줌

복합체 패턴의 구조

 

  • 최상위: Component(요소) 
    • 인터페이스(or 추상 클래스)
    • 공통 연산 execute()를 정의
  • 하위 클래스 1: Leaf
    • Component를 상속/구현
    • 오버라이딩된 execute() 안에서 실제 일을 직접 수행
    • 더 이상 자식이 없는 "말단 노드"
  • 하위 클래스 2: Composite(복합체)
    • Component를 상속/구현
    • 자기 안에 children: Component[]를 가지고 있음 >> 포함 관계(합성)
      • Composite는 Component를 상속받으면서, 자기 안에 여러 Component들을 포함(합성)하는 구조
      • Composite에 포함되는 Component는 Composite가 사라지면 함께 사라짐(합성 관계) 
    • execute()를 오버라이딩해서 children에 들어있는 각 Component의 execute()를 호출하도록 위임
    • 자식 관리용 메서드(add, remove, getChildren)를 가짐
  • Client
    • 어떤 구체 클래스도 상속하지 않음
    • 단지 Component 타입에 의존해서 사용함
    • Leaf인지 Composite인지 신경 안 쓰고, 전부 Component로 다형성 있게 다룸

복합체 코드 구조
복합체 패턴 코드 예시
Client & main

사용 사례) Java Swing

JFrame(Composite)에 JTextField(Leaf)나 JButton(Leaf)과 같은 UI 컴퓨넌트들을 추가하는 방식으로 사용됨

  • 사용 시기
    • 객체의 구조가 트리로 표현되는 상황에서, 단일/복합 객체의 관계를 단순화하여 균일하게 처리하고 싶을 때
  • 장점
    • 다형성 재귀를 통해 복잡한 트리 구조를 보다 편리하게 다룰 수 있음
    • 새로운 leaf 클래스 추가하더라도 클라이언트에는 영향 없음 (OCP:개방 폐쇄 원칙 준수) 
  • 단점
    • 재귀 호출 특징 상 트리 깊이가 깊어지면 디버깅에 어려움이 생김
    • 기능이 너무 다른 클래스들 간에는 공통 인터페이스 설계가 까다로움
      • Component에 선언되는 메소드가 공통으로 활용될 수 있는 의미를 가져야 함 

 


 

Decorator Pattern(데코레이터 패턴)

  • 객체에 추가적인 기능을 동적으로 더할 수 있게 해주는 구조 패턴
    • 컴파일 타임이 아닌 런타임에 객체에 대한 기능 확장이 가능
  • 데코레이터(장식자)라는 뜻은 기본 제품에 재료/디자인을 추가 및 변경 해줌으로써 새로운 종류의 제품을 만들어내는 것을 의미
  • 필요한 상황
    • ex) 사용자에게 이벤트 알람을 주기 위한 알람 라이브러리 설계
    • Afflication에서는 Notifier를 통해 기본적으로 이메일로 알람을 함
    • 어느 시점부터 사용자들이 이메일 이상의 알람 기능을 원하여 아래와 같이 상속 구조로 Notifier를 확장했다고 가정
      • 상속 구조에서는 한번에 특정 Notifier만 동작하게 됨

  • 여러 채널을 통해서 알람을 보내고 싶다면?
    • 상속 구조를 유지한 채로는 모든 조합에 따른 Notifier를 구현해야 함
      • 코드의 양도 늘어나고 확장의 유연성 떨어짐

모든 Notifier 조합 구현

  • 이미 있는 여러 Notifier들을 runtime에 조립할 수 있을까? >> 데코레이션 패턴

Decorator Pattern 구조

  • Component
    • 인터페이스 or 추상 클래스
    • 공통 메소드: execute()
    • 기본 기능을 하는 애든, 데코레이터든 전부 component 타입으로 취급하겠다는 역할
  • Concrete component
    • Component를 상속/구현한 실체 클래스
    • 원래 기본 기능을 진짜로 수행하는 곳
    • 아직 아무런 추가 기능(Deco)이 없는 순수한 객체
  • Base Decorator
    • Component를 상속/구현
    • 내부에 wrappee: Component 필드를 하나 더 가지고 있음
    • 이 wrappee 필드와 포함/집합 관계를 가짐
      • Base Decorator에 포함되는 wrappee는 Base Decorator가 사라져도 유지됨 (집합 관계)
    • 생성자로 c: Component 어떤 Component든 받아서 wrappee에 저장
    • "나는 껍데기고, 진짜 일은 나를 감싸고 있는 애(wrappee)한테 시킨다"라는 느낌
  •  Concrete Decorators
    • ConcreteDecorator1, ConcreteDecorator2, .. 같은 애들
    • BaseDecorator를 상속
    • execute()를 오버라이드해서
      • 필요하면 추가 작업 진행 후 super.execute() or wrappee.execute()를 호출해서 원래 행동을 수행하게 함
        • super.execute(): 자식 execute() >> 부모 execute() >> wrappee.execute()
        • wrappee.execute(): 자식 execute() >> wrappee.execute()
      • 또는 super 호출 전/후에 둘 다 끼워넣기도 함
  • Client
    • 오직 Component 타입에만 의존
    • 사용하는 흐름은 위 예시처럼
      1. a = new ConcComponent(): 기본 컴포넌트
      2. b = new ConcDecorator1(a): a를 감싼 데코레이터1
      3. c = new ConcDecorator2(b): b를 또 감싼 데코레이터2
      4. c.execute() 호출
        • 데코레이터2의 execute()
        • 데코레이터1의 execute()
        • 마지막에 ConcreteComponent의 execute()가 순차적으로 호출됨
  • 정리
    • 상속
      • Component << ConcreteComponent
      • Component << BaseDecorator << ConcreteDecorator
    • 포함(합성)
      • BaseDecorator 안에 wrappee: Component가 포함됨
        • wrappee에는 ConcreteComponent도 들어갈 수 있고, 또 다른 ConcreteDecorator가 들어가서 계속 감쌀 수도 있음
  • Decorator는 Component를 상속받으면서, 자기 안에 다른 Component를 필드로 포함해서, 기존 객체를 감싸고 런타임 도중 그 앞뒤에 기능을 덧붙이는 패턴

 

Decorator Pattern 구조 예시
Decorator Pattern 코드 예시 (Application이 Client 역할을 한다고 보면 됨)
send 메소드를 각각에 맞게 오버라이딩 해주되, super의 send를 호출해주어야 함
Decorator로 계속 감싸면서 기능을 추가하는 로직이 보여야 함

  • 위 코드를 실행하면 E-mail, Facebook, SMS, Slack 순서대로 출력됨
    • 호출 순서: Slack에서 super 호출 >> wrappee에 있는 SMS.send 호출 >> SMS에선 또 super.send 호출
      >> Facebook.send에서 super.send로 DefaultNotifier.send 호출
    • return 순서: DefaultNotifer.send에서 email send하고 return >> Facebook send하고 return
      >> SMS send하고 return >> Slack send 하고 return(끝) 

Java에서 Decorator Pattern 사용 사례

  • Stream
    • 어댑터이면서 동시에 Decorator라고 볼 수 있음

  • Collections
    • checkedList(), synchronizedList(), unmodifiableList() 등등

 

사용 시기

  • 객체 책임과 행동이 동적으로 상황/조건에 따라 다양한 기능이 빈번하게 추가/삭제되는 경우
    • 객체를 생성하는 코드에 변경이 없으면서 런타임에 추가 가능
  • 객체의 결합을 통해 기능이 생성되어야 하는 경우
  • 상속을 통해 서브 클래싱으로 객체의 동작을 확장하는 것이 어색하거나 불가능할 때

 

Decorator Pattern 장단점

  • 장점
    • 상속을 통해 서브클래스로 확장하는 것보다 훨씬 유연하게 기능을 확장할 수 있음
    • 객체를 여러 데코레이터로 래핑하여 여러 동작을 클라이언트가 자유롭게 결합할 수 있음
    • 단일 책임 원칙(SRP) 준수: 각 데코레이터 클래스마다 고유의 책임을 가짐
    • 개방 폐쇄 원칙(OCP) 준수: 클라이언트 코드 수정 없이 데코레이터 클래스 추가만으로도 기능 확장이 가능함
    • 의존 역전 원칙(DIP) 준수: 구현체가 아닌 인터페이스를 바라보기 때문에
      • Application(Client) 입장에서 Notifier 타입만 보고 있으며 Decorator가 얼마나 감싸져 있는지 모름
        상위 모듈(Application)이 추상(Notifier)에만 의존하고,
        구체 구현(DefaultNotifier/Decorators)은 그 추상에 맞춰짐
  •  단점
    • 만일 Decorator 일부를 동적으로 제거하고 싶다면, wrapper 스택에서 특정 wrapper를 제거하는 것이 어려움
    • 데코레이터를 조합하는 방식에 코드 복잡도가 올라갈 수 있음
    • 어느 장식자를 먼저 데코레이팅 하느냐에 따라 데코레이터 스택 순서가 결정지어지게 되므로,  순서에 의존하지 않는 방식으로 데코레이터를 구현하기 어려움
      • 어느 데코레이터를 먼저 감싸느냐에 따라 그에 대한 행동이 따르기에 순서에 유의

 

Summary

  • 복합체 패턴(Composite Pattern)
    • 전체-부분 관계의 트리 구조로 표현되는 객체들을 단일 객체처럼 취급할 수 있게 해주는 구조 패턴
  • 데코레이터 패턴(Decorator Pattern)
    • 객체에 추가적인 기능을 동적으로 더할 수 있게 해주는 구조 패턴
    • 컴파일 타임이 아닌 런타임에 객체에 대한 기능 확장이 가능