언어(Language)/Java

[Java] 데코레이터 패턴(Decorator Pattern)이란? - 개념 및 예제

잇트루 2023. 11. 13. 00:13
반응형

데코레이터 패턴(Decorator Pattern)

데코레이터 패턴(Decorator Pattern)은 객체 지향 디자인 패턴 중 하나로 객체에 동적으로 기능을 추가하여 확장할 수 있는 구조 패턴이다. 이 패턴은 상속을 통해 클래스를 확장하는 대신, 객체를 감싸는 방식을 사용하여 기능을 추가하거나 변경한다. 따라서 기존 코드를 수정하지 않고도 새로운 기능을 추가하거나 수정할 수 있게 된다.

데코레이터(Decorator)라는 단어의 의미인 장식자, 꾸미는 사람으로 접근하면 데코레이터 패턴의 역할을 쉽게 이해할 수 있을 것이다.

 

데코레이터 패턴의 장단점

장점

  • 유연한 확장 : 새로운 기능을 추가하거나 기존 기능을 수정할 때 동적으로 기능을 추가하거나 변경할 수 있다.
  • 코드 재사용 : 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있기 때문에 코드 재사용이 용이하다. 기존 구성 요소를 변경하지 않고 여러 조합으로 새로운 객체를 만들 수 있다.
  • 단일 책임 원칙(SRP) 준수 : SRP 원칙을 따르며, 각 데코레이터는 특정 기능의 추가 또는 수정에만 집중한다.
  • 개방 폐쇄 원칙(OCP) 준수 : 기존 코드를 변경하지 않고도 새로운 데코레이터 클래스를 추가함으로써 기능을 쉽게 확장할 수 있다.
  • 의존성 역전 원칙(DIP) 준수 : 컴포넌트와 데코레이터가 모두 동일한 인터페이스나 추상 클래스를 공유하므로 데코레이터가 컴포넌트에 의존하지 않는다.

 

단점

  • 복잡성 증가 : 객체를 여러 층으로 감싸기 때문에 클래스의 수가 많아지고 복잡성이 증가할 수 있다.
  • 순서 의존성 : 데코레이터를 적용하는 순서에 따라 최종적으로 생성되는 객체의 동작이나 상태가 달라질 수 있다.
  • 불필요한 복사 : 모든 데코레이터는 원본 객체의 인스턴스를 가지고 있기 때문에 여러 레벨의 데코레이터를 사용할 경우 불필요한 객체 복사가 발생할 수 있다.

 

데코레이터 패턴은 기존 객체의 기능을 동적으로 추가 또는 수정할 때 유용하게 사용할 수 있는 패턴으로 상속을 통한 확장이 적절하지 않을 때 사용한다. 그러나 무분별하게 많이 사용하면 코드를 복잡하게 만들 수 있으므로 신중하게 적용해야 한다.

 

 

데코레이터 패턴의 구조

데코레이터 패턴의 구조는 다음과 같다.

  • Component : 기본 기능을 정의하는 인터페이스로 데코레이터와 구체적인 컴포넌트가 이를 구현한다. 따라서 컴포넌트는 원본 객체와 데코레이터를 묶는 역할을 한다.
  • ConcreteComponent : 기본 기능을 실제로 구현하는 구체적인 컴포넌트로 데코레이터 패턴의 시작점이 된다.
  • Decorator : 컴포넌트를 감싸는 추상 클래스나 인터페이스로 새로운 기능을 추가하거나 기존 기능을 수정하기 위한 메서드를 선언한다. 데코레이터는 Component를 멤버 변수로 가진다.
  • ConcreteDecorator : 새로운 기능이나 기존 기능을 수정하는 역할을 하는 구체적인 클래스다. 데코레이터 인터페이스를 구현한다.

 

 

데코레이터 패턴 예제

커피 주문 애플리케이션에서 아메리카노를 주문할 때 설탕 추가, 샷 추가 등의 옵션들을 선택할 수 있다고 가정해 보자.

Coffee.java

public interface Coffee {
    String getCoffee();
    int price();
}
  • Coffee 인터페이스는 주문한 커피와 가격을 나타내는 메서드를 구현하도록 한다.

 

Americano.java

public class Americano implements Coffee {

    @Override
    public String getCoffee() {
        return "아메리카노";
    }

    @Override
    public int price() {
        return 2000;
    }
}
  • Americano 클래스는 Coffee 인터페이스의 구현체로 주문한 커피는 아메리카노를 나타내며, 가격은 2000원이다.

 

데코레이터 패턴을 사용하지 않는 경우

AmericanoAddShot.java

// 아메리카노에 샷 추가
public class AmericanoAddShot implements Coffee {
    private Coffee americano;

    public AmericanoAddShot(Coffee americano) {
        this.americano = americano;
    }

    @Override
    public String getCoffee() {
        return americano.getCoffee() + " 샷추가";
    }

    @Override
    public int getPrice() {
        return americano.getPrice() + 500;
    }
}

 

AmericanoAddSugar.java

// 아메리카노에 설탕 추가
public class AmericanoAddSugar implements Coffee {
    private Coffee americano;

    public AmericanoAddSugar(Coffee americano) {
        this.americano = americano;
    }

    @Override
    public String getCoffee() {
        return americano.getCoffee() + " 설탕추가";
    }

    @Override
    public int getPrice() {
        return americano.getPrice() + 500;
    }
}
  • Coffee 인터페이스를 구현하여 샷을 추가한 아메리카노와 설탕을 추가한 아메리카노를 구현하고 있다.

 

