ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [디자인패턴] 데코레이터 패턴 (Decorator Pattern)
    IT, 프로그래밍/Design Patterns 2017. 10. 21. 19:22

    데코레이터 패턴(Decorator pattern)이란 주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴으로, 객체에 추가적인 요건을 동적으로 첨가하며, 기능 확장이 필요할 때 서브클래싱 대신 쓸 수 있는 유연한 대안이 될 수 있다.

     

    복잡해 보이는 설명을 예시로 한 번 풀어서 표현해 보겠습니다.

    (본문에 나오는 예시가 아주 좋아서 그 주제를 가지고 설명하겠습니다.)

     

     

    이 글을 보고 계시는 대부분의 분들이 카페에 한 번쯤 가보셨다고 생각을 합니다. 카페에 가서 주문을 하는 상황을 한 번 상상해 봅시다.

     

    문을 밀고 들어가서 카운터에서 점원에게 주문을 합니다.

     

    "따뜻한 카페라떼 한 잔 주세요!",

     

    옆에서 커피가 나오는 동안 기다리는데, 다른 손님이 들어와서 주문을 합니다.

     

    "카라멜 프라푸치노에, 헤이즐럽 시럽 한 번 넣고 자바칩 넣어주세요! 아 그리고 카라멜 드리즐이랑 초코 드리즐 깔아주시구요.

     

     

    첫 번째 주문만 있을 경우에, 사실 데코레이터 패턴은 굳이 필요하지 않을 수도 있습니다. 하지만 현실은 그렇지가 않죠. 많은 고객들은 자신의 취향에 맞게 기존의 레시피에 추가를 하거나 빼서 주문을 시킵니다.

     

    두 번째 주문의 경우 카라멜 프라푸치노 + 헤이즐럿 시럽 + 자바칩 + 카라멜 드리즐 + 초코 드리즐 으로 커피 메뉴에 많은 데코레이션 재료가 추가 되었습니다.

     

    이것을 소프트웨어 적으로 바라본다면, 카라멜 프라푸치노 라는 객체에 헤이즐럿 시럽 객체, 자바칩 객체 등이 추가 되었다고 볼 수 있겠죠.

     

    여담이지만 이 레시피를 스타벅스에 가서 주문을 시키면, 악마의 음료라고 불리는 메뉴를 받을 수 있다고 합니다. (정확히는 저 레시피대로 주문하고 휘핑크림에 카라멜 드리즐과 통 자바칩을 올려 달라고 하세요)

     

     

     

    자 이제, 커피 전문점인 스타버즈 커피사에서 좀 더 효율적인 운영을 위해 음료 주문을 받는 프로그램을 만들었다고 가정 해 봅시다.

     

    기본적으로 이 프로그램을 만들때 공통적으로 가지고 있는 성질을 따로 빼서 '음료' 라는 클래스로 만들고, 이것을 상속 받아 사용하게 하였습니다.

     

     

     

    이렇게 구성을 하였으나, 위의 2번째 손님의 예시 처럼 다양하게 추가 해서 제조해 달라는 요청이 많아지면서 밑과 같이 변하게 됩니다.

     

     

    메뉴에 추가되는 재료들 때문에 객체 수가 엄청나게 많아 졌습니다. 이렇게 되면 메뉴

    가 추가 되면 추가될수록 그에 대한 객체도 기하 급수적으로 많아지고, 유지보수가 아주 힘들어 집니다.

     

    그래서 잠시 서비스를 중지하고 프로그램을 재 설계합니다.

     

     

     

    boolean 타입의 변수를 이용해 해당 재료를 첨가하면 super 클래스의 cost 메소드에서 들어있는 재료 들의 가격을 더 해줍니다.

     

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public int cost() {
            
            int total = 0;
        
            if(hasMilk()) total+=500;
            if(hasShot()) total+=400;
            if(hasCream()) total+=300;
            if(hasJavachip()) total += 700;
            
            return total;
            
            
        }
    cs

     

    차일드 클래스에서 상속 받은 클래스에서는,

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class CaffeLatte extends Beverage {
     
        @Override
        public int cost() {
            // TODO Auto-generated method stub
            return 5000+super.cost();
        }
        
    }
    cs

     

    재료들의 가격에 커피 가격을 합쳐서 최종 가격을 내게 되죠.

     

    이렇게 보았을때, 당장 사용하기는 편리 해 보일 수 있지만 지속적으로 사용하기 에는 몇 가지 큰 문제점들이 있습니다.

     

    1. 차후에 메뉴가 추가 되었을 시, 슈퍼 클래스를 고쳐야 합니다. 재료의 종류가 많아졌을 시 계속 슈퍼클래스를 수정 해야 하는 상황이 생기는 것이죠.

     

    2. 메뉴가 다양해 질 수록, Beverage 클래스의 정의가 불분명 해 집니다. 예를 들면 차(tea)를 팔게 된다면, 차에는 휘핑크림이나 샷 같은 것을 추가 해 먹지 않지만 hasWhip() 같은 필요없는 메소드를 계속 상속 받게 됩니다.

     

    3. 손님이 샷이나 휘핑 크림 등을 두 번 넣을 수 없습니다.

     

     

    디자인 원칙

     

    OCP (Open-Closed Principle) : 클래스는 확장에 대해서는 열려 있어여 하지만 코드 변경에 대해서는 닫혀 있어야 한다.

     

    즉, 기존 코드는 건드리지 말고 확장을 통해서 새로운 행동을 간단하게 추가 할 수 있도록 한다!

     

     

     

    이 프로그램을 데코레이터 패턴을 적용하여 다시 작성 해 보겠습니다.

     

    데코레이터 패턴의 기본적인 형태는 이렇습니다.

     

     

     

    데코레이터 패턴의 기본 UML 다이어그램

     

     

     

     

    데코레이터 패턴으로 설계한 스타버즈 커피 UML 다이어그램 입니다.

     

    첨가물들은 CordimentDecorator 라는 클래스를 상속 받습니다.  이 클래스는 Beverage 클래스를 확장한 것으로, 첨가물들에게 getDescription 메소드를 새롭게 정의하도록 만듭니다. 왜 이렇게 하냐면, Beverage를 상속받는 커피의 이름에 첨가물의 이름을 더 하기 위해 이렇게 합니다.

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public abstract class Beverage {
        
        
        String description = "no title"//음료 이름
     
        public abstract int cost();
     
        public String getDescription() {
            return description;
        }
        
    }
    cs

     

    Beverage class

     

    가장 상위의 슈퍼 클래스인 Beverage 클래스 입니다. 음료의 공통적인 성질을 따로 뺀 것으로 카페에서 판매하는 모든 음료는 이 클래스를 상속 받아야 합니다.

     

    1
    2
    3
    4
    5
    6
    7
    public abstract class CondimentDecorator extends Beverage {
        
        public abstract String getDescription();
        
     
    }
     
    cs

     

    CondimentDecorator class

     

    모든 첨가물들이 상속 받아야 하는 클래스 입니다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class Americano extends Beverage {
     
        public Americano() {
            super();
            description = "아메리카노";
            // TODO Auto-generated constructor stub
        }
     
        @Override
        public int cost() {
            // TODO Auto-generated method stub
            return 4000;
        }
     
    }
     
    cs

     

    Americano class

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class CaffeLatte extends Beverage {
     
        public CaffeLatte() {
            super();
            description = "카페라떼";
            // TODO Auto-generated constructor stub
        }
     
        @Override
        public int cost() {
            // TODO Auto-generated method stub
            return 5000;
        }
     
    }
     
     
    cs

     

    CaffeLatte class

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public class Cream extends CondimentDecorator {
     
        Beverage beverage;
        
            
        public Cream(Beverage beverage) {
            super();
            this.beverage = beverage;
        }
     
        @Override
        public String getDescription() {
            // TODO Auto-generated method stub
            return beverage.getDescription() + ", 크림";
        }
     
        @Override
        public int cost() {
            // TODO Auto-generated method stub
            return beverage.cost() + 500;
        }
        
        
     
    }
     
    cs

     

    Cream class

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class Shot extends CondimentDecorator {
        
        Beverage beverage;
     
        public Shot(Beverage beverage) {
            super();
            this.beverage = beverage;
        }
     
        @Override
        public String getDescription() {
            // TODO Auto-generated method stub
            return beverage.getDescription() + ", 샷";
        }
     
        @Override
        public int cost() {
            // TODO Auto-generated method stub
            return beverage.cost() + 400;
        }
        
        
     
    }
    cs

     

    Shot class

     

    첨가물 클래스로, 생성자에서 넘겨받은 Beverage의 인스턴스를 가지고, 현재 인스턴스에 구현 되어 있는 객체 (즉 음료 메뉴)의 필드와 메소드에 접근 합니다.

     

     

    지금 모든 클래스 들을 다 적지 않았지만, 다른 클래스들도 이름만 다를 뿐 똑같이 작성 되어 있습니다.

     

    그리고 주문을 받을 Customer 클래스를 작성해 봅시다.

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Customer {
     
        public static void main(String[] args) {
            
            Beverage beverage = new Americano();
            beverage = new Shot(beverage); //beverage 필드에 Amerciano 인스턴스 저장
            beverage = new Shot(beverage); //beverage 필드에 Shot 인스턴스 저장
            
            System.out.println("메뉴 : " + beverage.getDescription());
            System.out.println("가격 : " + beverage.cost());
            
            
        }
    }
     
    cs

     

    Customer 클래스

     

    누군가 샷을 두 번 추가한 아메리카노를 주문 한 상황을 표현하였습니다.

     

    실행 결과는

     

    메뉴 : 아메리카노, 샷, 샷
    가격 : 4800

     

    으로 나오게 됩니다.

     

    이 과정을 그림으로 한 번 살펴보겠습니다.

     

     

     

     

    (책의 예시를 들고 온 것이므로 가장 외부에 있는 Whip을 Shot, Mocha도 Shot, DarkRoast를 Americano 로 바꿔서 생각해 주시면 됩니다)

     

    Customer의 코드와 같이 초기화 시켰을 때, Shot-Shot-Americano 이렇게 감싸지는 형태로 객체가 생성 되게 됩니다. 첨가물들의 beverage 필드를 통해 저런 형태의 체인이 만들어 지게 되는데,

     

    이때 제일 외부의 Shot 객체의 getDescription 메소드를 실행 시키면, beverage에 저장 되어 있는 Shot 객체의 getDescription으로, 그리고 이어서 Americano 객체에 있는 getDescription 메소드가 차례로 호출 되게 됩니다.

     

    그리고 리턴은 역순으로 일어나게 되므로, Amerciano 객체에 있는 description이 차례로 리턴되면서 Shot 객체를 거쳐 문자열이 더해지며 최종 적으로는 "아메리카노, 샷, 샷" 으로 표시 되게 됩니다.

     

    cost 메소드 또한 Americano 까지 호출되어 가서, 리턴 되며 가격이 더해져서 최종적으로 더해진 가격이 나옵니다.

     

     

    이 데코레이터 패턴은 어디서 사용할까요?

     

    이미 눈치 채고 계신 분들도 있겠지만, 자바의 I/O 클래스에서 이 패턴으로 설계 되어 있습니다.

     

     

    출처 : http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html

     

    자바의 I/O 클래스는 이렇게 구성되어 있습니다. FileInputStream을 구현할 때, BufferedInputStream을 감싸서 주로 구현합니다.

     

    이때, 위에 설명한 예시와 같은 방식으로 동작 하는 것이죠. 차후에 이 부분에 대해서 한 번 포스팅 하도록 하겠습니다.

Designed by Tistory.