본문 바로가기
Java

java - 데코레이터 패턴(구조 패턴)

by sinabeuro 2021. 8. 23.
728x90

데코레이터 패턴은 기존 뼈대(클래스)는 유지하되, 이후 필요한 형태로 꾸밀 때 사용합니다. 

예를 들어 서로 다른 인터페이스를 지닌 2개의 객체를 묶어 단일 인터페이스를 제공하거나 객체들을 서로 묶어 새로운 기능을 제공하는 패턴입니다.

확장이 필요한 경우 상속의 대안으로도 활용하며, SOLID 중에서 개방폐쇄 원칙(OCP)과 의존역전원칙(DIP)를 따릅니다.

 

데코레이션 패턴 구조

 

Component
- ConcreteComponent 과 Decorator 가 구현할 인터페이스이며, 두 클래스의 공통 기능을 담고 있습니다.
- 사용자가 데코레이터 패턴은 사용해서 인스트턴스를 생성할 때 Component(인터페이스) 를 사용합니다.

ConcreteComponent
- 기본 기능을 구현할 클래스입니다.
- Decorator를 통해 기능을 확장할 수 있는 뼈대 클래스가 됩니다.
- 해당 클래스의 변경 없이 Decorator 클래스를 통해서 기능이 확장되는 것입니다.

Decorator
- Decorate 할 추상 클래스입니다. 
- 해당 Decorator 클래스로 직접적으로 인스턴스를 생성하지 않으며, 다른 하위 객체에 상속되어 ConcreteComponent의 기능을 전달, 확장합니다.
- ConcreteComponent 클래스와 동등한 관계를 가지며, ConcreteComponent 를 인자로 사용하여 ConcreteComponent 의 변형없이 제 3의 클래스(ConcreteDecorator)에 ConcreteComponent 기능을 확장해서 사용합니다.

ConcreteDecorator
- Decorator 추상 클래스를 상속받아 ConcreteComponent의 기능을 확장하여 구현합니다.
- 데코레이터 패턴의 결과물이라고 할 수 있습니다.

 

 

 

데코레이터 패턴 구현 예시 코드

 

// ICar.java
public interface ICar {
    int getPrice();
    void showPrice();
}

우선 Component 에 해당 되는 인터페이스를 생성합니다.

위의 인터페이스 ICar는 데코레이터 패턴에서, 기존의 핵심 로직인 ConcreteComponent 의 인스턴스와 데코레이터 결과물인 ConcreteDecorator 의 인스턴스를 생성하는데 사용합니다.

 

 

// Audi.java
public class Audi implements ICar {

    private int price;

    public Audi(int price) {
        this.price = price;
    }

    @Override
    public int getPrice() {
        return this.price;
    }

    @Override
    public void showPrice() {
        System.out.println("audi의 가격은 "+this.price+"원 입니다.");
    }
}

우선 ICar 인터페이스를 상속받는 Audi 클래스(ConcreteComponent)를 생성합니다.

Audi 클래스의 변형없이 데코레이터 패턴을 활용하여 확장을 합니다.

확장을 위한 데코레이터 클래스는 아래의 AudiDecorator 클래스입니다.

 

// AudiDecorator.java
public class AudiDecorator implements ICar {

    protected ICar audi;
    protected String modelName;
    protected int modelPrice;

    public AudiDecorator(ICar audi, String modelName, int modelPrice) {
        this.audi = audi;
        this.modelName = modelName;
        this.modelPrice = modelPrice;
    }

    @Override
    public int getPrice() {
        return audi.getPrice() + modelPrice;
    }

    @Override
    public void showPrice() {
        System.out.println(modelName + "의 가격은 "+getPrice()+"원 입니다.");
    }
}

Audi 클래스의 부모 인터페이스인 ICar를 상속받아 AudiDecorator 클래스를 만들고 ICar를 인자로 받아 생성자를 만듭니다.

ICar 인터페이스를 상속받아 메소드를 오버라이드합니다. 

이때 오버라이드를 통해 기능이 확장되는 것입니다.

 

// A3.java
public class A3 extends AudiDecorator {
    public A3(ICar audi, String modelName) {
        super(audi, modelName, 1000);
    }
}

 

// A4.java
public class A4 extends AudiDecorator {
    public A4(ICar audi, String modelName) {
        super(audi, modelName, 2000);
    }
}

확장을 구현할 클래스를 만들고 AudiDecorator 클래스를 상속하여 생성자를 만듭니다.

 

 

public class Main {

    public static void main(String[] args) {

        ICar audi = new Audi(1000);
        audi.showPrice();

        ICar audi3 = new A3(audi, "A3");
        audi3.showPrice();
        ICar audi4 = new A4(audi, "A4");
        audi4.showPrice();
    }
}

즉, A3, A4 클래스 인스턴스를 생성하게 되면 Audi(ConcreteComponent)의 기능을 확장해서 사용할 수 있게 되는 것입니다.

여기까지가 데코레이션 패턴에 해당되는 예시였습니다. 

그럼 데코레이션 패턴을 통해 A3, A4 클래스를 만드는 것과 일반 상속을 통해 A3, A4 클래스를 만드는 것이 무슨 차이가 가 있을까요?

 

상속을 통한 기능의 확장은 각 기능별로 클래스를 추가해야 합니다.

반면, 데코레이션 패턴에 의한 기능의 확장은 데코레이션 클래스를 통해 A3, A4 클래스를 일괄적으로 확장할 수 있습니다.

즉, 기능 확장으로 개별 클래스를 여러 개를 만들어야할 상황에서 데코레이션 패턴을 이용하는 것이 코드 절약을 할 수 있습니다.

 

728x90

댓글