❓ 템플릿 메서드 패턴이란?
템플릿 메서드 패턴은 알고리즘의 골격을 정의합니다. 템플릿 메서드를 사용하면, 알고리즘의 일부 단계를 서브 클래스에서 구현할 수 있으며, 알고리즘의 구조는 그대로 유지하면서 알고리즘의 특정 단계를 서브 클래스에서 재정의할 수도 있습니다.
템플릿 메소드는 알고리즘의 템플릿(틀)로, 일련의 단계로 알고리즘을 정의한 메서드입니다.
템플릿 메서드는 여러 단계 가운데 하나 이상의 단계가 추상 메서드로 정의되며, 추상 메서드는 서브 클래스에서 구현됩니다.
이러면, 서브 클래스가 일부분의 구현을 처리하게 하면서도 알고리즘의 구조는 바꾸지 않아도 됩니다.
❗️예 1 (커피와 홍차)
커피와 홍차는 매우 비슷한 방법으로 만들어집니다.
prepareRecipe() 메서드는 알고리즘의 절차를 정의한 메서드입니다.
그리고, 각 메소드는 알고리즘의 각 단계를 구현합니다.
먼저, 커피를 우려내는 Coffee 클래스입니다.
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("설탕과 우유를 추가하는 중");
}
}
다음은 홍차를 우려내는 Tea 클래스입니다.
Tea 클래스는 두 번째 단계와 네 번째 단계가 조금 다르지만, 기본적으로 Coffee 클래스와 유사합니다.
steepTeaBag(), addLemon()은 홍차 전용 메서드이지만, boilWater()와 pourInCup()은 Coffee 클래스와 동일합니다.
즉, 중복된 코드가 발생하였습니다.
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("레몬을 추가하는 중");
}
}
그럼 어떻게 하면 중복되는 코드를 줄일 수 있을까요? 단계 별로 설계를 수정해보겠습니다.
먼저, 두 클래스의 공통된 부분을 추상화해서 베이스 클래스로 만드는 방법을 생각해볼 수 있습니다.
먼저, 커피와 홍차 모두 카페인 음료이기 때문에 CaffeineBeverage를 베이스 클래스로 두었고, 서브 클래스에서 메서드를 구현할 수 있도록 추상 클래스로 두었습니다.
prepareRecipe()는 서브 클래스마다 다르기 때문에 추상 메서드로 선언하였고,
boilWater()과 pourInCup()은 두 클래스에서 공통으로 사용되므로, CaffeineBeverage 클래스에 정의하였습니다.
서브 클래스는 prepareRecipe()를 오버라이드해서 구현하고, brewCoffeeGrinds()와 같이 해당 클래스에만 존재하는 메서드는 서브 클래스에 그대로 두었습니다.
하지만, 현재 구조에서는 커피와 홍차의 제조 과정이 비슷함에도 서브 클래스에서 구현해야 된다는 단점이 있습니다.
아래는 커피와 홍차에 모두 적용시킬 수 있는 알고리즘입니다.
- 물을 끓인다.
- 뜨거운 물을 사용해서 커피 또는 찻잎을 우려낸다.
- 만들어진 음료를 컵에 따른다.
- 각 음료에 맞는 첨가물을 추가한다.
위 알고리즘은 커피와 홍차에 모두 적용시킬 수 있기 때문에, prepareRecipe(), 즉 카페인 음료의 제조 절차를 베이스 클래스에 추상화할 수 있습니다.
이전 설계에서, Coffee 클래스는 brewCoffeeGrinds()와 addSugarAndMilk() 메서드를 쓰고,
Tea 클래스는 steepTeaBag()과 addLemon()을 사용합니다.
따라서, brewCoffeeGrinds()와 steepTeaBag()을 brew() 메서드로 일반화하고, addSugarAndMilk()와 addLemon()은 addCondiments() 즉, 첨가물을 넣는 함수로 일반화 해주겠습니다.
public abstract class CaffeineBeverage{
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("물 끓이는 중");
}
void pourInCup() {
System.out.println("컵에 따르는 중");
}
}
public class Tea extends CaffeineBeverage{
@Override
void brew() {
System.out.println("찻잎을 우려내는 중");
}
@Override
void addCondiments() {
System.out.println("레몬을 추가하는 중");
}
}
public class Coffee extends CaffeineBeverage{
@Override
void brew() {
System.out.println("필터로 커피를 우려내는 중");
}
@Override
void addCondiments() {
System.out.println("설탕과 우유를 추가하는 중");
}
}
서브 클래스에 공통되는 메서드를 베이스 클래스에 정의해주었고, 서브 클래스마다 다르게 구현하는 brew()와 addCondiments() 메서드는 추상 메서드로 선언하였습니다.
서브 클래스는 CoffeinBeverage를 상속 받아서 추상 메서드를 구현해주면 됩니다.
(CoffeinBeverage 클래스의 prepareRecipe() 메서드와 같은 Concrete 메서드는 final로 선언하여, 서브 클래스에서 재정의하지 못하도록 할 수도 있습니다)
prepareRecipe()는 서브 클래스가 임의로 오버라이딩 할 수 없도록, final로 선언하였습니다.
prepareRecipe()가 템플릿 메서드입니다. 즉, 어떤 알고리즘의 템플릿 역할을 합니다.
여기서는 카페인 음료를 만드는 알고리즘의 템플릿인 것입니다.
⚠️ Concrete 메서드란 Default Method, Abstract Method, interface private Method 외의 메서드를 의미
❗️예 2 (Hook 메서드)
후크는 추상 클래스에서 선언되지만, 기본적인 내용만 구현되어 있거나, 아무 코드도 들어있지 않은 메서드입니다.
추상 클래스에 후크 메서드가 있으면, 서브 클래스는 그 메서드를 선택적으로 오버라이딩 할 수 있습니다.
오버라이드하지 않으면, 추상 클래스에서 기본으로 제공한 코드가 실행됩니다.
public abstract class CaffeineBeverageWithHook {
void prepareRecipe() {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) {
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("Boiling water");
}
void pourInCup() {
System.out.println("Pouring into cup");
}
boolean customerWantsCondiments() {
return true;
}
}
public class CoffeeWithHook extends CaffeineBeverageWithHook {
@Override
public void brew() {
System.out.println("Dripping Coffee through filter");
}
@Override
public void addCondiments() {
System.out.println("Adding Sugar and Milk");
}
// 첨가물의 추가 여부를 반환하는 함수
@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.print("Would you like milk and sugar with your coffee (y/n)? ");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));'
try {
answer = in.readLine();
} catch (IOException ioe) {
System.err.println("IO error trying to read your answer");
}
if (answer == null) {
return "no";
}
return answer;
}
}
CaffeineBeverageWithHook 클래스, 즉, 추상클래스의 customerWantsCondiments()의 구현부를 보면 단순히 true를 반환하고 있습니다.
서브 클래스는 이 메서드를 선택적으로 오버라이딩 하면 됩니다.
위의 CoffeeWithHook 클래스는 추상 클래스의 Hook 메서드를 오버라이딩 하였습니다.
따라서, 커피를 제조할 때 사용자에게 입력을 받아서 첨가물의 추가 여부를 결정하게 됩니다.
반면, 서브 클래스가 이 메서드를 구현하지 않는다면, 카페인 음료를 제조할 때, 반드시 첨가물을 추가합니다.
❓ 템플릿을 만들 때 추상 메서드를 써야될 때와 후크를 써야할 때를 어떻게 구분할 수 있나요?
✋ 서브 클래스가 알고리즘의 특정 단계를 필수로 제공해야 한다면 추상 메서드를 써야 합니다.
알고리즘의 특정 단계가 선택적으로 적용된다면, 후크를 쓰면 됩니다.
❓ 추상 메서드가 너무 많아지면 서브 클래스에서 일일이 추상 메서드를 구현해야 하니까 별로 좋지 않을 것 같아요.
✋ 알고리즘 단계를 너무 잘게 쪼개지 않는 것도 한 가지 방법입니다.
하지만, 알고리즘을 큼직한 몇 가지 단계로만 나눠 놓으면 유연성이 떨어진다는 단점도 있습니다.
또한, 모든 단계가 필수가 아니기 때문에, 필수가 아닌 부분을 후크로 구현하면,
그 추상 클래스의 서브 클래스를 만들 때 부담이 줄어듭니다.
🎯실 사용 예시 (Arrays.sort())
private static void mergeSort(Object[] src, Object[] dest,
int low, int high, int off) {
// 많은 코드
for (int i=low; i<high; i++){
for (int j=i; j>low &&
((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)
swap(dest, j, j-1);
}
// 많은 코드
}
Arrays의 mergeSort() 함수를 보면 이런식으로 작성되어 있습니다.
mergeSort() 안에서는 compareTo()와 swap()이라는 method를 사용합니다.
- compareTo() - Comparable 인터페이스의 메서드
- swap() - Arrays 클래스에 의미 정의되어 있는 Concreate 메서드
이러한 mergeSort()도 템플릿 메서드라고 할 수 있습니다.
❓ Arrays는 추상 클래스도 아닐 뿐더러, Arrays의 서브 클래스를 만들지 않았는데 템플릿 메서드 패턴인가?
✋ 실전에서 패턴을 적용는 방법이 책에 나와있는 방법과 완전히 같을 수는 없습니다.
주어진 상황과 구현상 제약조건에 맞게 고쳐서 적용해야합니다.
Arrays의 mergeSort() 메서드를 디자인한 사람도 몇 가지 제약조건이 있었습니다.
자바에서는 배열의 서브 클래스를 만들 수 없지만,
모든 타입의 배열에 대해 정렬 기능을 사용할 수 있도록 만들어야 했습니다.
그래서 정적 메소드를 정의한 다음, 대소를 비교하는 부분은 정렬될 객체에서 구현하도록 만든겁니다.
만약 Array가 상속 가능했다면 위와 같은 방식으로 구현됐을 것입니다.
❓ 추상 클래스 말고, 인터페이스의 디폴트 메소드로 템플릿 메소드 패턴을 구현할 수 있지 않나요?
✋ 인터페이스의 경우, 디폴트 메서드는 final로 정의할 수 없어 재정의가 가능하기 때문에,
추상 클래스에서 알고리즘을 독점할 수 없습니다.
또한, 인터페이스의 모든 추상 메소드는 public이므로
템플릿 메서드 내부에서만 호출되어야 할 메서드들이 의도치 않은 사용처에서 호출될 위험이 있습니다.
👍 템플릿 메서드 패턴의 장점
시시한 Tea와 Coffee 클래스 | 템플릿 메서드로 새로 만든 CoffeinBeverage 클래스 |
Coffee와 Tea 클래스가 각각 작업을 처리하고, 두 클래스에서 각자 알고리즘을 수행 | CaffeineBeverage 클래스에서 작업을 처리하고, 알고리즘을 독점 |
Coffee와 Tea 클래스에 중복된 코드가 존재 | CaffeineBeverage 클래스 덕분에 공통 메서드를 서브 클래스에서 재사용할 수 있음 |
알고리즘이 바뀌면, 서브 클래스를 일일이 고쳐주어야 함 | 알고리즘이 한 군데 모여 있으므로, 한 부분만 고치면 됨 |
클래스 구조상 새로운 음료를 추가하려면, 꽤 많은 일을 수행해야 하며, 중복 코드가 또 발생함 | 다른 음료도 쉽게 추가할 수 있는 프레임워크를 제공, 음료를 추가할 때 몇 가지 메서드만 더 만들면 됨 |
알고리즘 지식과 구현 방법이 여러 클래스에 분산되어 있음 | CaffeineBeverage 클래스에 알고리즘 지식이 집중되어 있으며, 일부 구현만 서브 클래스에 의존. 즉, 알고리즘과 구체적인 구현을 분리할 수 있음. |
참조
- Head First Design Pattern Chapter 8, 템플릿 메소드 패턴
- [Design Pattern] 템플릿메소드(Template Method)란?
- [Design Pattern] 템플릿 메서드 패턴이란
- How does the JLS specify the terms "abstract method", "concrete method" and "default method"?
- JLS 8.4.3.1, JLS 9.4
'스터디' 카테고리의 다른 글
데코레이터 패턴(Decorator Pattern)이란? (0) | 2023.04.04 |
---|---|
22.11.15 SSAFY 스터디 CS 발표 - 디자인 패턴 (전략 패턴) (0) | 2022.12.01 |
22.10.11 SSAFY 스터디 CS 발표 - 컴퓨터 구조, 기억장치 (0) | 2022.10.11 |
22.09.22 SSAFY 스터디 CS 발표 - 컴퓨터 구조 (0) | 2022.09.22 |
22.08.18 SSAFY 스터디 CS 발표 - Java 어셈블리어 분석 (0) | 2022.08.16 |