Decorator Pattern 정리

테사벨로그·2025년 10월 22일

Design Pattern

목록 보기
4/19

1. 왜 Decorator Pattern이 생겨났는가?

문제 1

  • 각 조합마다 클래스 만들자고? 너무 복잡해!

문제 2

  • 아까보다는 나아보임
  • has** 함수를 봐라 너무 과하게 많다.
  • 더블 추가면 어떻게 할건데? 불가능함
  • 녹차에 휘핑크림 넣을 수 없게 어떻게 막을건데? 못막음

문제점:

  • 새로운 첨가물 추가 시 기존 코드 수정 필요 (OCP 위반)
  • 첨가물 가격 변경 시 코드 수정 필요
  • 일부 음료에 맞지 않는 첨가물도 강제로 가짐
  • 더블 모카처럼 같은 첨가물 여러 번 추가 불가능
  • 첨가물에 대해서 다 테스트해볼거야?

2. Component Interface VS Decorator Class

1. Component (Beverage)

  • "나는 기본 음료이거나 장식된 음료입니다"
  • 모든 객체가 구현해야 할 공통 규격 정의
  • "is-a" 관계의 기반
public abstract class Beverage {
    protected String description = "Unknown Beverage";
    
    public String getDescription() {
        return description;
    }
    
    public abstract double cost();  // 모든 음료가 구현
}

왜 Abstract Class인가?

  • 모든 음료와 데코레이터가 공유하는 타입
  • description 같은 공통 필드 제공
  • 기본 행동(getDescription)도 정의 가능

2. Decorator (CondimentDecorator)

  • "나는 음료를 감싸서 기능을 추가합니다"
  • Component를 감싸는 규격 정의
  • "has-a" 관계 (Decorator는 Component를 가짐)
public abstract class CondimentDecorator extends Beverage {
    protected Beverage beverage;  // 감쌀 대상 보관
    public abstract String getDescription();
}

왜 Beverage를 상속하는가?

  • Decorator도 Beverage 타입이어야 함
  • 다른 Decorator로 감쌀 수 있게 하기 위해
  • 클라이언트는 원본과 장식된 객체를 동일하게 취급

3. 왜 상속과 조합을 함께 사용하는가?

핵심: "타입 매칭"을 위한 상속 + "행동 확장"을 위한 조합

// Decorator는 Beverage를 '상속' (타입 통일)
public class Mocha extends CondimentDecorator {
    // Beverage를 '조합' (기능 확장)
    private Beverage beverage;
    
    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }
    
    // 기존 행동에 새 행동 추가
    public double cost() {
        return 0.20 + beverage.cost();  // 위임 + 추가
    }
}

상속의 목적:

  • 타입을 통일하여 같은 인터페이스 제공
  • Beverage beverage = new Mocha(new Espresso()) 가능

조합의 목적:

  • 런타임에 동적으로 기능 확장
  • 기존 코드 수정 없이 새로운 기능 추가 (OCP)

4. Decorator Pattern 핵심 구조

  • Decorator는 Component를 "상속" (같은 타입)
  • Decorator는 Component를 "포함" (감싸기)
  • 무한히 감쌀 수 있음: Whip(Mocha(Mocha(Espresso)))

5. 예시 코드

Step 1: Component 정의

// Component (기본 음료)
public abstract class Beverage {
    protected String description = "Unknown Beverage";
    
    public String getDescription() {
        return description;
    }
    
    public abstract double cost();
}

Step 2: ConcreteComponent 구현

// 구체적인 음료들
public class Espresso extends Beverage {
    public Espresso() {
        description = "Espresso";
    }
    
    public double cost() {
        return 1.99;  // 에스프레소 가격
    }
}

public class DarkRoast extends Beverage {
    public DarkRoast() {
        description = "Dark Roast Coffee";
    }
    
    public double cost() {
        return 0.99;
    }
}

Step 3: Decorator 정의

// Decorator 추상 클래스
public abstract class CondimentDecorator extends Beverage {
    protected Beverage beverage;  // 감쌀 음료
    public abstract String getDescription();
}

