👨🏼💻 데코레이터 패턴은 기본 기능에 추가할 수 있는 기능의 종류가 많은 경우에
각 추가 기능을 Decorator 클래스로 정의한 후 필요한 Decorator객체를
조합함으로써 추가 기능의 조합을 설계하는 방식이다.
예를 들어 기본 도로 표시 기능에서
4가지 추가 기능이 있을 때 추가 기능의 모든 조합은 15가지가 된다. 데커레이터
패턴을 사용하면 개별 추가 기능에 해당하는 Decorator
클래스 4개만 구현하고
개별 추가 기능을 객체의 형태로 조합함으로써 추가 기능의 조합을 구현할 수 있다.
또한 프로그램 실행 중에도 Decorator
객체의 조합이 가능하므로 필요한 추가 기능의
조합을 동적으로 생성하는 것도 가능하다.
내비게이션 SW에서 도로를 표시하는 기능을 생각해보자. 가장 기본적인 기능은 도로를 간단한 선으로 표시하는 것이다. 그리고 도로의 차선을 표시하는 기능과 같은 추가
기능도 제공할 수 있다.
그러므로 이런 상황에서 기본 도로 표시 기능을 제공하는 RoadDisplay
클래스와 기본 도로 표시에 추가적으로 차선을 표시하는 RoadDisplayWithLane
클래스를 설계할 수 있다. 두 클래스간 관계를 다이어그램으로 표현하면 다음과 같다.
RoadDisplay
public class RoadDisplay {
public void draw() {
System.out.println("기본 도로 표시");
}
}
RoadDisplayWithLane
public class RoadDisplayWithLane extends RoadDisplay {
@Override
public void draw() {
super.draw();
drawLane();
}
private void drawLane() {
System.out.println("차선 표시");
}
}
Client
public class Client {
public static void main(String[] args) {
RoadDisplay road = new RoadDisplay();
road.draw();
RoadDisplayWithLane roadWithLane = new RoadDisplayWithLane();
roadWithLane.draw();
}
}
RoadDisplay
를 상속받아 교통량을 표시하는 클래스를 새로 정의할 수 있다.
RoadDisplayWithTraffic
public class RoadDisplayWithTraffic extends RoadDisplay {
@Override
public void draw() {
super.draw();
drawTraffic();
}
private void drawTraffic() {
System.out.println("교통량 표시");
}
}
다양한 기능의 조합을 고려해야 하는 경우 상속을 통한 기능의 확장은 각 기능별로
클래스를 추가해야 한다는 단점이 있다.
기본 기능에 추가로 차선 표시, 교통량 표시, 교차로 표시를 한다고 가정할 때 총
8가지의 조합이 도출된다.
이렇게 조합 수가 늘어나는 문제를 해결할 수 있는 설계를 하려면 각 추가 기능별로
개별적인 클래스를 설계하고 기능을 조합할 때 각 클래스의 객체 조합을 이용하면 된다.
LaneDecorator
는 RoadDisplay
객체에 대한 참조가 필요한데, 이는 LaneDecorator
클래스의 상위 클래스인 DisplayDecorator
클래스에서 Display
클래스로의 컴포지션 관계를
통해 표현되고 있다. 위 설계를 바탕으로 다음과 같이 코드를 구성할 수 있다.
Display
public abstract class Display {
public abstract void draw();
}
RoadDisplay
public class RoadDisplay extends Display {
@Override
public void draw() {
System.out.println("기본 도로 표시");
}
}
DisplayDecorator
public abstract class DisplayDecorator extends Display {
private Display decorateDisplay;
public DisplayDecorator(Display decorateDisplay) {
this.decorateDisplay = decorateDisplay;
}
@Override
public void draw() {
decorateDisplay.draw();
}
}
LaneDecorator
public class LaneDecorator extends DisplayDecorator {
public LaneDecorator(Display decorateDisplay) {
super(decorateDisplay);
}
@Override
public void draw() {
super.draw();
drawLane();
}
private void drawLane() {
System.out.println("\t차선 표시");
}
}
TrafficDecorator
public class TrafficDecorator extends DisplayDecorator {
public TrafficDecorator(Display decorateDisplay) {
super(decorateDisplay);
}
@Override
public void draw() {
super.draw();
drawTraffic();
}
private void drawTraffic() {
System.out.println("\t교통량 표시");
}
}
아래 예시는 3가지 유형의 도로 표시 객체를 생성한 Client
클래스의 코드다.
public class Client {
public static void main(String[] args) {
Display road = new RoadDisplay();
road.draw();
Display roadWithLane = new LaneDecorator(new RoadDisplay());
roadWithLane.draw();
Display roadWithTraffic = new TrafficDecorator(new RoadDisplay());
roadWithTraffic.draw();
}
}
주목할 점은 기능에 대한 접근이 모두 Display
클래스를 통해 이루어진다는 것이다. 구체 클래스에 관계 없이 Client
는 동일한 Display
클래스만을 통해 일관성
있는 방식으로 도로 정보를 표시할 수 있다.
이런 방식의 설계를 이용하면 추가 기능별로 별도의 클래스를 구현하는 대신 각 추가
기능에 해당하는 클래스의 객체를 조합해 추가 기능을 구현할 수 있다.
public class Client {
public static void main(String[] args) {
Display roadWithLaneAndTraffic =
new TrafficDecorator(new LaneDecorator(new RoadDisplay()));
roadWithLaneAndTraffic.draw();
}
}
이와 같은 설계는 추가 기능의 수가 많을수록 효과가 크다. 예를 들어 교차로를
표시하는 추가 기능을 지원하면서 기존의 다른 추가 기능과의 조합을 지원하려면 CrossingDecorator
클래스를 추가하면 된다.
CrossingDecorator
public class CrossingDecorator extends DisplayDecorator {
public CrossingDecorator(Display decorateDisplay) {
super(decorateDisplay);
}
@Override
public void draw() {
super.draw();
drawCrossing();
}
private void drawCrossing() {
System.out.println("\t교차로 표시");
}
}
Client
public class Client {
public static void main(String[] args) {
Display roadWithCrossingAndLaneAndTraffic = new LaneDecorator(
new TrafficDecorator(new CrossingDecorator(new RoadDisplay())));
roadWithCrossingAndLaneAndTraffic.draw();
}
}
당연하게도 draw()
메서드의 실행 순서는 먼저 생성된 객체의 draw()
순으로 실행된다.