[Design Pattern] Decorator

Yeonjae Im·2021년 8월 17일
0

디자인 패턴

목록 보기
2/2

의미

  • 특정 클래스의 객체들이 할 수 있는 일들을 여러가지 두고, 각 객체마다 사용자가 원하는대로 골라 시키거나 기능들을 필요에 따라 장착하게 할 때 사용 (참고: https://www.youtube.com/watch?v=q3_WXP9pPUQ)

문제 상황

알림 관련 라이브러리를 생각해보자. 처음에는 알림 클래스가 간단하게 구성되어 있을 것이다. 해당 알림 클래스는 중요한 이벤트가 발생할 때마다 사용자에게 알림을 보내는 역할을 한다.

하지만 나중에 facebook이나 slack 같은 어플리케이션에 알림을 보내려고 하면, 추가로 다른 알림 클래스를 생성하여, 기존 클래스를 상속받아야 한다.

그리고 한 번에 여러 어플리케이션에 알림을 보내려고 하면 각 알림 클래스들을 결합시킨 클래스를 새로 생성해야한다.

해결 방안

  1. 상속 대신 집합(Aggregation)이나 합성(Composition)을 이용하기
  • 상속의 단점
    - static해서 runtime에 코드 수정을 할 수 없다.
    - 여러 클래스를 동시에 상속받을 수 없다.
    cf) 집합 -> 합성 -> 상속 순으로 갈 수록 클래스 간의 결합도가 커진다.
  1. Wrapper 클래스 사용
  • Wrapper 클래스란?
    • 기존 객체를 감싸고, 새로운 기능을 추가시킬 수 있는 클래스
    • 밑의 그림에서는 BaseDecorator가 Wrapper 클래스

구조

예시 및 구현 방법

  • 커피에 여러가지 토핑을 추가시키는 코드를 예시로 작성해보자.
  1. 어떤 메소드가 공통된 행위(예: 토핑 추가)를 가지는지 식별한 후, 해당 메소드를 Component 인터페이스로 선언한다.
// Component
public interface Coffee {
    void addTopping();
}
  1. ConcreteComponent 클래스(Americano)를 생성하고, Component 인터페이스(Coffee)를 상속받아 공통된 행위를 클래스에 맞게 구현한다.
    • ConcreteComponent: wrapped 될 객체의 클래스
// Concrete Component
public class Americano implements Coffee {
    @Override
    public void addTopping() {
        System.out.println("아메리카노 추가");
    }
}
  1. BaseDecorator 클래스(CoffeeDecorator)를 생성하고, wrapped된 객체의 참조를 저장하는 private 필드(보통 wrappee라 함)를 생성한다. 해당 필드는 ConcreteComponent와 연결될 수 있도록 Component 인터페이스 타입으로 생성되어야 한다. 그 후, Component 인터페이스의 메소드를 모두 구현한다.
// Base Decorator
public class CoffeeDecorator implements Coffee {
    private final Coffee wrappee;

    CoffeeDecorator(Coffee coffee) {
        this.wrappee = coffee;
    }

    @Override
    public void addTopping() {
        wrappee.addTopping();
    }
}
  1. BaseDecorator 클래스를 상속받는 ConcreteDecorator 클래스(JavaChipDecorator, CoffeeDecorator, MilkDecorator)를 생성한다.
// Concrete Decorator
public class JavaChipDecorator extends CoffeeDecorator{
    public JavaChipDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public void addTopping() {
        super.addTopping();
        System.out.println("자바칩 추가");
    }
}

public class MilkDecorator extends CoffeeDecorator{
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public void addTopping() {
        super.addTopping();
        System.out.println("우유 추가");
    }
}

public class WhippingCreamDecorator extends CoffeeDecorator{
    public WhippingCreamDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public void addTopping() {
        super.addTopping();
        System.out.println("휘핑크림 추가");
    }
}
  1. 객체 생성하는 코드를 덧 씌우는 방식으로 Client 코드를 작성한다.
public class Client {
    public static void main(String[] args) {
        // 아메리카노 추가
        new Americano().addTopping();
		
        // 아메리카노 추가
        // 우유 추가
        new MilkDecorator(
                new Americano()
        ).addTopping();
	
    	// 아메리카노 추가
        // 우유 추가
        // 자바칩 추가
        // 휘핑크림 추가
        new WhippingCreamDecorator(
                new JavaChipDecorator(
                        new MilkDecorator(
                                new Americano()
                        )
                )
        ).addTopping();
    }
}

장단점

  • 장점
    • 새로운 자식클래스를 만들지 않고도 객체의 행위를 상속시킬 수 있다.
    • runtime에 객체의 책임을 더하거나 제거할 수 있다.
      • 상속말고 집합이나 합성을 써서 얻게 된 이점
    • 다수의 decorator로 객체를 감싸서 여러 행위를 조합할 수 있다.
    • SRP
  • 단점
    • 자잘한 객체들이 많이 생길 수 있다.

참고

0개의 댓글