Step 4: ConcreteDecorator 구현

// 모카 첨가물
public class Mocha extends CondimentDecorator {
    
    public Mocha(Beverage beverage) {
        this.beverage = beverage;  // 음료를 감쌈
    }
    
    public String getDescription() {
        // 기존 설명에 모카 추가
        return beverage.getDescription() + ", Mocha";
    }
    
    public double cost() {
        // 기존 가격에 모카 가격 추가 (핵심!)
        return 0.20 + beverage.cost();
    }
}

// 휘핑크림 첨가물
public class Whip extends CondimentDecorator {
    
    public Whip(Beverage beverage) {
        this.beverage = beverage;
    }
    
    public String getDescription() {
        return beverage.getDescription() + ", Whip";
    }
    
    public double cost() {
        return 0.10 + beverage.cost();
    }
}

// 두유 첨가물
public class Soy extends CondimentDecorator {
    
    public Soy(Beverage beverage) {
        this.beverage = beverage;
    }
    
    public String getDescription() {
        return beverage.getDescription() + ", Soy";
    }
    
    public double cost() {
        return 0.15 + beverage.cost();
    }
}

Step 5: 실행

public class StarbuzzCoffee {
    public static void main(String[] args) {
        
        // 1. 아무것도 추가하지 않은 에스프레소
        Beverage beverage1 = new Espresso();
        System.out.println(beverage1.getDescription() 
            + " $" + beverage1.cost());
        // 출력: Espresso $1.99
        
        // 2. 다크 로스트 + 모카 + 모카 + 휘핑크림
        Beverage beverage2 = new DarkRoast();
        beverage2 = new Mocha(beverage2);     // 첫 번째 모카로 감싸기
        beverage2 = new Mocha(beverage2);     // 두 번째 모카로 감싸기
        beverage2 = new Whip(beverage2);      // 휘핑크림으로 감싸기
        System.out.println(beverage2.getDescription() 
            + " $" + beverage2.cost());
        // 출력: Dark Roast Coffee, Mocha, Mocha, Whip $1.49
        
        // 3. 하우스 블렌드 + 두유 + 모카 + 휘핑크림
        Beverage beverage3 = new HouseBlend();
        beverage3 = new Soy(beverage3);
        beverage3 = new Mocha(beverage3);
        beverage3 = new Whip(beverage3);
        System.out.println(beverage3.getDescription() 
            + " $" + beverage3.cost());
        // 출력: House Blend Coffee, Soy, Mocha, Whip $1.34
    }
}

실행 흐름 (beverage2의 경우)

beverage2.cost() 호출 순서:

1. Whip.cost() 
   → 0.10 + beverage.cost() 호출
   
2. Mocha.cost() 
   → 0.20 + beverage.cost() 호출
   
3. Mocha.cost() 
   → 0.20 + beverage.cost() 호출
   
4. DarkRoast.cost() 
   → 0.99 반환

역순으로 계산:
0.99 + 0.20 + 0.20 + 0.10 = 1.49

6. Java I/O에서의 Decorator Pattern

Java I/O 구조

// FileInputStream: Component (파일에서 바이트 읽기)
// BufferedInputStream: Decorator (버퍼링 기능 추가)
// LineNumberInputStream: Decorator (줄 번호 추가)

InputStream in = 
    new LineNumberInputStream(        // 줄 번호 기능
        new BufferedInputStream(      // 버퍼링 기능
            new FileInputStream("file.txt")  // 기본 파일 읽기
        )
    );

커스텀 Decorator 만들기

// 소문자 변환 Decorator
public class LowerCaseInputStream extends FilterInputStream {
    
    public LowerCaseInputStream(InputStream in) {
        super(in);  // Component 저장
    }
    
    // 한 바이트 읽기 - 소문자로 변환
    public int read() throws IOException {
        int c = super.read();  // 감싼 스트림에서 읽기
        return (c == -1 ? c : Character.toLowerCase((char)c));
    }
    
