자바 I/O에 대해 공부하다가 I/O 패키지의 많은 부분들이 데코레이터 패턴을 이용하여 만들어졌다는 것을 알았다.
그래서 데코레이터 패턴이 뭐지? 하는 궁금증에 데코레이터 패턴에 대해서도 조금 공부해봤다.
데코레이터 패턴
객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는 것을 통해서 기능을
유연하게 확장할 수 있는 방법을 제공한다.
당연히 정의만 봐서 무슨 소리인지 이해를 못하겠다.
예제를 보며 차분히 이해해보자.
스타버즈라는 카페가 있다. 스타버즈는 엄청난 급속도로 성장해서 다양한 음료들을 포괄하는 주문시스템을
이제서야 겨우 갖추려고 준비중이다.
처음 사업 시작 시 클래스들은 다음과 같이 구성되어 있었다.
커피를 주문할 때 스팀 우유, 두유, 모카(초코), 휘핑과 같은 토핑을 변경할 수 있는데 이런 경우
기존 구성을 어떻게 변경해야 할까?
처음 스타버즈는 이렇게 해보기로 했다.
와 뭔가 잘 될 것 같다! 라고 생각할 수 있지만 이 구조에는 몇 가지의 문제점이 있을 수 있다.
이런 문제점으로 인해 바로 위의 구조인 상속을 써서 음료 가격과 토핑 가격을 합한 총 가격을 계산하는 방법은
그리 좋은 방법이 아니다.
스타버즈는 다음 대안으로 다음과 같이 생각해본다. 우선 특정 음료에서 시작해서, 토핑으로 그 음료를 장식(decorate)
할 것이다. 예를 들어 손님이 모카하고 휘핑을 추가한 에스프레소를 주문한다면 다음과 같다.
- Espresso 객체를 가져온다.
- Mocha 객체로 장식한다.
- Whip 객체로 장식한다.
- cost() 메소드를 호출한다. 이 때 토핑 가격을 계산하는 일은 해당 객체들에게 위임된다.
그러면 객체를 어떻게 "장식" 할 수 있을까?
1️⃣ Espresso 객체에서 시작한다.
2️⃣ 모카 토핑을 주문했으니 Mocha 객체를 만들고 그 객체로 Espresso를 감싼다.
3️⃣ 휘핑 크림도 같이 주문했기 때문에 Whip 데코레이터를 만들고 그 객체로 Mocha를 감싼다.
4️⃣ 마지막으로 가격을 구한다. 가격을 구할 때는 가장 바깥쪽에 있는 데코레이터인 Whip의 cost()를 호출로 시작한다.
이제 실제 코드를 구현하기 전해 조금 더 이해하기 편하도록 클래스 다이어그램을 살펴보자.
Beverage beverage
이제 실제 코드를 작성하며 앞의 내용들을 더 명확하게 알아보자.
🥤 Beverage 클래스 🥤
public abstract class Beverage {
private String description = "제목없음";
public void setDescription(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
public abstract int cost();
}
🥛 ToppingDecorator 클래스 🥛
public abstract class ToppingDecorator extends Beverage {
public abstract String getDescription();
}
☕️ Espresso 클래스 (음료 클래스 구현) ☕️
public class Espresso extends Beverage {
public Espresso () {
setDescription("에스프레소");
}
@Override
public int cost() {
return 4000;
}
}
🍫 Mocha 클래스(토핑 데코레이터 클래스) 🍫
추상 구성요소 (Beverage), 구상 구성요소 (Esppreso), 추상 데코레이터(ToppingDecorator) 까지 만들었으니
마지막으로 구상 데코레이터를 구현하자.
public class Mocha extends ToppingDecorator {
Beverage beverage;
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
@Override
public int cost() {
return 1000 + beverage.cost();
}
@Override
public String getDescription() {
return beverage.getDescription() + ", 모카";
}
}
이제 준비가 다 됐으니 커피를 주문해보자.
🛎 실행 🛎
public class StarbuzzCoffee {
public static void main(String[] args) {
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription() + " " + beverage.cost() +"원");
Beverage beverage2 =new DarkRoast();
beverage2 = new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
System.out.println(beverage2.getDescription() + " " + beverage2.cost() + "원");
Beverage beverage3 = new HouseBlend();
beverage3 = new Soy(beverage3);
beverage3 = new Mocha(beverage3);
beverage3 = new Whip(beverage3);
System.out.println(beverage3.getDescription() + " " + beverage3.cost() + "원");
}
}
// 결과
에스프레소 4000원
다크 로스트, 모카, 모카, 휘핑 7000원
하우스 블렌드, 두유, 모카, 휘핑 7200원
첫 번째 에스프레소는 아무것도 들어가지 않는 에스프레소를 주문하고,
두 번째, 세 번째 커피는 각각 토핑을 추가하여 토핑 데코레이터로 감싸서 최종 주문을 할 수 있다.
이 쯤 되니 조금 눈치를 채보면 토핑 데코레이터로 감싸는 부분을 어디서 많이 본 것도 같다?!?!
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tempFile));
위에 공부한 자바 I/O에서 많이 본 코드이다. 몰랐지만 열심히 데코레이터를 쓰고있었다!
데코레이터 패턴도 알았으니 직접 I/O 입력 데코레이터를 만들 수 있다.
👉 입력 스트림에 있는 대문자를 전부 소문자로 바꿔주는 데코레이터를 만들자!
public class LowerCaseInputStream extends FilterInputStream {
protected LowerCaseInputStream(InputStream in) {
super(in);
}
public int read() throws IOException {
int c = super.read();
return (c == -1 ? c : Character.toLowerCase((char)c));
}
public int read(byte[] b, int offset, int len) throws IOException {
int result = super.read(b, offset, len);
for (int i = offset; i < offset+result; i++) {
b[i] = (byte)Character.toLowerCase((char)b[i]);
}
return result;
}
}
public class InputTest {
public static void main(String[] args) {
int c;
try (LowerCaseInputStream in =
new LowerCaseInputStream(
new BufferedInputStream(
new FileInputStream("test.txt")))) {
while((c = in.read()) != -1) {
System.out.print((char)c);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
I love pizza, because I just like Pizza.
test.txt 파일의 내용이 아래와 같이 대문자 -> 소문자로 변경된 것을 확인할 수 있다.
i love pizza, because i just like pizza.
데코레이터 패턴을 이용하면 일반적인 상속관계보다 유연하게 기능을 확장할 수 있지만 너무 많은 클래스가
생긴다거나 감싼 구조가 많아지다 보면 구조 때문에 디버깅이 어려워질 수 도 있다.
필요한 부분에 적절하게 사용해야하는데 항상 적절하게? 라는 말은 어렵다🥲