프로그래밍 Design Pattern 이해하기 - 3 데코레이터 패턴
데코레이터(팅) 패턴
스타버즈라는 커피숍은 매우 빠른 속도로 성장한 초대형 커피전문점이다. 너무 빠른 성장을 했기 때문에 음료의 종류가 다양한데, 이 음료의 종류를 모두 포괄하는 음료 주문시스템을 만들려고 한다.
처음 사업을 시작할 때 만들어진 클래스는 아래의 구조를 띈다.
- Superclass beverage는 '음료'를 나타내는 abstract 클래스며, 모든 음료수는 이를 상속받는다.
- cost()는 abstract메소드로 자식클래스가 이를 새로 정의하여 가격을 구현한다.
- 처음에는 4가지의 커피만 판매하고 있다.
하지만! 엄청나게 빠른 성장을 거두면서, 음료수의 종류가 폭발적으로 늘어가고 각 음료수에 우유, 두유, 모카, 생크림 등 다양한 종류가 생기면서 고민에 빠진다!
첨가물만 들어간다고 생각을 해도,
HouseBlend, DarkRoast, Decaf, Espresso, DecafWithSoyandMocah, DecafWithMilk, DecafWithMilkAndSoy
클래스의 관리도 불가능할뿐더러(경우의 수가 너무 많아짐), 첨가되는 생크림의 가격이 인상되면 모든 '생크림'포함 음료수의 가격을 수정해야 한다!
이런 문제점을 해결하기 위해 방법을 고민해보자!
1. super class에 첨가되는 첨가물을 선언하고 추가사항을 관리하자!
총 5개의 클래스만 있으면 된다! 그리고 우유와 두유가 들어간 Espresso를 시키면, Espresso.setMilk, Espresso.setSoy 만 해주면, 관리가 가능하다! cost()를 호출하면 상위클래스에서 override할때 hasXX를 가져와 특정 첨가물의 가격을 더한다!
하지만 생각을 해보면,
1. 첨가물 가격이 바뀌면 기존 코드를 수정해야 한다.(milkCost, soyCost)
2. 첨가물의 종류가 많아지면, 새로운 메소드를 추가해야한다.(메서드가 수백개를 써야할 수도)
3. 더블 에스프레소, 더블모카 같은 메뉴를 주문한다면?
이런 문제점을 해결하기 위해 '데코레이터 패턴'을 알아보자!
먼저, 특정음료에서 시작해 첨가물로 음료수를 장식한다!
이를테면, 다크로스트 커피에 '모카'와 '휘핑크림'을 추가하면
- DarkRoast 객체 생성
- Mocha 객체로 장식
- Whip 객체로 장식
- cost() 호출. (가격을 계산할 때 첨가물의 가격은 해당 객체들에게 위임)
조금 있다 코드에서 좀 더 이해를 해보기로 하고, 그럼 Beverage 클래스와 전체 클래스는 어떻게 생겼을까?
Beverage가 가장 기본이 되는 Component 추상 클래스이며,
커피들은 Beverage를 상속받는다.
각각의 첨가물 클래스들(데코레이터)을 만든다. 코드를 보면 이해가 쉽다.
Beverage.java
Espresso.javaCondimentDecorator.java
Mocha.java
StarBuzzCoffee.java
자바의 API 중 많은 부분이 데코레이터 패턴을 적용하고 있는데, Java I/O(입출력) 부분이 데코레이터 패턴을 적용하고 있다.
추상 구성요소 : InputStream
구상 구성요소 : FileInputStream, StringBufferInputStream, ByteArrayInputStream
추상 데코레이터 : FilterInputStream
구상 데코레이터 : PushbackInputStream, BufferedInputStream, DataInputStream, LineNumberInputStream
(참고: http://docs.oracle.com/javase/6/docs/api/java/io/PushbackInputStream.html)
Read라는 method를 가장 위에 있는 InputStream이 담당하며,
예를 들어
}
해당 파일을 그냥 읽거나, 혹은 Buffer부분을 읽으려고 할 때 위와 같이 사용이 가능하다.
데코레이팅 패턴에 대해 정리를 해보면,
- 행동을 확장할 수 있다. (BufferedInputStream은 InputStream의 read()를 사용하지만, 자신만의 buffer관련 메서드가 확장가능하다)
- 다른 객체를 인스턴트로 갖고 있기 때문에 다양하게 섞어도 유연성을 잃지 않는다.
- 데코레이터의 슈퍼클래스는 자신이 장식하고 있는 객체의 슈퍼클래스와 같다.
- 데코레이터 패턴을 너무 많이 사용하면, 자잘한 객체들이 많이 추가될 수 있고, 코드가 필요 이상으로 복잡해질 수 있다.
+ 데코레이팅 패턴을 쓰면 '상속'대신 '서브클래싱'이 가능하다고 하는데, 결국에 이 구조는 '상속'이되 확장이 가능한 상속이 아닌가? 왜 많은 자료에 '상속'이 아니라는 뉘앙스로 적혀있을까..
'상속'이란 의미가 상하구조인 것에 반면, 데코레이팅 패턴은 상하구조이되 꼭대기에 하나가 있고, 그 아래에 있는 클래스들끼리 슈퍼클래스가 같기 때문에 감싸준다는 의미로 이해를 했는데...
분명 Decorator가 Component를 '상속'받는 것은 맞으며, 이 패턴에서는 상속을 이용해서 행동을 물려받고자 함이 아니라 '형식'만 맞추는 것으로 사용되는 것이 다르다고 한다.
그렇다면, 이를 실무에서 써본 적이 어떨때일까?
흠... 뭔가 공통된 속성을 가지고 있되 확장을 해야 하는 경우!? 쉬운 예가 떠오르지가 않는다.
좋은 예제 : http://en.wikipedia.org/wiki/Decorator_pattern
Window 예제가 오히려 이해하기 쉬움. 웨딩앱에 적용한다면?
책 내용 중 'The Open-Closed Principle'에 대한 이야기가 나오는데, 조금 더 자세히 알아보자.
한국말로는 '개방-폐쇄 원칙'이라 불리며, 간단하게 한마디로 압축하면, "확장에 대해서는 열려있으며 수정에 대해 닫혀있다"로 압축이 가능하다.
자세히 알아보면, 의존적인 모듈에서 한 곳을 변경할 경우 이 것이 단계적으로 변경을 일으킬 때(상속에 상속을 받는 경우) 이는 굉장히 좋지 않은 구조이다.
예제참조 : http://lng1982.tistory.com/124
public class BootStrap {
public static void main(String[] args) {
Computer computer = new Computer();
computer.boot();
}
}
public class Computer {
private final SKeyboard sKeyboard = new SKeyboard();
public void boot() {
System.out.println("부팅 중~~");
sKeyboard.connect();
}
}
public class SKeyboard {
public void connect() {
System.out.println("S사 키보드가 연결 되었습니다.");
}
위와 같이 키보드와 컴퓨터를 만들어 판매를 했는데, 고객들이 SKeyboard가 아닌 새로운 키보드를 사서 컴퓨터에 연결하니 작동을 하지 않는다!! 위와 같은 짜여진 코드는 오로지 SKeyboard만 지원을 하기 때문이다.
아래와 같이 짜면 어떨까?
public class BootStrap {
public static void main(String[] args) {
Computer computer = new Computer();
computer.setKeyboard(new SKeyboard());
computer.boot();
}
}
public class Computer {
private Keyboard keyboard;
public void setKeyboard(Keyboard keyboard) {
this.keyboard = keyboard;
}
public void boot() {
System.out.println("부팅 중~~");
keyboard.connect();
}
}
자 위와 같이 짜여져있다면, 새로운 키보드가 연결이 되더라도 해당 키보드에서 자기 자신을 넣어주면 되고, 키보드업체들은, Keyboard 클래스들을 상속받아 각자 키보드에 맞는 코드만을 짜면 된다.
외부에서 키보드를 만들어 주입하기 때문에 하나의 키보드가 주입되는 순간 그 외의 다른 키보드들은 변경되지 않는 부분을 "변경에는 닫혀있다"가 성립한다. Keyboard를 상속받아 다양한 키보드를 만들 수 있기 때문에 "확장에는 열려있다"가 성립하게 된다.
즉, 클래스의 확장에는 열려있되, 각 클래스가 명확히 분리가 되도록 만들어야 한다는 것이 OCP이다.