본문 바로가기

소프트웨어/디자인패턴

[소프트웨어/디자인패턴] 데커레이터 패턴(Decorator Pattern)

데커레이터 패턴은 추가기능 클래스들의 객체 조합을 통해서 클래스 폭발을 해소하는 방식이다.

Context

지도 어플리케이션을 만들자고 해보자, 지도 어플리케이션은 기본적으로 도로를 보여주며 추가적인 기능으로 차선, 교통상황 교차로 등을 보여줄 수 있다. 일단 기본적인 기능인 도로와 차선정보 표시를 보여주는 것을 만들어보자.


RoadDisplay이 기본적인 도로정보를 표시해주는 기능을 담당하는 클래스라면 RoadDisplayWithLaneRoadDisplay를 상속받아 차선 정보 기능을 추가적으로 확장시킨다.

class RoadDisplay {
    public void draw() {
        System.out.println("기본 도로 표시");
    }
}

class RoadDisplayWithLane extends RoadDisplay {
    @Override
    public void draw() {
        super.draw();
        drawLane();
    }

    private void drawLane() {
        System.out.println("차선 표시");
    }
}

Problem

추가 기능의 조합

네비게이션이 업데이트되면서 점점 확장할 수 있는 기능이 많아졌다. 먼저 차선정보 뿐만아니라 교통정보도 추가적으로 표시하고 싶을 땐 기존 그대로 상속을 통한 기능 확장을 한다.

직관적으로 다이어그램으로 나타내자면


class RoadDisplay {
    public void draw() {
        System.out.println("기본 도로 표시");
    }
}

class RoadDisplayWithLane extends RoadDisplay {
    @Override
    public void draw() {
        super.draw();
        drawLane();
    }

    private void drawLane() {
        System.out.println("차선 표시");
    }
}

class RaodDisplayWithTraffic extends  RoadDisplay {
    @Override
    public void draw() {
        super.draw();
        drawTraffic();
    }
    private void drawTraffic () {
        System.out.println("교통량 표시");
    }
}

아직까지는 크게 문제가 없어보인다. 실질적인 문제는 기능들의 조합에서 나타나는데, TrafficLane을 동시에 표시하는 기능을 만들고 싶을떈 아래와 RoadDisplayWithLaneTraffic같은 조합 클래스가 생겨버린다.

만약 추가기능이 3개 이상이라고 아래와 같은 설계가 나온다.


3C1 + 3C2+ 3C3 = 7로 총 7개의 확장클래스가 생겼다, 위 예는 최악의 케이스로 실제 확장기능에 대한 구현 코드는 기존 확장기능클래스를 가지고 그대로 사용하기에 코드의 양 자체는 많이 증가하지 않지만, 클래스가 많아진다는 것은 애플리케이션의 크기가 커지기 때문에 결코 좋지않은 설계이다.

Solution

사실 이를 해소할 수 있는 방법은 여러가지가 있지만 추가기능에 관점에서는 Decorator 패턴이 있다.

확장기능의 관점에서 볼때 아래을 순차적으로 실행해야한다.

  1. 원본 기능 수행
  2. 확장 기능 수행

그럼 기존 Display클래스가 아래와 같이 설계했을 때

abstract class Display {
    public abstract void draw();
}

class RoadDisplay extends Display {
    @Override
    public void draw() {
        System.out.println("도로 표시");
    }
}

추가 기능에 대해서는 아래와 같이 일반화 시킬 수 있다.

abstract class DisplayDecorator extends Display {
    private Display displayDecorator;

    public DisplayDecorator(Display displayDecorator) {
        this.displayDecorator = displayDecorator;
    }

    @Override
    public void draw() {
        displayDecorator.draw();
    }
}

데커레이터(확장기능)는 기존 기능을 수행하기에 위해 기존 기능을 속성으로 가지고 있고 draw메서드에는 원본 기능 수행만을 책임지도록 한다. 하지만 데커레이터(확장기능) 자체는 특정한 추상화된 클래스가 아니므로 abstract클래스로 지정하여 객체를 생성할 수 없게 해야한다.

class LaneDecorator extends DisplayDecorator {
    public LaneDecorator(Display displayDecorator) {
        super(displayDecorator);
    }

    @Override
    public void draw() {
        super.draw();
        drawLane();
    }

    private void drawLane() {
        System.out.println("차선표시");
    }
}

추상 데커레이터 클래스를 상속받은 구현데커레이터(ConcreteDecorator라고 한다)를 만든다.

  1. 원본 기능 수행 : draw를 오버라이드하고 super::draw를 호출함으로써 가능하다.
  2. 확장 기능 수행 : 별로 프라이빗 메서드drawLane를 만들고 해당 메서드를 draw에서 호출한다.

데커레이터 패턴과 템플릿 메서드 패턴을 응용하면 위와같은 원래 메서드를 오버라이드하거나 별도의 추가 기능의 메서드를 따로 정의하지 않고 만들 수 있지만 여기에서 언급할 내용이 아니므로 생략한다.

기능을 확장해보자, 위의 예인 LaneDecorator와 같이 TrafficDecorator를 만들어본다.

class TrafficDecorator extends DisplayDecorator {
    public TrafficDecorator(Display displayDecorator) {
        super(displayDecorator);
    }

    @Override
    public void draw() {
        super.draw();
        drawTraffic();
    }

    private void drawTraffic() {
        System.out.println("교통량 표시");
    }
}

그리고 기능이 확장되었음에도 원본 코드의 수정이 없음을 사용을 통해 확인한다.

class Client {
    public static void main(String[] args) {
        Display road = new RoadDisplay();
        Display roadWithLane = new LaneDecorator(road);
        Display roadWithLaneTraffic = new TrafficDecorator(roadWithLane);

        roadWithLaneTraffic.draw();
    }
}

원본 코드가 없음에도 잘 원본 기능을 추가적으로 확장할 수 있었다. 사실 디버거로 콜스택을 출력해보면 알겠지만 결과적으로 데커레이턴은 자신보다 상위 기능을 먼저 수행함으로 아래와 같은 패턴을 띈다.

  1. 확장기능2에서 확장기능1 호출
  2. 확장기능1에서 원본기능 호출
  3. 원본기능 수행
  4. 확장기능1 수행
  5. 확장기능2 수행

따라서 결과적으로 확장기능들이 잘 수행됬음을 알 수 있다. 이것을 다이어그램으로 나타내면 아래와 같다.

Summary

데커레이터 패턴은 기본 기능에 추가할 수 있는 기능의 종류가 많은 경우에 각 추가 기능을 Decorator 클래스로 정의한 후 필요한 Decorator 객체를 촣합함으로써 추가 기능의 초합을 설계하는 방식이다.