[Design Pattern] 데코레이터 패턴(Decorator Pattern)

2021. 2. 5. 22:39개발/디자인패턴

Head First Design Patterns를 읽고 학습한 내용을 기록합니다.

 

정의


 

데코레이터 패턴에서는 객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공한다.

 

예제


 커피 주문 프로그램을 만든다고 생각해보자. 당연히 커피의 종류가 정의 되어야 할것이다. 

일반적으로 아래처럼 만들수 있을것이다. Beverage는 추상클래스이며 Espresso , DarkRoast등 서브클래스가 확장해서 사용하게 된다. 

위 구조에서 커피에 두유와 모카 토핑을 하려면 어떻게 해야할까? 

 첫번째 방법으로는 아래처럼 커피와 토핑이 조합가능한 모든 경우를 서브 클래스로 정의하는것이다.  디카페인 커피(Decaf)에 두유,모카를 토핑하는 경우를 정의했는데 벌써부터 서브클래스가 순식간에 3개가 늘어났다. 여기다 또다른 토핑이 추가된다면 더 많은 서브클래스가 생길것이다. 

조합가능한 경우의 수를 모두 서브클래스로 정의한다

 

두번째 방법은 Beverage 클래스에 토핑에 대한 Boolean 변수를 추가하는 것이다. Decaf를 주문했을때 mocha를 추가했다면 mocha = true로 셋팅하는 방식이다. 이 방법에서 단점은 우유같은 새로운 토핑이 추가 될때 기존의 Beverage 코드를 수정해야한다는 점이다. 또한, 새로운 커피가 출시 되었는데 토핑을 올릴수 없는 경우라면 불필요한 토핑 정보를 상속받게 된다. [ 책에선 모카를 두번 올리는 경우도 문제가 될 수도 있다고 한다. 하지만 mocha 자료형을 int로 선언해서 관리하면 큰 불편함 없이 해결 가능해 보인다. 이는 구조보다는 자료형의 문제인거같다. ]

 

 

두번째 해결법이 그럴듯해 보이긴 하지만 디자인 원칙중 하나인 OCP(Open-Close Principle)에 위배된다. 

OCP는 클래스의 확장에 대해 열려 있어야 하지만, 코드 변경에 대해서는 닫혀있어야한다.

이말이 무슨 뜻인고하니... 

두번째 해결법에서 우유와 같은 새로운 토핑이 추가 되었을때 Beverage 클래스를 변경해야했다. 이는 확장에 대해서 코드변경이 발생한것이다.

만약 OCP를 철저하게 지킨 구조였다면 새로운 토핑이 추가되었을때 Beverage 클래스의 소스코드 변경없이(코드변경에 대해서 닫혀있음) 토핑 추가가 가능(클래스 확장에 대해 열려있음)해야한다.

 

 

세번째 방법이 이 OCP구조를 지키는 데코레이터 패턴이다. 

 

아래와 같이 커피와 토핑을 분리하는 구조를 택했다. 

일단 커피는 Beverage를 그대로 상속받는다. 토핑은 CondimentDecorator(재료 데코레이터) 클래스를 상속받는다. 단 CondimentDecorator는 Beverage 클래스를 상속받는다. [ 처음에 CondimentDecorator 클래스의 존재 이유에 대해서  이해가 안가서 막혔는데 마지막에 구현하면서 알게됐다. 추후에 소스코드를 보면서 같이 설명하겠다. ]

 

그럼 커피와 토핑이 추가되었을떄 위 구조로 어떻게 해서 OCP원칙을 지킬 수 있는지 보자.

 

먼저 Beverage 추상 클래스를 생성한다. Beverage는 커피이름을 담을 수 있는 description 변수와 가격을 의미하는 cost 추상메소드가 있다. [ 추상메소드가 하나라도 존재하면 클래스는 추상 클래스로 선언해야하며, 추상 클래스를 상속받는 클래스는 추상클래스내 추상 메소드를 반드시 구현해야한다. 즉, Beverage를 상속받는 클래스는 반드시 cost메소드를 구현해야한다.]

 

 

디카페인 커피(Decaf) 클래스를 생성하고 Beverage를 상속받는다. Beverage에서 cost를 추상메소드로 선언했기때문에 Decaf는 반드시 cost 메소드를 구현해야한다. [ 디카페인 커피의 가격은 1.05 달러 ]

 

 재료 데코레이터 클래스 ( CondimentDecorator )를 생성한다. CondimentDecorator 클래스는 Beverage로 부터 cost를 상속받고 구현한다. 또한 서브클래스들이 반드시 getDescription() 메소드를 구현하도록 하기위해 getDescription()을 추상 메소드로 정의한다. [ CondimentDecorator를 만든 이유는 해당 클래스를 상속하는 서브 클래스는 반드시 getDescription() 메소드를 구현하도록 하기 위함. ]

 

 

 토핑 중 하나인 두유(Soy) 클래스를 생성한다. Beverage의 객체를 하나 생성한다. 토핑을 얹을 커피를 입력으로 받기위해서다.  [ 전체적인 구조가 커피를 가만히 두고 그위에 토핑을 하는게 아니라, 커피가 이리저리 움직이면서 토핑을 찾아다닌다고 생각하면 이해가 쉬웠다. ] 

그리고 추상클래스로부터 상속받은 메소드인 cost()와 getDescription()을 구현한다. 입력으로 받은 커피에 두유(Soy) 토핑에 대한 정보를 추가해서 return 할 수 있도록 구현했다.

 

하나만 하면 재미없으니 스팀밀크 토핑을 하나 더 만들어본다. 

 

 

 

이제 커피에 토핑을 얹어보자. 커피는 바쁘게 토핑을 찾아다닌다. 

 

먼저 decaf라는 이름으로 Beverage 객체를 하나 선언한다. [ 7번 Line ] 

Decaf 클래스를 메모리에 올리고( 인스턴스화 ) decaf에 레퍼런스를 저장한다. [ 8번 Line ]

decaf를 Soy클래스의 입력으로 사용하며 Soy 클래스를 인스턴스화 한다. 레퍼런스는 decaf 변수에 저장.[ 9번라인 ]

decaf를 토핑에 넘기면서 그 결과를 다시 decaf에 넣는방식.

 

소스코드를 실행하면 아래와 같이 나온다.

 

 

 위 소스코드의 흐름을 그림으로 그려봤다. [ 사실 출력이 왜 이렇게 나오는지 헷갈려서 그림을 그려가면서 이해했다 ]

 

 그림의 각 번호별로 getDescription() 메소드를 호출하면 아래와 같다. 1번의 상태를 2번으로 넘겨주고 2번의 상태를 3번으로 넘겨주는 방식을 택하면서 토핑을 붙여나간다. 

 

 

 

 

데코레이터 패턴을 학습하면서 단점이 떠올랐는데 책에도 역시나 거론되어 있었다. 

 결과를 통한 역추적이 어려워 보이는데 구현한 소스코드의 결과를 보면 "디카페인 커피, 두유, 두유, 스팀밀크" 이런식으로 출력된다. 만약 "디카페인 커피, 두유2, 스팀밀크" 로 출력을 하려면 여러 단계의 데코레이터를 역추적해야하는 일이 발생한다. 전혀 불가능한건 아니지만 데코레이터 패턴이 만들어진 의도와 어긋난다라고 쓰여있다. [ 데코레이터 패턴은 클래스의 유연한 확장에만 초점을 맞춘 디자인이라 그런게 아닐까 하는 생각이 든다. ]