JAVA

[JAVA] 디자인패턴 - 데코레이터 패턴(Decorator Pattern)을 알아보자

sagecode 2024. 12. 3. 18:20

1. 데코레이터 패턴(Decorator Pattern)

데코레이션 패턴은 구조 패턴 중 하나로, 객체의 동작을 확장할 수 있도록 해주는 패턴이다. 기본 기능을 구현한 후 추가할 수 있는 기능의 종류가 많은 경우, Decorator 클래스로 정의 한 뒤 Decorator 객체를 조합함으로써 추가 기능을 덧붙인다. 

 

  • 데코레이터 패턴을 왜 사용하는가?

데코레이터 패턴을 사용하면 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있다. 상속보다는 구성(Composition)을 활용하여 객체의 유연성과 재사용성을 높인다.

구성(Composition)이란? 객체의 재사용과 확장을 위해 사용하는 설계 기법 중 하나로 클래스간의 "포함(has a ~)" 관계를 표현한다. 객체를 상속하는게 아닌 다른 객체를 멤버 변수로 포함하여 기능을 확장하는 방식이다.

 

그럼 왜 상속보다 합성을 더 자주 사용하는 이유가 무엇일까? 물론 중복 코드를 제거하고 클래스를 묶는 다형성을 이용할 수 있어서 좋은 기술처럼 보인다. 하지만 상속은 정말 개념적으로 포함되어 있을때만 제한적으로 사용한다.

  1. 상속을 하게 되면 부모 클래스와 자식 클래스의 관계가 컴파일 시점에 관계가 결정되기 때문에 결합도가 높아진다.
  2. 부모 클래스에 메소드를 추가했을 때, 자식 클래스에는 적합하지 않은 메소드를 상속할 수 있다. 
  3. 부모 클래스에 오류가 있다면 자식 클래스에도 그대로 넘어오게 된다.
  4. 자식 클래스가 부모 클래스의 메소드를 오버라이딩 할 때 접근 제한자가 변경될 수 있어서 캡슐화를 위반할 수 있다.
  5. 다양한 조합이 필요한 상황이 오면 java는 클래스 다중 상속을 허용하지 않기 때문에 조합의 수 만큼 단일 상속을 통해 새로운 클래스를 추가해야 한다.

데코레이터 패턴을 구현하기 위한 예제를 생각해보자

 

기본 햄버거에 여러가지 토핑을 추가하여 다양한 햄버거 객체를 만들어볼까 한다.

interface Burger {
    void make();
}

class HamBurger implements Burger {
    @Override
    public void make() {
    	System.out.println("햄버거를 만들었다!");
    }
}

class TomatoCheeseBurger() {
    @Override
    public void make() {
    	System.out.println("햄버거를 만들었다!");
    }
    
    @Override
    public void make() {
    	System.out.println("햄버거를 만들었다!");
        addTomato();
        addCheese();
    }
    
    public void addTomato() {
    	System.out.println("토마토 추가");
    }
    
    public void addCheese() {
    	System.out.println("치즈 추가");
    }
}

class CheeseBurger() {
    @Override
    public void make() {
    	System.out.println("햄버거를 만들었다!");
    }
    
    @Override
    public void add() {
    	System.out.println("햄버거를 만들었다!");
        addCheese();
    }
    
    public void addCheese() {
    	System.out.println("치즈 추가");
    }
}

 

 

 

데코레이터 패턴없이 구현하게 된다면 여러가지 토핑을 추가할 때마다 클래스를 계속 추가하게 된다.

  • 데코레이터 패턴 구현

따라서 토핑을 추가한 상태의 버거를 일일이 구현하는것이 아닌 각 토핑을 미리 정의해주고, new BurgerTopping(new Tomato(new Cheese())) 이런식으로 생성자를 감싸듯이 구성(Composition)하여 자유롭게 동적으로 토핑을 추가할 수 있다.

 

