2021. 10. 14. 20:54ㆍComputer Sciences/Design Patterns
문제
어느 카페에서는 커피와 홍차를 판매한다. 이 둘의 공통점은 무엇일까? 카페인이 들어간다는 것이다. 각각을 만드는 법은 다음과 같다.
커피 만드는 법
- 물을 끓인다.
- 끓는 물에 커피를 우려낸다.
- 커피를 컵에 따른다.
- 설탕과 우유를 추가한다.
홍차 만드는 법
- 물을 끓인다.
- 끓는 물에 차를 우려낸다.
- 차를 컵에 따른다.
- 레몬을 추가한다.
이를 각각 클래스로 작성하면 아래와 같다.
public class Coffee {
void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
public void boilWater() {
System.out.println("물 끓이는 중");
}
public void brewCoffeeGrinds() {
System.out.println("필터를 통해서 커피를 우려내는 중");
}
public void pourInCup() {
System.out.println("컵에 따르는 중");
}
public void addSugarAndMilk() {
System.out.println("설탕과 우유를 추가하는 중");
}
}
public class Tea {
void prepareRecipe() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
public void boilWater() {
System.out.println("물 끓이는 중");
}
public void steepTeaBag() {
System.out.println("차를 우려내는 중");
}
public void pourInCup() {
System.out.println("컵에 따르는 중");
}
public void addLemon() {
System.out.println("레몬을 추가하는 중");
}
}
뭔가 중복이 보이는 것 같다. 이를 상속을 통해 해결하자.
public abstract class CaffeinBeverage {
abstract void prepareRecipe();
public void boilWater() {
System.out.println("물 끓이는 중");
}
public void pourInCup() {
System.out.println("컵에 따르는 중");
}
}
public class Coffee extends CaffeinBeverage {
@Override
void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
public void brewCoffeeGrinds() {
System.out.println("필터를 통해서 커피를 우려내는 중");
}
public void addSugarAndMilk() {
System.out.println("설탕과 우유를 추가하는 중");
}
}
public class Tea extends CaffeinBeverage {
@Override
void prepareRecipe() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
public void steepTeaBag() {
System.out.println("차를 우려내는 중");
}
public void addLemon() {
System.out.println("레몬을 추가하는 중");
}
}
상속을 통해 훌륭하게 구현했다. 그런데 아직 뭔가 찝찝하다. 우려내는 행위와 추가하는 행위는 두 음료 모두 똑같은데 이것도 처리하고 싶다. 우려내는 행위를 brew()
로 뽑아내고 추가하는 행위도 addCondiments()
로 뽑아내자.
public abstract class CaffeinBeverage {
abstract void prepareRecipe();
public void boilWater() {
System.out.println("물 끓이는 중");
}
public void pourInCup() {
System.out.println("컵에 따르는 중");
}
abstract void brew();
abstract void addCondiments();
}
public class Coffee extends CaffeinBeverage {
@Override
void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
@Override
public void brew() {
System.out.println("필터를 통해서 커피를 우려내는 중");
}
@Override
public void addCondiments() {
System.out.println("설탕과 우유를 추가하는 중");
}
}
public class Tea extends CaffeinBeverage {
@Override
void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
@Override
public void brew() {
System.out.println("차를 우려내는 중");
}
@Override
public void addCondiments() {
System.out.println("레몬을 추가하는 중");
}
}
이렇게 바꿔놓고 보니 두 음료의 prepareRecipe()
가 완전히 동일해진 것을 볼 수 있다. 이것도 추상화할 수 있을까? 물론이다. 방법은 매우 간단하다. CaffeinBeverage
에서 이 메서드를 final
로 선언하고 구현하면 된다.
public abstract class CaffeinBeverage {
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
public void boilWater() {
System.out.println("물 끓이는 중");
}
public void pourInCup() {
System.out.println("컵에 따르는 중");
}
abstract void brew();
abstract void addCondiments();
}
public class Coffee extends CaffeinBeverage {
@Override
public void brew() {
System.out.println("필터를 통해서 커피를 우려내는 중");
}
@Override
public void addCondiments() {
System.out.println("설탕과 우유를 추가하는 중");
}
}
public class Tea extends CaffeinBeverage {
@Override
public void brew() {
System.out.println("차를 우려내는 중");
}
@Override
public void addCondiments() {
System.out.println("레몬을 추가하는 중");
}
}
이를 클래스 다이어그램으로 표현하면 다음과 같다.
갑자기 어떤 마법이 일어난걸까? 아니다. 두 클래스의 행위는 완전히 동일하여 우리는 이 행위들을 추상화했고 추상 클래스에서 공통된 일련의 행위를 템플릿화하여 이를 상속하는 클래스들이 각자 다른 행위들을 구현하도록 만든 것이다. 이러한 패턴을 템플릿 메서드 패턴이라고 부른다.
템플릿 메서드 패턴
메서드에서 알고리즘의 골격을 정의하는 패턴이다. 알고리즘의 여러 단계 중 일부는 서브 클래스에서 구현할 수 있다. 템플릿 메서드를 이용하면 알고리즘의 구조는 그대로 유지하면서 서브 클래스에서 특정 단계를 재정의할 수 있다.
훅(Hook)
훅은 추상 클래스에서 선언되는 메소드이긴 하지만 기본적인 내용만 구현되어 있거나 아무 코드도 들어있지 않은 메서드이다. 이렇게 하면 서브 클래스 입장에서는 다양한 위치에서 알고리즘에 끼어들 수 있다. 물론 그냥 무시하고 넘어갈 수도 있다.
public abstract class CaffeinBeverageWithHook {
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) {
addCondiments();
}
}
public void boilWater() {
System.out.println("물 끓이는 중");
}
public void pourInCup() {
System.out.println("컵에 따르는 중");
}
abstract void brew();
abstract void addCondiments();
boolean customerWantsCondiments() {
return true;
}
}
public class CoffeeWithHook extends CaffeinBeverage {
@Override
public void brew() {
System.out.println("필터를 통해서 커피를 우려내는 중");
}
@Override
public void addCondiments() {
System.out.println("설탕과 우유를 추가하는 중");
}
@Override
public boolean customerWantsCondiments() {
String answer = getUserInput();
if (answer.toLowercase().startswith("y")) {
return true;
} else {
return false;
}
}
private String getUserInput() {
String answer = null;
System.out.println("커피에 우유와 설탕을 넣으시겠습니까? y/n");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
try {
answer = br.readLine();
} catch (IOException e) {
System.err.println("IO 오류");
}
if (answer == null) {
return "no";
}
return answer;
}
}
헐리우드 원칙
이번 템플릿 메서드 패턴에서 사용된 새로운 객체지향 원칙은 헐리우드 원칙이다. 헐리우드 원칙을 활용하면 의존성 부패(dependency rot)를 방지할 수 있다. 의존성 부패는 고수준의 구성요소가 저수준의 구성요소에 의존하고, 그 저수준 구성요소는 다시 고수준의 구성요소에게 의존하는 이런 서로 얽히는 복잡한 구조를 말한다. 헐리우드 원칙은 저수준 구성요소에서 시스템에 접속을 할 수는 있지만, 언제 어떤 식으로 그 구성요소들을 사용할지는 고수준 구성요소에서 결정하게 한다. 즉, 고수준 구성요소에서 저수준 구성요소에게 "먼저 연락하지 마세요. 제가 먼저 연락 드리겠습니다"라고 얘기하는 것과 같다.
우리는 템플릿 메서드 패턴을 구현하면서 이 원칙을 훌륭하게 지켰다. Coffee
와 Tea
는 CaffeinBeverage
를 구현하여 각각의 알고리즘을 완성시킨다. 하지만 직접 할 수 있는 것은 아무것도 없다. 실제 동작은 전부 CaffeinBeverage
가 쥐고 있으며 prepareRecipe()
가 실행될 때 Coffee
와 Tea
는 잠깐 사용되는 것이다. 물론 서브 클래스는 슈퍼 클래스를 상속하는 것이고 서브 클래스에서 슈퍼 클래스에 작성된 내용을 사용할 수는 있다. 헐리우드 원칙은 이러한 관계를 최대한 줄이고 순환 참조를 막기 위한 원칙 중 하나이다.
'Computer Sciences > Design Patterns' 카테고리의 다른 글
11. Composite Pattern (0) | 2021.10.15 |
---|---|
10. Iterator Pattern (0) | 2021.10.15 |
8. Facade Pattern (0) | 2021.10.14 |
7. Adapter Pattern (0) | 2021.10.14 |
6. Command Pattern (0) | 2021.10.14 |