템플릿 메소드 패턴 (Template Method Pattern)
알고리즘의 골격을 정의한다.
이 퍁너을 사용하면 알고리즘의 일부 단계를 서브클래스에서 구현할 수 있으며, 알고리즘의 구조는 그대로 유지하면서 알고리즘의 특정 단계를 서브클래스에서 재정의할 수도 있다.
템플릿 메소드 패턴은 말그대로 알고리즘의 템플릿(틀)을 만든다.
여기서 템플릿이란 그냥 메소드를 말한다. 구체적으로는, 일련의 단계로 알고리즘을 정의한 메소드이다.
여러 단계 중 하나 이상의 단계가 추상 메소드로 정의되며,
그 추상 메소드는 서브클래스에서 구현된다.
이러면 서브클래스가 일부분의 구현을 처리하게 하면서도 알고리즘의 구조는 바꾸지 않아도 된다.
커피와 홍차는 비슷하다. 만드는 방법 또한 비슷하다.
[커피 만드는 법]
1. 물을 끓인다.
2. 끓는 물에 커피를 우려낸다.
3. 커피를 컵에 따른다.
4. 설탕과 우유를 추가한다.
[홍차 만드는 법]
1. 물을 끓인다.
2. 끓는 물에 찻잎을 우려낸다.
3. 홍차를 컵에 따른다.
4. 레몬을 추가한다.
전체적으로 만드는 방법이 비슷하기 때문에,
공통된 부분은 추상화하면 좋을 거 같다 !
클래스를 추상화하면 위와 같다.
아까 말했듯이 커피와 홍차 제조법의 알고리즘을 똑같다.
그렇기 때문에 prepareRecipe() 메소드를 추상화할 수 있을 거 같다.
일단, [물을 끓이고], [컵에 붓는] 과정을 똑같다.
[커피를 우려낸다/찻잎을 우려낸다], [설탕과 우유를 추가한다/레몬을 추가한다] 부분만 다른데, 또 완전 다른 건 아니다.
전자는 [우리기], 후자는 [첨가물 추가]로 통일할 수 있다.
그럼 코드가 다음과 같이 된다.
[ CaffeineBeverage (인터페이스) ]
public abstract class CaffeineBeverage {
final void prepareRecipe() {
boidWater();
brew();
pourInCup();
addCondiments();
}
// Coffee와 Tea에서 두 메소드를 서로 다른 방식으로 처리한다.
// -> 추상 메소드로 선언
// -> 서브클래스가 알아서 처리한다.
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("물 끓이는 중");
}
void pourInCup() {
System.out.println("컵에 따르는 중");
}
}
[ Coffee ]
public class Coffee extends CaffeineBeverage {
public void brew() {
System.out.println("필터로 커피를 우려내는 중");
}
public void addCondiments() {
System.out.println("설탕과 우유 추가 중");
}
}
[ Tea ]
public class Tea extends CaffeineBeverage {
public void brew() {
System.out.println("찻잎을 우려내는 중");
}
public void addCondiments() {
System.out.println("레몬 추가 중");
}
}
앞에서 봤던 Coffee
와 Tea
클래스에 템플릿 메소드 패턴을 적용했다.
그리고 CaffeineBeverage
클래스에 템플릿 메소드 가 들어있다.
여러 메소드 중 prepareRecipe() 가 템플릿 메소드였다.
1. 일단 메소드이고,
2. 카페인 음료를 만드는 알고리즘의 템플릿(틀) 역할을 하기 때문이다.
그렇다면 템플릿 메소드 패턴의 장점은 무엇일까.
단순히 Coffee
와 Tea
클래스를 사용했을 때에 비교하여,
템플릿 메소드가 있는 CaffeineBeverage
를 사용하면 다음과 같은 장점을 가진다.
CaffeineBeverage
클래스에서 작업을 처리한다.
즉, 알고리즘을 독점한다.
CaffeineBeverage
덕분에 서브클래스에서 코드를 재사용할 수 있다.
알고리즘이 한 군데에 모여있으므로 한 부분만 수정하면 된다.
다른 음료도 쉽게 추가할 수 있는 프레임워크를 제공한다.
음료를 추가하고 싶으면 몇 가지 메소드만 더 만들면 된다.
CaffeineBeverage
클래스에 알고리즘 지식이 집중되어 있으며, 일부 구현만 서브클래스에 의존한다.
후크(hook) 는 추상 클래스에서 선언되지만 기본적인 내용만 구현되어 있거나 아무 코드도 들어있지 않은 메소드이다.
후크를 사용하면, 서브클래스가 다양한 위치에서 알고리즘에 끼어들 수 있다.
물론 그냥 무시하고 넘어갈 수도 있다.
아까 본 CaffeineBeverage
클래스에 후크를 추가해보자.
(클래스명만 살짝 바꿔줬다.)
[ CaffeineBeverageWithHook (인터페이스) ]
public abstract class CaffeineBeverageWithHook {
final void prepareRecipe() {
boidWater();
brew();
pourInCup();
// 고객 요청이 있을 때만 호출
if (customerWantsCondiments()) {
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("물 끓이는 중");
}
void pourInCup() {
System.out.println("컵에 따르는 중");
}
// 후크
// 별 내용이 없는 기본 메소드: true만 리턴
// 서브클래스에서 필요할 때 오버라이드
boolean customerWantsCondiments() {
return true;
}
}
그럼 이제 후크를 활용해보자.
후크를 사용하려면 서브클래스에서 후크를 오버라이드 해야한다.
서브클래스 CoffeeWithHook
클래스를 만들자.
[ CoffeeWithHook ]
public class CoffeeWithHook extends CaffeineBeverageWithHook {
public void brew() {
System.out.println("필터로 커피 우려내는 중");
}
public void addCondiments() {
System.out.println("우유랑 설탕 추가 중");
}
// 후크 오버라이드
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 in = new BufferedReader(new InputStreamReader(System.in));
try {
answer = in.readLine();
} catch (IOException ioe) {
System.err.println("IO error");
}
if (answer == null) {
return "no";
}
return answer;
}
}
[ Test code ]
public class BeverageTestDrive {
public static void main(String[] args) {
TeaWithHook teaHook = new TeaWithHook();
CoffeeWithHook coffeeHook = new CoffeeWithHook();
System.out.println("홍차 준비 중");
teaHook.prepareRecipe();
System.out.println("커피 준비 중");
coffeeHook.prepareRecipe();
}
}
그렇다면 후크의 용도는 정확히 뭘까.
후크는 여러 가지 용도로 쓰이는데 몇 가지 설명하면 다음과 같다.
알고리즘에서 필수적이지 않은 부분을 서브클래스에서 구현하도록 만들고 싶을 때 사용하는 용도
팀플릿 메소드에서 앞으로 일어날 일이나 막 일어난 일에 서브클래스가 반응할 수 있도록 기회를 제공하는 용도
서브클래스가 추상 클래스에서 진행되는 작업을 처리할지 말지 결정하게 하는 기능을 부여하는 용도
디자인 원칙
먼저 연락하지 마세요. 저희가 연락 드리겠습니다.
이번에도 새로운 디자인 원칙이 등장한다.
할리우드 원칙을 사용하면 의존성 부패(dependency rot) 을 방지할 수 있다.
아니, 의존성 부패는 또 뭐야?
의존성 부패는 의존성이 복잡하게 꼬여있는 상황 을 의미한다.
쉽게 설명하면, 어떤 고수준 구성 요소가 저수준 구성 요소에 의존하고, 그 저수준 구성 요소는 다시 고수준 구성 요소에 의존하고, 그 고수준 구성 요소는 다시 또 다른 구성 요소에 의존하고, 그 다른 구성 요소는 또 저수준 구성 요소에 의존하는 것과 같은 상황이다.
할리우드 원칙을 사용하면,
저수준 구성 요소가 시스템에 접속할 수는 있지만,
언제, 어떻게 그 구성 요소를 사용할지는 고수준 구성 요소가 결정한다.
즉, "먼저 연락하지 마세요. 제가 먼저 연락 드리겠습니다." 이 말은 고수준 구성 요소가 저수준 구성 요소에게 하는 말이다.
템플릿 메소드 패턴과 할리우드 원칙에 관계는 다음 그림에서 확인할 수 있다.
객체지향 기초
- 추상화
- 캡슐화
- 다형성
- 상속
객체지향 원칙
- 바뀌는 부분은 캡슐화 한다.
- 상속보다는 구성을 활용한다.
- 구현보다는 인터페이스에 맞춰서 프로그램이한다.
- 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.
- OCP: 클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다.
- 추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.
- 진짜 절친에게만 이야기해야 한다.
- 먼저 연락하지 마세요. 저희가 연락 드리겠습니다.
객체지향 패턴
전략 패턴
전략 패턴은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 한다.
전략패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다.옵저버 패턴
한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로, 일대다 (one-to-many) 의존성을 정의한다.
데코레이터 패턴
객체에 추가 요소를 동적으로 더할 수 있다.
데코레이터를 사용하면 서브 클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있다.팩토리 메소드 패턴
객체에서 생성할 때 필요한 인터페이스를 만든다.
어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정한다.
이 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에게 맡긴다.추상 팩토리 패턴
구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공한다.
구상 클래스는 서브클래스에서 만든다.싱글턴 패턴
클래스 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공한다.
커맨드 패턴
요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화할 수 있다. 이러면 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있다.
어댑터 패턴
특정 클래스 인터페이스를 클라이언트에서 요구하는 다른 인터페이스로 변환한다.
인터페이스가 호환되지 않아 같이 쓸 수 없었떤 클래스를 사용할 수 있게 도와준다.퍼사드 패턴
서브시스템에 있는 일련의 인터페이스를 통합 인터페이스로 묶어 준다.
또한 고수준 인터페이스도 정의하므로 서브시스템을 더 편리하게 사용할 수 있다.템플릿 메소드 패턴 (Template Method Pattern)
알고리즘의 골격을 정의한다.
템플릿 메소드를 사용하면 알고리즘의 일부 단계를 서브클래스에서 구현할 수 있으며, 알고리즘의 구조는 그대로 유지하면서 알고리즘의 특정 단계를 서브클래스에서 재정의할 수도 있다.