// 원본 객체와 장식된 객체 모두를 묶는 인터페이스
interface Burger {
    void make();
}
// 장식될 원본 객체
class HamBurger implements Burger {
    @Override
    public void make() {
    	System.out.print("햄버거를 만들었다!");
    }
}
// 장식자 추상 클래스
abstract class BurgerTopping implements Burger {
    private Burger burger;
    
    BurgerTopping(Burger burger) {this.burger = burger;}
    
    @Override
    public void make() {
    	burger.make();
    }
}

// 장식자 클래스 (토마토 토핑 추가 버거)
class Tomato extends BurgerTopping {
    Tomato(Burger burger) {super(burger);}
    
    @Override
    public void make() {
        addTomato();
        super.make();
    }
    
    public void addTomato() {
    	System.out.println("토마토 추가");
    }
}

// 장식자 클래스 (치즈 토핑 추가 버거)
class Cheese extends BurgerTopping {
    Cheese(Burger burger) {super(burger);}
    
    @Override
    public void make() {
        addCheese();
    	super.make();
    }
    
    public void addCheese() {
    	System.out.println("치즈 추가");
    }
}
public class Customer {
    public static void main(String[] args) {
        
        // 토마토 버거
        Burger tomatoBurger = new Tomato(new HamBurger());
        tomatoBurger.make();
        
        // 토마토 치즈 버거
        Burger tomatoCheeseBurger = new Tomato(new Cheese(new HamBurger()));
        tomatoCheeseBurger.make();
    }
}
토마토 추가
햄버거를 만들었다!

토마토 추가
치즈 추가
햄버거를 만들었다!

이렇게 장식자 클래스를 여러개 만들어 준 후 자유롭게 원하는 햄버거를 만들때마다 구성(Composition)을 통해 추가해 줄 수 있다. 또한, 데코레이션의 순서는 가장 밖에 wrapping 되어있는 순서로 진행된다.

 

  • 데코레이터 패턴 원리
Burger tomatoCheeseBurger = new Tomato(new Cheese(new HamBurger()));
tomatoCheeseBurger.make();

토마토 치즈 버거를 만들 때 이렇게 구성(Composition)을 하게 되면 super.make() 메소드가 각 상위 장식자의 메소드로 교체되어 결과값이 변하게 된다. 결국 tomatoCheesBurger 객체를 클래스로 나타낸다면 아래와 같이 나타낼 수 있다.

class 토마토치즈버거 extends BurgerTopping {
    @Override
    public void make() {
    	addTomato(); // Tomato 클래스로 장식
        
        addCheese(); // Cheese 클래스로 장식
        
        System.out.println("햄버거를 만들었다!"); // Burger 클래스(원본 클래스)에서 make()
    }
    
    public void addTomato() {
    	System.out.println("토마토 추가");
    }
    
    public void addCheese() {
    	System.out.println("치즈 추가");
    }
}

 

  • 데코레이터 패턴 순서

데코레이터 패턴은 구성(Composition)하는 순서를 주의해야 한다. 예를 들어 토마토와 치즈토핑을 추가한 햄버거를 만들 때 토마토를 먼저 추가하느냐, 치즈를 먼저 추가하느냐는 다른 방식으로 wrapping 해야한다.

public class CustomerEx {
    public static void main(String[] args) {
        
        // 치즈 토마토 버거
        Burger cheeseTomatoBurger = new Cheese(new Tomato(new HamBurger()));
        cheeseTomatoBurger.make();
        
        // 토마토 치즈 버거
        Burger tomatoCheeseBurger = new Tomato(new Cheese(new HamBurger()));
        tomatoCheeseBurger.make();
    }
}
치즈 추가
토마토 추가
햄버거를 만들었다!

토마토 추가
치즈 추가
햄버거를 만들었다!

 

이렇게 데코레이터 패턴은 여러가지 장식자로 기본 객체에 많은 조합을 할 수 있는 상황일 때 사용한다.