Decorator Pattern
- 자바의 입출력 스트림은 decorator pattern 이다.
- 여러 decorator들을 활용하여 다양한 기능을 제공
- 상속보다 유연한 구현 방식
- 데코레이터는 다른 데코레이터나 컴포넌트를 포함해야 한다.
- 지속적인 기능의 추가와 제거가 용이
- decorator와 component는 동일한 것이 아니다 (기반 스트림 클래스가 직접 읽고 쓸 수 있다. 보조 스트림은 추가적인 기능을 제공한다)
구현
데코레이터 패턴을 활용하여 커피 머신 기능을 만들어보자.
요구사항
기본적인 에스프레소 커피에 물을 추가하면 아메리카노, 우유를 추가하면 라떼, 모카 시럽을 추가하면 모카커피가 되는 형식이다.
먼저 커피를 구현해보자
public abstract class Coffee {
public abstract void brewing();
}
Coffee 클래스는 위 도표에서 Component 에 해당한다. abstract
로 만들어줘서 객체 생성을 하지 않으며 상속을 위한 클래스임을 명시해준다.
그리고 에스프레소 커피를 추출할 원두를 만들어보자.
public class Ethiopia extends Coffee {
@Override
public void brewing() {
System.out.println("Ethiopia espresso");
}
}
Coffee(Component)를 상속하여 기반이 될 커피원두들(ConcreteComponent)을 만들어준다.
이제 이 클래스들을 바탕으로 에스프레소를 만들어보자.
public class CoffeeTest {
public static void main(String[] args) {
Coffee ethiopiaCoffee = new Ethiopia();
ethiopiaCoffee.brewing();
}
}
console 에 의도한대로 출력되는 모습을 확인할 수 있다.
이제 다양한 종류의 커피를 만들기 위해 재료(Decorator)를 구현해보자.
Decorator
public abstract class Decorator extends Coffee {
Coffee coffee;
public Decorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public void brewing() {
coffee.brewing();
}
}
이 코드가 바로 Decorator Pattern 의 핵심적인 부분이다. Decorator 는 Component 를 상속하며 내부적으로도 가지고 있다가 생성자를 통해 초기화한다. 그러므로 Decorator 는 생성자의 매개변수로 Component 또는 다른 Decorator 를 받을 수 있게 된다.
또한 상속만을 위해 만들어진 클래스이므로 abstract
를 붙여서 추상 클래스로 만들어준다.
public class Latte extends Decorator {
// 상위 클래스에 기본 생성자가 없으면 하위 클래스에서 매개변수를 가진 생성자는 super() 를 호출해야한다.
public Latte(Coffee coffee) {
super(coffee);
}
@Override
public void brewing() {
super.brewing();
System.out.println("Adding Milk");
}
}
public class Americano extends Decorator {
public Americano(Coffee coffee) {
super(coffee);
}
@Override
public void brewing() {
super.brewing();
System.out.println("Add Water");
}
}
이제 만들고 싶은 커피들을 만들어주면서 Decorator 를 상속하도록 한다.
이 과정에서 상위클래스인 Decorator 에는 기본 생성자가 없으므로 반드시 상위클래스의 생성자를 호출하는 생성자를 만들어 준다.
그럼 이제 라떼를 만들어 준다면 어떻게 될까?
public class CoffeeTest {
public static void main(String[] args) {
Coffee ethiopiaCoffee = new Ethiopia();
ethiopiaCoffee.brewing();
System.out.println("--------------------");
Coffee ethiopiaLatte = new Latte(ethiopiaCoffee);
ethiopiaLatte.brewing();
}
}
실행과정은 다음과 같다.
Coffee ethiopiaLatte = new Latte(ethiopiaCoffee);
- Latte constructor 의 매개변수로 Ethiopia 가 들어감
- Latte constructor 내부의 super(coffee) 에 의해서 상위 클래스인 Decorator 의 생성자가 Ethiopia 를 매개변수로 가지고 호출.
- Decorator 는 생성자에서 field 를 초기화하는데 이 과정에서 필드값이 Ethiopia 로 설정.
- Latte 객체 생성 완료.
ethiopiaLatte.brewing();
- Latte 의 brewing() 메서드를 호출하는 과정에서 super.brewing() 이 호출됨.
- super.brewing() 에 의해 상위 클래스인 Decorator 의 brewing() 이 호출되며 coffee.brewing() 이 실행
- Decorator 의 coffee 에는 Ethiopia 가 설정되어 있으므로 Ethiopia 의 brewing() 이 실행. → console에 espresso 를 출력
- 다시 Latte 로 돌아와서 "Adding Milk" 를 출력하고 종료.
이제 계속 Decorator 를 상속하는 클래스들만 만들어주면 다양한 종류의 커피를 만들 수 있게 된다.
public class CoffeeTest {
public static void main(String[] args) {
Coffee ethiopiaCoffee = new Ethiopia();
ethiopiaCoffee.brewing();
System.out.println("--------------------");
Coffee ethiopiaLatte = new Latte(ethiopiaCoffee);
ethiopiaLatte.brewing();
System.out.println("--------------------");
Coffee ethiopiaMocha = new Mocha(new Latte(ethiopiaCoffee));
ethiopiaMocha.brewing();
System.out.println("--------------------");
Coffee kenyaCoffee = new WhippingCream(new Mocha(new Latte(new Kenya())));
kenyaCoffee.brewing();
System.out.println("--------------------");
Coffee ethiopiaAmericano = new Americano(new Ethiopia());
ethiopiaAmericano.brewing();
}
}
이런 식으로 계속 Decorator를 추가해주면서 코드를 작성해주면, 원하는 기능만 골라서 추가하거나 삭제할 수 있게 된다. (케냐 원두를 쓴 아메리카노, 에티오피아 원두를 쓴 아메리카노, 우유가 없는 라떼 등등...)
확장과 분리에 아주 유연하게 대처가능한 객체지향 원칙을 잘 지키고 있다고 볼 수 있겠다.
한 번 출력해보자.
이제 간결한 레시피를 출력할 수 있다.