

커피랑 홍차를 만들 때는 꽤나 비슷한 구석이 있는데 이를 클래스 다이어그램으로 나타내보면 다음과 같다.

내가 잘못 생각한 게 있는데... 물 끓이기와 컵에 붓기는 커피랑 홍차 둘 다 같은 로직이기 때문에 인터페이스에서 순수 가상 함수로 정의하는게 아니라 일반 메서드를 작성해야 했다 ㅋㅋ

즉, 이렇게 되면 중복되는 코드(boilWater(), pourInCup())를 인터페이스(정확히는 추상 클래스)에 정의하여 subclass에서 재활용할 수 있고 prepareRecipe는 커피랑 홍차를 만드는 과정이 같지는 않아서 추상 메서드로 남겨두고 이를 정의하는 건 커피랑 홍차가 한다.
위 디자인에서 놓친 또 다른 공통점은 없을까?
제일 위의 레시피를 보면 "우려낸다" 와 "첨가물(레몬, 설탕 등)"을 "추가"한다. 도 공통된다고 생각했다.
음.. 그럼 steep - brew(둘 다 우려냄..?) 과 첨가물을 추가하는 메서드를 추상화시킬 수 있지 않을까..? 하는 생각이 들었다. 이를 다시 클래스 다이어그램으로 나타내보면 다음과 같지 않을까?

여기서 steep과 addCondiment는 대상이 있는데(커피, 찻잎, 설탕, 시럽 등..) 이 대상들을 어떻게 구현할 수 있을까?의 문제가 남는다.
그리고 내가 놓친 부분이 있는데, Recipe를 준비하는 것도 다시 보니 1) 뭔갈 끓이고 2) 뭔갈 우려내고 3) 뭔가를 컵에 넣고 4) 첨가물을 추가 하는 것도 공통된 게 아닌가? 하는 생각이 들었다. 이렇게 구현되려나 생각했다.
class Beverage {
public:
void prepareRecipe() {
boilWater();
steep();
pourInCup();
addCondiment();
}
void boilWater() { cout<< "물이 끓고 있어요" << endl; }
void pourInCup() { cout<< "음료를 컵에 부어요" << endl; }
virtual void steep() = 0;
virtual void addConiment() = 0;
};
class Coffee : public Beverage {
private:
vector<Condiments*> condiments;
public:
Coffee(vector<Condiments*> condiments){ this->condiments = condiments; }
void steep() { cout<< "커피 우려내는 중" << endl; }
void addCondiments() {
for (auto condiment : condiments){
cout << condiment << "가 추가 됐어요" << endl;
}
}
};
// Tea도 똑같이 구현
class Tea : public Beverage {
private:
vector<Condiments*> condiments;
public:
Tea(vector<Condiments*> condiments){ this->condiments = condiments; }
void steep() { cout<< "찻잎 우려내는 중" << endl; }
void addCondiments() {
for (auto condiment : condiments){
cout << condiment << "가 추가 됐어요" << endl;
}
}
};
첨가물은 음료마다 다르고 종류가 다양하기 때문에 배열로 선언했고 임의의 커피 객체에 어떤 첨가물이 추가됐는지 알기 위해 for문을 통해 출력하도록 구현했다.

커피와 홍차 이야기에서 prepareRecipe 메서드를 보면 카페인 음료(커피와 홍차)가 만들어지는 과정(알고리즘)을 추상화했다. 이렇게 추상화한 메서드를 템플릿 메서드라고 한다.
특징은 템플릿 메서드 내의 알고리즘의 각 단계는 메서드로 표현되며 일부 메서드는 subclass에 위임할 수 있다는 것이다.
딱딱한 정의는 다음과 같다.
‘defines the skeleton of an algorithm in a method, deferring some steps to subclasses’
즉, 메서드에 알고리즘의 구조를 정의하고 일부 과정은 subclass에게 위임한다.
위에서 본 것처럼, prepareRecipe 메서드는 음료과 만들어지는 과정(알고리즘의 구조)을 정의했고 일부 과정(steep, addCondiment)은 subclass에게 위임했다. 이러한 메서드를 템플릿 메서드라고 한다.

