Head First Design Patterns 책을 읽으며 정리한 내용 입니다. 문제가 될시 글을 내리도록 하겠습니다!
Decorator Pattern 이란?
Decorator Pattern - 객체에 추가적인 요건을 동적으로 첨가한다.
데코레이터는 서브클래스를 만드는 방식을 통하여 유연하게 확장하는 방법을 제공한다.
커피 한잔을 주문할때 단순 커피만이 아닌 모카시럽, 휘핑크림 등 을 추가하고 싶을 수 있습니다.
데코레이터 패턴 에서는 우선 커피 객체를 만들고, 이 만들어진 커피 객체를 Mocha 객체로 감싸고, 다시 이 모카커피 객체를 휘핑크림 객체로 감싸면서 Wrapper 형태로 만들어 집니다. 다음 그림과 같이 말이죠!
이제 커피의 가격을 측정하고 싶다면 가장 외각의 Whip 객체의 Cost() 를 호출합니다.
그럼 Whip이 Mocha의 cost()를 호출하게 됩니다.
다시 Mocha가 Coffee의 Cost()를 호출하게 되고, Coffee의 cost()가 1000원을 반환합니다.
Mocha 객체에서는 반환받은 1000원에 모카 값 500원을 추가하여 1500원 을 반환합니다.
Whip 객체에서는 반환받은 1500원에 휘핑크림 값 700원을 추가하여 2200원 을 반환합니다.
1) 데코레이터의 수퍼클래스는 자신이 장식하고 있는 객체의 수퍼클래스와 같습니다.
2) 따라서 원래 객체가 들어갈 자리에 데코레이터 객체를 집어 넣어도 상관이 없습니다.
3) 데코레이터는 자신이 감싸면서 장식하고 있는 객체에게 어떤 행동을 위임하는 것 외에도 추가적인 행동을 수행할 수 있습니다.
4) 객체는 언제든 동적으로 필요한 데코레이터 객체에게 감싸질 수 있습니다.
이말이 무슨 의미인지 다음 단락부터 예시를 통하여 설명해 보겠습니다!
패턴 소개
다음과 같은 음료에 관한 추상 클래스가 있다고 생각해 봅시다!
Beverage{
description
getDescription();
abstract cost();
}
위의 추상 클래스를 바탕으로 이를 구현하는 여러 커피class를 만들 수 있습니다. 다음과 같이 말이죠!
Latte implements Beverage{
cost();
}
Espresso implements Beverage{
cost();
}
DarkRoast implements Beverage{
cost();
}
문제는! 커피를 주문할때 스팀 우유, 휘핑크림, 모카 등 을 추가하기도 한다는 점 입니다.
각각 을 추가할 때 마다 커피의 가격이 올라가기 때문에 이러한 점을 고려한 class들을 모두 만들어야 합니다.
Latte implements Beverage{
cost();
}
LatteWithSteamMilk implements Beverage{
cost();
}
LatteWithWhipandMocha implements Beverage{
cost();
}
... 등등등
이런식으로 작업하면 토핑을 추가할 때 마다 클래스의 수가 끝도없이 늘어나게 될 것 입니다.
여기서 잠깐!!
OCP(Open Closed Principle) 에 대하여 알아봅시다.
간단히 "기존의 코드는 건드리지 않으면서 확장을 통해 새로운 행동을 추가할 수 있도록 하자" 는 원칙 입니다.
클래스는 확장에 대해서는 열려있지만, 코드 변경에 대해서는 닫혀 있어야 합니다.
위에서와 같이 class를 여러개 만드는 대신 우리가 사용할 방법은 "장식" 을 추가해주는 방식 입니다.
예를 들어 모카하고 휘핑 크림을 추가한 latte를 주문한다면 다음과 같은 방식으로 해결할 수 있습니다.
1) Latte 객체를 생성한다.
2) Latte 객체에 Mocha 객체를 장식으로 더한다.
3) (Latte + Mocha)에 휘핑크링 장식을 더한다.
4) cost()를 호출한다. 이때 해당 첨가물의 가격을 계산하는 일은 해당 토핑 객체가 수행해야 합니다!
이에 대한 설명과 사진을 이 게시물 도입부에서 보여드린적이 있습니다! 기억하시죠? (아까 위에서 이해를 잘 못하셨다면 이제 다시한번 읽어보고 오시길 권장합니다!)
이제 데코레이터 에 관한 다이어그램을 우선 확인해 봅시다.
coffeeDecorator 또한 Coffee 라는 슈퍼클래스를 상속하고 있습니다.
글쓰니인 저는 여기서 한가지 의문이 들었습니다...
"상속을 한다고? 이책 읽을때 객체지향의 원칙으로 상속보다는 구성을 활용하라고 했는데? 뭐지?"
이 생각을 하자마자 바로 옆 페이지에서 기다렸다는 듯 이에 대한 설명을 해주었습니다... 와... 2004년도 책에서 2021 년도 저의 의문을 예측 당할 줄 이야...
데코레이터의 형식이 그 데코레이터로 감싸는 객체의 형식과 같다는 점이 핵심입니다. 상속을 통하여 형식을 맞추는 거지, 상속을 통하여 행동을 물려받는 것이 목적이 아니라 이말입니다.
그럼 행동은 어디서 오는거임?? ( 거짓말 없이 100%로 이렇게 생각했는데... 또 이책이 저를 예측해버린... )
행동은 구성요소(감싸질 대상)를 갖고 데코레이터로 감싸줄 때 행동을 추가해주면 됩니다.
데코레이터는 그 데코레이터가 감싸고 있는 객체에 행동을 추가하기 위한 용도로 만들어 진 것 입니다.
행동이 슈퍼클래스로부터 상속받는 것 이 아닌! 객체들을 구성하는(인스턴스 변수로 다른 객체를 저장하는 방식) 방법을 통하여 얻게되는 것 입니다.
만약 행동을 상속받아 사용했다면 컴파일타임에 정적으로 결정되어버리고 말 것 입니다. 이는 즉 수퍼클래스에서 받은것 or 오버라이드 한 것만 쓸 수 있다는 말 입니다.
하지만 우리는 구성을 활용하면 실행중에 데코레이터를 동적으로 조합할 수 있게됩니다.
이제 데코레이터 패턴을 이용하여 직접 구현해 봅시다! 우선 다이어그램 살펴본 후 코드를 봅시다.
다이어 그램은 위에서 보여줬던 다이어 그램과 거의 동일합니다.
우선 Beverage class 를 확인해 봅시다.
public abstract class Beverage {
String description = "제목 없음";
public String getDescription() {
return description;
}
public abstract double cost();
}
이제 토핑을 나타내는 추상클래스인 데코레이터를 확인해 봅시다.
// 추가 토핑을 의미하는 데코레이터 추상 클래스
public abstract class CondimentDecorator extends Beverage{ // Beverage가 들어갈 자리에 들어가기 위해 확장
public abstract String getDescription(); // 모든 데코레이터에서 새로 구현하도록 하기 위함
}
베이스 클래스들이 준비되었으니 음료를 구현해 봅시다.
에스프레소와 하우드 블렌드 2종류만 구현하였습니다.
public class Espresso extends Beverage{
public Espresso(){
description = "에스프레소"; // Beverage로 부터 상속받은 인스턴스 변수를 초기화
}
public double cost(){
return 1.99;
}
}
public class HouseBland extends Beverage{
public HouseBland(){
description = "하우스 블렌드 커피";
}
public double cost(){
return 0.89;
}
}
데코레이터 추상 클래스를 기반으로 하는 토핑들을 구현해 봅시다.
일단 모카 하나만 구현해놨는데, 두유나 휘핑 크림등 동일하게 구현됩니다.
public class Mocha extends CondimentDecorator {
Beverage beverage; // 레퍼런스 변수를 갖고있다.
public Mocha(Beverage beverage){
this.beverage = beverage; // 위의 레퍼런스 변수에 감싸고자 하는 객체로 설정하기 위한 생성자.
}
public String getDescription() {
return beverage.getDescription() + ", 모카";
}
public double cost(){
return 0.20 + beverage.cost(); //
}
}
이제 커피를 만들어 봅시다!
public class StarBuzzCoffee {
public static void main(String[] args){
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription() + " $" + beverage.cost());
Beverage beverage2 = new Espresso();
beverage2 = new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
System.out.println(beverage2.getDescription() + " $" + beverage2.cost());
Beverage beverage3 = new HouseBland();
beverage3 = new Mocha(beverage3);
beverage3 = new Soy(beverage3);
beverage3 = new Whip(beverage3);
System.out.println(beverage3.getDescription() + " $" + beverage3.cost());
}
}
실행결과는 다음과 같습니다.
이런 완벽할 것 같은 데코레이터 패턴에도 단점은 존재합니다.
1) 자질한 클래스들이 엄청나게 추가되는 것 입니다.
자바의 I/O 라이브러리 또한 데코레이터 패턴을 활용하는데 얼마나 지저분한지 확인할 수 있습니다.
다행인건 그 많은 클래스 들이 InputStream을 감싸기 위한 일련의 Wrapper class라 생각하면 편하다는 것 입니다.
2) 또한 데코레이터를 도입하면 구성요소를 초기화 하는데 복잡해지는 단점이 있습니다.
데코레이터를 사용하게 되면 단순하게 구성요소 인스턴스만 생성한다고 해서 끝나는 것 이 아니기 때문입니다. 데코레이터로 대부분 감싸고 나서야 사용할만한 인스턴스가 되는 것 이죠.
3) 클라이언트 쪽에서는 데코레이터를 사용하고 있다는 것을 전혀 알 수 없다는 문제가 있습니다. 다음 코드를 살펴봅시다.
public class Client {
public static void main(String[] args) {
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription() + " $" + beverage.cost());
Beverage beverage2 = new Espresso();
beverage2 = new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
System.out.println(beverage2.getDescription() + " $" + beverage2.cost());
}
}
각 beverage, beverage2 객체의 접근이 모두 Beverage 클래스를 통해 이루어 집니다.
beverage를 사용하는 입장에서는 이게 순수한 Espresso 인지, 토핑이 추가된건지는 마셔보기 전까지는(사용해보기 전까지는) 모릅니다.
즉, 어떤 기능을 추가하느냐에 관계없이 Client 클래스는 동일한 Beverage 클래스만을 통해 일관성 있는 방식으로 커피 인스턴스에 접근할 수 있게됩니다.
이는 구현이 아닌 인터페이스에 맞춰 프로그래밍 했기 때문에 오는 이점이자, 단점이라 할 수 있겠습니다.
이러한 단점은 팩토리, 빌더 를 통하여 도움 받을 수 있게됩니다. 이는 차후 글로 작성해 보겠습니다.
'BackEnd > Design Patterens' 카테고리의 다른 글
[Design Patterns] Command Pattern : 커맨드 패턴 (0) | 2022.01.13 |
---|---|
[Design Patterns] Singleton Pattern : 싱글턴 패턴 (0) | 2022.01.13 |
[Design Patterns] Factory Pattern : 팩토리 패턴 (0) | 2022.01.13 |
[Design Patterns] Observer Pattern : 옵저버 패턴 (0) | 2022.01.12 |
[Design Patterns] Strategy Pattern : 스트래티지 패턴 (0) | 2022.01.12 |
댓글