데커레이터 패턴은 추가기능 클래스들의 객체 조합을 통해서 클래스 폭발을 해소하는 방식이다.
Context
지도 어플리케이션을 만들자고 해보자, 지도 어플리케이션은 기본적으로 도로를 보여주며 추가적인 기능으로 차선, 교통상황 교차로 등을 보여줄 수 있다. 일단 기본적인 기능인 도로와 차선정보 표시를 보여주는 것을 만들어보자.
RoadDisplay
이 기본적인 도로정보를 표시해주는 기능을 담당하는 클래스라면 RoadDisplayWithLane
가
RoadDisplay
를 상속받아 차선 정보 기능을 추가적으로 확장시킨다.
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("교통량 표시");
}
}
아직까지는 크게 문제가 없어보인다. 실질적인 문제는 기능들의 조합에서 나타나는데, Traffic
과 Lane
을 동시에
표시하는 기능을 만들고 싶을떈 아래와 RoadDisplayWithLaneTraffic
같은 조합 클래스가 생겨버린다.
만약 추가기능이 3개 이상이라고 아래와 같은 설계가 나온다.
3C1 + 3C2+ 3C3 = 7로 총 7개의 확장클래스가 생겼다, 위 예는 최악의 케이스로 실제 확장기능에 대한 구현 코드는 기존 확장기능클래스를 가지고 그대로 사용하기에 코드의 양 자체는 많이 증가하지 않지만, 클래스가 많아진다는 것은 애플리케이션의 크기가 커지기 때문에 결코 좋지않은 설계이다.
Solution
사실 이를 해소할 수 있는 방법은 여러가지가 있지만 추가기능에 관점에서는 Decorator 패턴이 있다.
확장기능의 관점에서 볼때 아래을 순차적으로 실행해야한다.
- 원본 기능 수행
- 확장 기능 수행
그럼 기존 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
라고 한다)를 만든다.
- 원본 기능 수행 :
draw
를 오버라이드하고super::draw
를 호출함으로써 가능하다. - 확장 기능 수행 : 별로 프라이빗 메서드
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();
}
}
원본 코드가 없음에도 잘 원본 기능을 추가적으로 확장할 수 있었다. 사실 디버거로 콜스택을 출력해보면 알겠지만 결과적으로 데커레이턴은 자신보다 상위 기능을 먼저 수행함으로 아래와 같은 패턴을 띈다.
- 확장기능2에서 확장기능1 호출
- 확장기능1에서 원본기능 호출
- 원본기능 수행
- 확장기능1 수행
- 확장기능2 수행
따라서 결과적으로 확장기능들이 잘 수행됬음을 알 수 있다. 이것을 다이어그램으로 나타내면 아래와 같다.
Summary
데커레이터 패턴은 기본 기능에 추가할 수 있는 기능의 종류가 많은 경우에 각 추가 기능을 Decorator 클래스로 정의한 후 필요한 Decorator 객체를 촣합함으로써 추가 기능의 초합을 설계하는 방식이다.
'소프트웨어 > 디자인패턴' 카테고리의 다른 글
[소프트웨어/디자인패턴] 팩토리 메서드 패턴(Factory Method Pattern) (0) | 2017.01.27 |
---|---|
[소프트웨어/디자인패턴] 템플릿 메서드 패턴(Template Method Pattern) (0) | 2017.01.26 |
[소프트웨어] 디자인 패턴에 대해서 (0) | 2017.01.23 |
[소프트웨어/디자인패턴] 옵저버 패턴(Observer Pattern) (0) | 2017.01.23 |
[소프트웨어/디자인패턴] 커맨드 패턴(Command Pattern) (2) | 2017.01.21 |