👀 학습 목표
- 데코레이터 패턴을 이해한다.
- 데코레이터 패턴과 관련된 디자인 원칙을 학습한다.
1. 데코레이터 패턴이란?
1-1. 데코레이터 패턴이란?
- 기본 객체(Component) 에 추가적인 요소(Decorator)를 동적으로 첨가하는 것을 말한다. 여기서 데코레이터는 기본 객체(Component)의 서브 클래스로 만들어 유연하게 확장 가능하다.
예를 들어 펭수에게 추운 겨울을 지내기 위해 옷을 입힌다고 해보자. 그럼 펭수는 여기서 기본 객체인 Componet라고 할 수 있고, 추가적인 요소 옷들은 Decorator라고 할 수 있다.
데코레이터 패턴 다이어그램 이해
다이어그램을 보면서 각 기능에 대해 이해해보자
- Component
- 추상 클래스 혹은 인터페이스로 구현
- 기본 요소인 ConcreateComponentA와 추가적 요소인 Decorator로 구성되어 있다.
- ConcreateComponenetA
- 기본적인 요소로 다음 예제에서는 기본 음료인 에스프레소 등이 이에 해당된다.
- Decorator
- 추상 클래스 혹은 인터페이스로 구현
- 추가적 요소로 실제 구성은 자식 클래스에서 구현한다.
- 다음 예제에서는 커피의 추가 요소인 우유, 휘핑크림 등이 이에 해당된다.
2. 데코레이터 패턴 이해
커피 전문점을 운영하는 CEO라고 생각해보자. 다양한 음료를 팔기 위해 다양한 음료를 모두 포괄하는 주문 시스템을 만들어야 한다.
2-1. 스타버즈 커피 만들기 ☕️
- 처음에는 아래와 같이 간단히 추상 클래스 음료를 만들고, 실제 커피는 “음료”를 상속 받는 것으로 구성하였습니다.
- 문제점 😹
2-2. 문제 해결된 커피 주문 시스템 만들기 1
- 그러면 “음료”라는 추상 클래스에 각 음료에 우유, 두유, 모카, 휘핑 크림이 들어가는지 여부를 나타내는 메소드를 추가하여 비용을 알 수 있도록 합시다.
- 문제점 😹
2-3. 문제 해결된 커피 주문 시스템 만들기 2
- 데코레이터 패턴을 적용하기 전에 OCP 디자인 원칙에 대해 배워 봅시다.
- OCP(Open-Closed Principle)
아래 원칙을 보면 위에서 작성한 코드는 반대로 설계 했다는 것을 알 수 있죠? 확장(새로운 음료 추가)는 어려웠고, 코드 변경은 계속 했어야 합니다!
클래스는 확장에 대해서는 열러 있어야한다. 반면, 코드 변경에는 닫혀 있어야한다.
- 데코레이터 패턴 적용하기 전에…
위에서 설명했던 데코레이터 패턴 입니다. 그럼 커피 시스템에 어떤 식으로 적용해야 할지 고민해 봅시다.
- Component
- 추상 클래스 혹은 인터페이스로 구현
- 기본 요소인 ConcreateComponentA와 추가적 요소인 Decorator로 구성되어 있다.
- ConcreateComponenetA
- 추상 클래스 혹은 인터페이스로 구현
- 기본적인 요소로 이 예제에서는 기본 음료인 에스프레소 등이 이에 해당된다.
- Decorator
- 추가적 요소로 실제 구성은 자식 클래스에서 구현한다.
- 이 예제에서는 커피의 추가 요소인 우유, 휘핑크림 등이 이에 해당된다.
- ConcreateDecoratorA
- 자신이 장식하고 있는 객체에게 행동을 위임하는 것 외에 원하는 추가적인 작업을 수행하도록 만들 수 있다.
- 데코레이터 패턴 적용하기
- Beverage
- 데코레이터와 기본 요소가 이를 상속한다.
- 여기서 추상 클래스로 구현한 이유는 단지, 처음에 구현 시, 추상 클래스로 구현되서 인터페이스로 구현 안한 것 뿐이다. 이는 인터페이스로 구현해도 된다.
- CondimemtDecorator
- 데코레이터로 Mocha, Whip 같은 커피의 첨가물들이 이를 상속한다.
- Decaf, HoseBlend, Espresso 등
- 이는 Component 들로 기본 요소를 나타낸다.
- 적용 코드
- Class 정의
- main
beverage2 객체를 잘 확인해보자.
- “디카페인” 인 기본 요소를 생성 한다. > Mocha객체가 “디카페인” 객체를 감싼다. > Mocha객체가 “모카가 들어간 디카페인” 객체를 감싼다. > Whip 객체가 “더블모카가 들어간 디카페인” 객체를 감싼다.
-
아래 사진은 beverage2 객체를 디버깅한 결과이다. 해당 객체를 보면 Whip이 모카를 감싸고, 모카는 모카를 감싸고, 모카는 디카페인 커피 객체를 감싼 것을 확인할 수 있다.
-
beverage2.getDescription(), beverage2.cost()
- 이 두 메소드를 호출 순서를 살펴보자. 두 메소드가 비슷하기에 cost() 메소드만 확인하자. beverage2는 현재 Whip 객체로 되어있고, Whip 객체의 cost()메소드는 아래 코드와 같이 구성되어 있다. 여기서 beverage는 whip이 감싼 Mocha 객체이다. 이 객체의 cost()는 return 0.2 + beverage.cost(); 를 리턴하는 메소드이다. 그럼 또 모카가 감싼 모카 cost() 메소드를 호출한다. 그리고 마지막으로 모카는 Decaf 커피 객체를 감싸서 return 1.05; 를 리턴한다.
- 정리하자면, 아래와 같다.
> 저는 이는 마치 재귀함수 호출과 비슷하다고 생각하였다.
![](/assets/img/design_pattern/decorator/Untitled-fed515c4-0e85-482f-9c1a-4ba55b5a5b5c.png)
3. 데코레이터 패턴 적용 예
자바 i/o에 파일에서 데이터를 읽어오는 기능이 데코레이터 패턴이다. 여기서는 데코레이터가 행동 위임뿐만 아니라 기능 추가 작업도 한다. 예를 들어 LineNumberInputStream 은 데이터를 읽을 때, 행 번호를 붙어주는 기능을 추가해주는 역할을 합니다.
- 확인 가능
- https://docs.oracle.com/javase/7/docs/api/
-
InputStream 클래스 참고
- LowerCaseInputStream 생성
대문자를 소문자로 변경하는 데코레이터 추가
- main
- in2.read() 호출 시, 1. LowerCaseInputStream.read() 호출 2. LowerCaseInputStream 안에서 BufferedInputStream.read() 호출 3. BufferedInputStream 안에서 FileInputStream.read() 호출하고 결과 리턴 ….
참고
- Head first design patterns 책