Main.java

public class DecoratorMain {
    public static void main(String[] args) {
        Coffee americano = new Americano();

        // 아메리카노 샷 추가
        Coffee americanoAddShot = new AmericanoAddShot(americano);
        System.out.println(americanoAddShot.getCoffee());
        System.out.println(americanoAddShot.getPrice());

        // 아메리카노 샷, 설탕 추가
        Coffee americanoAddSugar = new AmericanoAddSugar(americano);
        System.out.println(americanoAddSugar.getCoffee());
        System.out.println(americanoAddSugar.getPrice());
    }
}
// 출력
아메리카노 샷추가
2500
아메리카노 설탕추가
2500
  • 아메리카노에 샷 추가 또는 설탕 추가한 클래스를 구현하고 있다.
  • 이 경우 샷 추가된 아메리카노에 설탕까지 추가하려면 새로운 클래스가 필요할 것이다.
  • 뿐만 아니라 아메리카노가 아닌 라떼인 경우 똑같이 각각의 클래스가 필요하게 된다.
  • 따라서 옵션 추가를 유연하게 하지 못한다.

 

데코레이터 패턴을 사용하는 경우

Coffee 인터페이스를 데코레이터 패턴을 통해 추가 옵션을 정의하고 이를 구현하여 동적으로 기능을 추가하거나 수정하도록 구현할 수 있다.

CoffeeDecorator.java

// Decorator
public abstract class CoffeeDecorator implements Coffee {
    private Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public String getCoffee() {
        return coffee.getCoffee();
    }

    @Override
    public int getPrice() {
        return coffee.getPrice();
    }
}
  • CoffeeDecorator는 데코레이터 역할을 하는 추상 클래스로 Coffee 인터페이스를 멤버 변수로 가진다. 이는 데코레이터가 감싸고 있는 실제 컴포넌트를 나타낸다.
  • Coffee 인터페이스의 메서드를 오버라이딩하여 coffee의 메서드를 호출한다. 이로써 CoffeeDecorator를 구현한 구현체들이 Coffee의 메서드를 호출하여 그 동작을 수정하거나 확장할 수 있게 한다.

 

다음은 데코레이터를 상속하는 ConcreteDecorator 클래스로 샷 추가와 설탕 추가 옵션에 대한 기능이다.

ShotDecorator.java

// ConcreteDecoratorA
public class ShotDecorator extends CoffeeDecorator {
    public ShotDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getCoffee() {
        return super.getCoffee() + " 샷추가";
    }

    @Override
    public int getPrice() {
        return super.getPrice() + 500;
    }
}
  • 샷을 추가하는 데코레이터로 부모 클래스의 생성자와 메서드를 호출한다.
  • getCoffee() 메서드에는 CoffeeDecorator의 getCoffee() 메서드를 호출하고 샷추가를 덧붙여서 반환한다.
  • getPrice() 메서드에는 CoffeeDecorator의 getCoffee() 메서드를 호출하고 500원을 더하여 반환한다.

 

SugarDecorator.java

// ConcreteDecoratorB
public class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getCoffee() {
        return super.getCoffee() + " 설탕추가";
    }

    @Override
    public int getPrice() {
        return super.getPrice() + 500;
    }
}
  • 설탕을 추가하는 데코레이터로 부모 클래스의 생성자와 메서드를 호출한다.
  • getCoffee() 메서드에는 CoffeeDecorator의 getCoffee() 메서드를 호출하고 설탕추가를 덧붙여서 반환한다.
  • getPrice() 메서드에는 CoffeeDecorator의 getCoffee() 메서드를 호출하고 500원을 더하여 반환한다.

 

작성한 코드는 다음과 같이 사용할 수 있다.

public class Main {
    public static void main(String[] args) {
        Coffee americano = new Americano();
        Coffee americanoShotAdd = new ShotDecorator(americano);
        Coffee americanoShotSugarAdd = new SugarDecorator(americanoShotAdd);

        // 아메리카노 샷 추가
        System.out.println(americanoShotAdd.getCoffee());
        System.out.println(americanoShotAdd.getPrice());

        // 아메리카노 샷, 설탕 추가
        System.out.println(americanoShotSugarAdd.getCoffee());
        System.out.println(americanoShotSugarAdd.getPrice());
    }
}
// 출력
아메리카노 샷추가
2500
아메리카노 샷추가 설탕추가
3000
  • 기본 아메리카노 객체를 생성하여 ShotDecorator와 SugarDecorator를 통해 샷과 설탕을 추가하는 동작을 수행할 수 있다.
  • 데코레이터 패턴을 사용하여 컴포넌트(아메리카노)에 동적으로 기능(샷, 설탕 추가)을 추가하고, 동일한 인터페이스를 사용하여 다양한 커피를 주문할 수 있도록 한다.
  • 만약 라떼와 같은 커피 종류가 추가되더라도 이미 구현된 데코레이터를 통해 똑같이 사용할 수 있게 된다.

 

 

정리

데코레이터 패턴은 기존 객체를 수정하지 않고도 새로운 기능을 추가하거나 변경할 수 있도록 하는 패턴이다. 클라이언트 코드는 데코레이터와 컴포넌트를 동일한 인터페이스로 다룰 수 있어 코드의 일관성을 유지하면서도 다양한 기능을 적용할 수 있다.

추가로 앞서 설명한 예제의 구조를 살펴보면, 단일 책임 원칙(SRP), 개방 폐쇄 원칙(OCP)과 의존성 역전 원칙(DIP)을 준수하고 있다.

반응형