Design Pattern - Decorator Pattern

겔로그·2022년 9월 14일
0

Design Pattern

목록 보기
4/7
post-thumbnail

개요

오늘은 Decorator Pattern에 대해 알아보는 시간을 가지겠다. Decorator Pattern은 Head First Design Pattern 책의 117p ~ 145p에 설명되어 있으며 책의 내용과 구글링 정보를 종합해서 정리할 예정이다.

데코레이터 패턴을 설명하기 전에, OCP에 대해 먼저 알아보자.

OCP(Open-Close Principle)

디자인 원칙 중 하나인 OCP는 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 있어서는 닫혀 있어야 한다.

Decorator Pattern이란?

  • 객체의 결합 을 통해 기능을 동적으로 유연하게 확장 할 수 있게 해주는 패턴
  • 기본 기능에 추가할 수 있는 기능의 종류가 많은 경우에 각 추가 기능을 Decorator 클래스로 정의 한 후 필요한 Decorator 객체를 조합함으로써 추가 기능의 조합을 설계 하는 방식이다.

Ex) 기본 도로 표시 기능에 차선 표시, 교통량 표시, 교차로 표시, 단속 카메라 표시의 4가지 추가 기능이 있을 때 추가 기능의 모든 조합은 15가지가 된다.

-> 데코레이터 패턴을 이용하여 필요 추가 기능의 조합을 동적으로 생성할 수 있다.

데코레이터 패턴은 OCP에 충실한 디자인 패턴이다. 해당 패턴을 이용할 경우 다음과 같은 장단점을 얻을 수 있다.
장점

  • OCP에 충실하여 기능 확장에 유연하며 코드 수정이 불필요하다.

단점

  • 클래스가 많아진다.(남들이 처음 볼 경우 이해하기가 힘든 복잡한 구조가 나온다.)
  • 구성 요소를 초기화할 때 코드가 훨씬 복잡해진다.

Decorator Pattern 적용 사례

데코레이터 패턴이 적용된 대표적인 사례로는 자바 I/O가 있다. Java I/O의 구조를 볼 경우 다음과 같이 구성되고 있다.

InputStream

OutputStream

Java I/O는 다음과 같이 객체 속에 객체를 결합하여 InputStream을 구현하였다.

문제

카페를 연 개발자 박씨는 카페 시스템을 개발해 운영하려 한다. 원만하게 운영하던 그 때 어떤 손님이 찾아와 다음과 같이 말했다.

여기 민트초코에 휘핑크림, 샷, 초콜릿 추가해주시고 텀블러에 담아주세요~
저는 아이스 아메리카노 에스프레소 샷 추가인데 로스팅 된 원두로 부탁드려요~

뭔 민트 초코를 이렇게 먹나..? 이 손님은 커잘알인가... 주문의 디테일이 엄청나다. 막상 주문을 접수하려 하니 현재 시스템으로는 주문이 불가능한 요구사항이었다.

개발자 박씨는 어떻게 구현해야만 해당 주문을 성공적으로 시스템에 등록할 수 있을까?

현재 시스템 구조는 다음과 같다.

음.. 해당 구조에서 어떻게 해야 될까?

문제 요구사항 접근

만약.. 그냥 단순히 모든 토핑을 추가한다고 하면 다음과 같은 클래스 다이어그램이 구성될 것 같다.

오우 쉣... 딱 봐도 너무나도 더럽기 때문에 단순한 방법으로는 어려울 것 같다.

가장 먼저 생각해야 될 부분은 새로운 메뉴를 주문하는 것이 아닌 기존 메뉴에 구성 요소를 추가한다는 것이다. 현재 음료인 Beverage 클래스에 토핑들을 추가하는 방법이니 단순히 객체를 만들어서 구현하기에는 다소 어려움이 존재한다.

또한, 앞으로 신 메뉴가 나오거나 새로운 토핑이 생길 경우 기존 시스템에 영향을 주지 않고 구현이 가능해야 된다.

필수 고려사항

  • 신 메뉴, 토핑이 생겼을 때 기존 시스템 코드는 영향을 받지 않아야 한다.
  • 토핑이 추가될 경우, 아무런 영향없이 추가되어야 한다.

해결 방법

앞서 배웠던 데코레이터 패턴은 어느 상황에서 사용하는지 상기해보자.

객체의 결합 을 통해 기능을 동적으로 유연하게 확장 할 수 있게 해주는 패턴

따라서 객체의 결합에 대해 생각하며 진행해보자. 데코레이터 패턴을 적용할 경우 기존 클래스는 음료일 것이고 데코레이터 패턴에 들어갈 내용은 토핑일 것이다.

객체의 결합을 중점적으로 볼 때 유동적으로 추가되는 토핑은 객체의 결합의 주체일 것이기에 토핑이 데코레이터 클래스에 추가될 것이다.

다음을 그림으로 표현해보자.

데코레이터 클래스 내부에는 다음과 같이 토핑이 들어갈 예정이며, 추가될 경우에는 계속해서 원 내부로 새로운 토핑 클래스가 등록될 것이다.