위 클래스 다이어그램을 보면, 템플릿 메서드에는 어떤 행위를 수행하는 알고리즘이 선언되어 있고(operation1, 2) 알고리즘을 이루는 메서드들은 추상 클래스에 정의되었거나 subclass에서 정의한 메서드를 호출한다.
코드로 살펴보기
class AbstractClass {
public:
final void templateMethod(){
primitiveOperation1();
primitiveOperation2();
concreteOperation();
}
virtual void primitiveOperation1() = 0;
virtual void primitiveOperation2() = 0;
void concreteOperation() {
// implementation
}
void hook() {}
};
위 코드의 구조는 templateMethod에는 일련의 행위를 묶어놓은 알고리즘이 존재하고 그 행위들은 메서드로 이뤄지며 그 메서드의 종류는 3가지가 있다.
내용을 읽어 오면 1, 2는 이해가 됐지만 3은 모를 거다.
추상 클래스에 정의되어 있는 정의가 비었거나 아주 간단한 메서드다. 이는 subclass에서 재정의하거나 하지 않아도 되는 메서드다. 간단한 활용법은 다음과 같다.
class CaffeinBeverage {
public:
void prepare() {
boilWater();
brew();
pourInCup();
if (customerWantCondiment()) {
addCondiments();
}
}
// hook
virtual bool customerWantCondiment () { return true; }
};
class Coffee {
public:
// 위 예시 메서드와 같음.
};
위 예시 처럼 hook 메서드를 고객이 첨가물을 원하는지에 대한 여부를 체크할 때 쓸 수 있다.
더 구체적으로 살펴보면 다음과 같이 구현할 수 있다. 음료를 시키고 첨가물을 넣을 건지 고객한테 물어보고 대답에 따라 결정하는 로직이다.
class Coffee : public CaffeinBeverage {
public:
void brew() override { cout << "커피 우려내는 중.." << endl; }
void addCondiments () override { cout << "우유랑 설탕 추가" << endl; }
bool customerWantsCondiments() override {
bool isCustomerWantsCondiments = false;
cout << "우유랑 설탕을 추가하시겠어요?: " << endl;
string input;
cin >> input;
// 고객이 입력한 문자열 소문자 변환
for (int i = 0; i < input.size(); i++) {
input[i] = tolower(input[i]);
}
// 고객이 입력한 대답의 맨 앞이 소문자 y라면 우유랑 설탕 추가
if (input[0] == 'y') {
isCustomerWantsCondiments = true;
}
return isCustomerWantsCondiments;
}
};
class CaffeinBeverage {
public:
void prepare() {
boilWater();
brew();
pourInCup();
if (customerWantCondiment()) {
addCondiments();
}
}
그렇게 true가 반환된다면 Coffee 객체의 addCondiment가 실행되어 우유랑 설탕을 추가한다.
이렇게 hook 메서드를 활용할 수 있다!
C++에서 virtual를 선언해야 재정의가 가능한 이유?
상속 시 subclass 객체의 메모리 형태를 보면 다음과 같다.
Compile time
![]()
이 때 Baseclass* prt = new Derived(); 이렇게 ptr이 가리키는 것은 Base 부분이다. 이 때 비가상함수를 호출하게 되면 컴파일 타임 때 결정된 포인터의 타입인 Base 클래스의 비가상함수가 호출된다.
Run time
- 반면에 virtual 키워드를 선언한 가상 함수는 virtual table이라는 곳에 함수의 시그니쳐와 해당 메서드의 실제 메모리 주소가 저장된다. 그리고 이 가상 테이블을 가리키는 포인터(Virtual Pointer > vptr)가 컴파일러에 의해 생성된다.
- virtual 키워드가 선언된 함수를 상속받아 자식 객체에서 함수 구현을 바꾸는 경우 이를 overrding(재정의)라고 하고 재정의한 함수의 주소를 vtable에 매핑해뒀다가 함수 호출 시 런타임에 vptr->vptr+number에 있는 해당 가상함수를 호출한다.
- 즉! virtual 키워드를 선언하지 않은 함수를 상속받아서 구현을 바꾸는 것은 hiding 또는 methodOverloading이라고 하는데, 이는 다른 편에서 다루도록 한다.

"나한테 전화 걸지마세요. 제가 드릴텡께"
위 그림과 같이 High level 구성 요소가 low-level 구성 요소를 제어하고 low-level component는 직접적으로 호출할 수 없다는 원칙이 할리우드 원칙이다.
이게 템플릿 메소드랑 무슨 관련이 있을까?

High-level-component(Beverage):
Low-level-component(Coffee and Tea):
이렇게 음료가 "내가 전화할 때만 받어 먼저 전화하지 마쇼!" 를 구현할 수 있는데 이를 할리우드 원칙이라고 한다.