본문 바로가기

취업과 기본기 튼튼/빽 투더 기본기

[디자인 패턴 8편] 구조 패턴, 데코레이터(Decorator)

1. 개념

데코레이터 패턴은 기본 객체에
추가적인 기능을 동적으로 유연하게 첨가하는 패턴이다.

1.1. 장점

  • 객체에 동적으로 기능 추가가 간단하게 가능하다.

1.2. 단점

  • 자잘한 데코레이터 클래스들이 계속 추가되어 클래스가 많아질 수 있다.
  • 겹겹이 애워싸고 있기 때문에 객체의 정체를 알기 힘들고 복잡해질 수 있다.

1.3. 활용 상황

  • 객체가 상황에 따라 다양한 기능이 추가되거나 삭제되어야 할 때.

2. 구조

  • Component
    • ConcreteComponent 과 Decorator 가 구현할 인터페이스다.
      두 객체를 동등하게 다루기 위해 존재함
  • ConcreteComponent
    • Decorate 를 받을 객체다.
      즉, 기능 추가를 받을 기본 객체
  • Decorator
    • Decorate 를 할 객체의 추상 클래스다.
      즉, 기능 추가를 할 객체는 이 객체를 상속받는다.
  • ConcreteDecorator
    • Decorator 를 상속받아 구현할 다양한 기능 객체이다.
      이 기능들은 ConcreteComponent 에 추가되기 위해 만들어 진다.

3. 구현

크리스마스 트리에 다양한 장식을 하고싶다고 해보자.
기본 크리스마스 트리가 있고, 장식으로는 깜빡이는 불이나, 꽃들을 생각해볼 수 있다.

Component

먼저 크리스마스 트리와 장식을 각각 추상화 해야할 것이다.
이들을 먼저 다음과 같은 인터페이스로 공통 추상화 하자.

interface ChristmasTree {
  String decorate();
}

ConcreteComponent

먼저 기본 크리스마스 트리를 다음과 같이 구체화할 수 있다.

class DefaultChristmasTree implements ChristmasTree {
  @Override
  public String decorate() {
    return "Christmas tree";
  }
}

그렇다. 그냥 아무 장식이 없는 그냥 나무다.

Decorator

다음으로, 트리에 올라갈 장식은 다음과 같이 추상화할 수 있다.

abstract class TreeDecorator implements ChristmasTree {
  private ChristmasTree christmasTree;

  public TreeDecorator(ChristmasTree christmasTree) {
    this.christmasTree = christmasTree;
  }

  @Override
  public String decorate() {
    return christmasTree.decorate();
  }
}

구체적인 장식들, 즉 장식용 불이나 꽃은 이 추상 클래스를 상속받아 구현하면 된다.

ConcreteDecorator

먼저 장식으로 두를 불을 먼저 구체화 해보자.

class Lights extends TreeDecorator {

  public Lights(ChristmasTree christmasTree) {
    super(christmasTree); // 여기가 포인트.
  }

  public String addLights() {
    return " with Lights";
  }

  @Override
  public String decorate() {
    return super.decorate() + addLights(); // 여기가 포인트.
  }
}

왜 이렇게 구현했는지는 나중에 클라이언트 쪽 코드를 보면 알게된다.
마찬가지로 장식용 꽃도 비슷하게 구체화 하자.

class Flowers extends TreeDecorator {

  public Flowers(ChristmasTree christmasTree) {
    super(christmasTree);
  }

  public String addFlowers() {
    return " with Flowers";
  }

  @Override
  public String decorate() {
    return super.decorate() + addFlowers();
  }
}

클라이언트 단

클라이언트 단에서는 다음과 같이 코드를 작성한다.

public class DecoratorDemo {

  public static void main(String[] args) {
    // Christmas tree 만 있음.
    ChristmasTree tree = new DefaultChristmasTree();
    System.out.println(tree.decorate());

    // Christmas tree + Lights
    ChristmasTree treeWithLights = new Lights(
      new DefaultChristmasTree()
    );
    System.out.println(treeWithLights.decorate());

    // Christmas tree + Lights + Flowers
    ChristmasTree treeWithLightsAndFlowers = new Flowers(
      new Lights(
        new DefaultChristmasTree()
      )
    );
    System.out.println(treeWithLightsAndFlowers.decorate());
  }
}

실행하면 다음과 같은 결과를 볼 수 있다.

Christmas tree
Christmas tree with Lights
Christmas tree with Lights with Flowers

다시 클라이언트 코드부분을 살펴보면,
기본 객체인 new DefaultChristmasTree() 에 기능 추가를 new Lights(new DefaultChristmasTree()); 와 같이 동적인 방식으로 하고 있다.

이게 가능한 이유는

  • Decorator 객체의 생성자로 Component 를 받음으로써 Decorator 를 이어 붙일 수가 있고
  • super 를 통해 넘어오는 Component 의 operation(decorate()) 을 먼저 수행하기 때문이다.

이후 추가적인 장식(기능)을 만들고 싶으면 TreeDecorator 를 상속받아 위와 같은 꼴로 또 구현하면 된다.
즉 새로운 기능을 유연하게 만들고 추가할 수 있다.

4. 참고