그럼 박씨의 개인카페의 시스템 클래스 다이어그램을 다시 한 번 생각해보자.

Decorator 클래스를 하나 생성한 이후 각 토핑들이 해당 데코레이터 클래스를 상속받도록 설계하였다.
메인 클래스인 음료같은 경우 Beverage 클래스를 상속받으며, Decorator 클래스를 추가로 포함시켜 토핑을 추가할 수 있게 되었다.

구조만 봐서는 이해가 안될수도 있으니 코드를 작성해보자.

코드 구현

Beverage.java

public abstract class Beverage {
    private String description;

    public String getDescription() {
        description = "Beverage : ";
        return description;
    }

    public Integer cost() {
        return 0;
    }
}

Decorator.java

public class Decorator extends Beverage{
    @Override
    public String getDescription(){
        return "Add Topping: ";
    }
}

음료들

	public class Americano extends Beverage {
    @Override
    public Integer cost() {
        return super.cost() + 2500;
    }

    @Override
    public String getDescription(){
        return super.getDescription() + "Americano ";
    }
}

public class Latte extends Beverage {
    @Override
    public Integer cost() {
        return super.cost() + 3000;
    }
    @Override
    public String getDescription(){
        return super.getDescription() + "Latte ";
    }
}

public class MintChoco extends Beverage {
    @Override
    public Integer cost() {
        return super.cost() + 3500;
    }

    @Override
    public String getDescription(){
        return super.getDescription() + "Mint Choco ";
    }
}

public class Smoothie extends Beverage {
    @Override
    public Integer cost() {
        return super.cost() + 4000;
    }

    @Override
    public String getDescription(){
        return super.getDescription() + "Smoothie ";
    }
}

토핑들

public class Choco extends Decorator {
    private Beverage beverage;

    public Choco(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public Integer cost() {
        return beverage.cost() + 500;
    }
    @Override
    public String getDescription(){
        return beverage.getDescription() + super.getDescription() + "Choco ";
    }
}

public class Milk extends Decorator {
    private Beverage beverage;

    public Milk(Beverage beverage) {
        this.beverage = beverage;
    }
    @Override
    public Integer cost() {
        return beverage.cost() + 500;
    }

    @Override
    public String getDescription(){
        return beverage.getDescription() + super.getDescription() + "Milk ";
    }
}

public class Shot extends Decorator {
    private Beverage beverage;

    public Shot(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public Integer cost() {
        return beverage.cost() + 500;
    }
    @Override
    public String getDescription(){
        return beverage.getDescription() + super.getDescription() + "Shot ";
    }
}

Main.java

public class main {
    public static void main(String[] args){
        Beverage americano = new Americano();
        System.out.println(americano .getDescription() + "cost is : " + americano.cost());
        americano = new Shot(americano);
        System.out.println(americano .getDescription() + "cost is : " + americano.cost() + "\n");


        Beverage mintChoco = new MintChoco();
        System.out.println(mintChoco .getDescription() + "cost is : " + mintChoco.cost());
        mintChoco = new Choco(mintChoco);
        System.out.println(mintChoco .getDescription() + "cost is : " + mintChoco.cost());
        mintChoco = new Milk(mintChoco);
        System.out.println(mintChoco .getDescription() + "cost is : " + mintChoco.cost()+ "\n");


        Beverage latte = new Latte();
        System.out.println(latte.getDescription() + "cost is : " + latte.cost());
        latte = new Milk(latte);
        System.out.println(latte.getDescription() + "cost is : " + latte.cost());

    }
}

결과

여기서 잠깐!

getDescription() 메소드를 한 번 자세히 들여다 보길 바란다. 앞서 클래스 다이어그램을 다시 봐보자. 만약 Decorator Class에서 getDescription(); 메소드에 super.getDescription()을 추가했다면 어떤 답이 나올까?

public class Decorator extends Beverage{
    @Override
    public String getDescription(){
        return super.getDescription() + "Add Topping: ";
    }
}

한 번 고민해보기를 바란다. 힌트는 다음과 같다.

모든 놈들이 결국 Beverage를 상속받고 있으니 흠... 한 번 머릿 속에서 출력을 그려보는 것도 좋을 것 같다.

결론

오늘은 데코레이터 패턴을 배워보았다. 막상 main 코드를 구현하려고 하니 무엇을 호출해야 되는지 잠시 애좀 먹었고, 출력이 생각대로 되지 않아서 잠깐 헤메었지만 확실히 어떤 기능을 추가할 경우에 기존 코드를 수정하지 않아도 될 것 같다는 생각이 드는 구조였다.

단점은 너무 많은 클래스를 생성해야 되다 보니 관리 차원에서 생각보다 까다로울수도 있을 것 같다는 생각이 들었고, 한층 더 복잡해지면 어떤 구조로 되어있는지 이해하기가 굉장히 어려울 것 같다.

하지만 확장성을 고려해야되는 상황이면 데코레이터 패턴은 항상 고려해볼만한 패턴이라 생각한다.

profile
Gelog 나쁜 것만 드려요~

0개의 댓글