    // 여러 바이트 읽기 - 소문자로 변환
    public int read(byte[] b, int offset, int len) throws IOException {
        int result = super.read(b, offset, len);
        for (int i = offset; i < offset + result; i++) {
            b[i] = (byte)Character.toLowerCase((char)b[i]);
        }
        return result;
    }
}

사용 예시

public class InputTest {
    public static void main(String[] args) throws IOException {
        int c;
        
        try {
            // 여러 Decorator로 감싸기
            InputStream in = 
                new LowerCaseInputStream(      // 소문자 변환
                    new BufferedInputStream(   // 버퍼링
                        new FileInputStream("test.txt")  // 파일 읽기
                    )
                );
            
            while((c = in.read()) >= 0) {
                System.out.print((char)c);
            }
            
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

7. 핵심 정리

Design Principle: OCP (Open-Closed Principle)

"확장에는 열려있고, 수정에는 닫혀있어야 한다"

  • 새로운 Decorator 추가 → 기존 코드 수정 불필요
  • 새로운 기능 추가 → 새로운 클래스만 작성

Decorator Pattern의 구성

요소역할특징
Component기본 인터페이스 정의원본과 장식된 객체 모두의 기반 타입
ConcreteComponent실제 기본 객체기능이 추가될 원본 객체 (Espresso, DarkRoast)
Decorator장식자 추상 클래스Component를 포함하고 상속함
ConcreteDecorator실제 장식 기능새로운 행동/책임 추가 (Mocha, Whip)

언제 사용하는가?

  • 런타임에 객체에 책임 추가가 필요할 때
  • 상속으로 확장이 어렵거나 비실용적일 때
  • 기능을 동적으로 조합해야 할 때
  • 기존 코드 수정 없이 확장이 필요할 때

핵심 메커니즘

  1. 타입 통일: Decorator가 Component와 같은 타입

    • Beverage beverage = new Mocha(new Espresso())
  2. 위임 (Delegation): Decorator가 Component에게 작업 위임

    public double cost() {
        return 0.20 + beverage.cost();  // 위임 + 추가
    }
  3. 재귀적 구조: Decorator를 Decorator로 감쌀 수 있음

    Whip(Mocha(Mocha(Espresso)))

장점 vs 단점

장점:

  • ✅ 객체에 동적으로 책임 추가 가능
  • ✅ 기능 조합의 유연성 (더블 모카 등)
  • ✅ 단일 책임 원칙 준수 (각 Decorator는 하나의 기능)
  • ✅ OCP 준수 (확장에 열림, 수정에 닫힘)

단점:

  • ❌ 작은 클래스가 많이 생성됨 (Java I/O 예시)
  • ❌ 패턴을 모르면 코드 이해 어려움
  • ❌ Decorator 순서에 따라 결과가 달라질 수 있음
  • ❌ 특정 Decorator를 제거하기 어려움

관련 패턴

  • Adapter: 다른 인터페이스 제공
  • Proxy: 같은 인터페이스 제공 (접근 제어)
  • Decorator: 향상된 인터페이스 제공 (기능 추가)

8. 실전 활용 시나리오

시나리오: 피자 주문 시스템

// Component
public abstract class Pizza {
    protected String description = "Basic Pizza";
    
    public String getDescription() {
        return description;
    }
    
    public abstract double cost();
}

// ConcreteComponents
public class Margherita extends Pizza {
    public Margherita() {
        description = "Margherita Pizza";
    }
    
    public double cost() {
        return 8.00;
    }
}

// Decorators
public class ExtraCheese extends ToppingDecorator {
    public ExtraCheese(Pizza pizza) {
        this.pizza = pizza;
    }
    
    public String getDescription() {
        return pizza.getDescription() + ", Extra Cheese";
    }
    
    public double cost() {
        return 1.50 + pizza.cost();
    }
}

// 사용
Pizza myPizza = new Margherita();
myPizza = new ExtraCheese(myPizza);
myPizza = new Olives(myPizza);
myPizza = new Mushrooms(myPizza);

System.out.println(myPizza.getDescription() + " $" + myPizza.cost());
// Margherita Pizza, Extra Cheese, Olives, Mushrooms $12.50
profile
다들 응원합니다.

0개의 댓글