C++ - 템플릿 메서드 패턴 (Template Method Pattern)
Architecture & Design Pattern 전체 링크
참고
- 헐리우드 원칙
템플릿 메서드 패턴 (Template Method Pattern) - 행동 패턴
- 알고리즘의 구조를 정의하는 패턴
- 알고리즘 구조는 유지하면서 알고리즘의 일부 단계를 서브클래스에서 구현할 수 있다.
구현
- Template Method : 알고리즘의 구조를 정의한 메서드, final 등으로 재정의가 불가능하도록 정의
- Abstract Method : Template Method에서 정의한 구조의 일부 단계를 구체화하기 위해 하위 클래스에서 구현
- Hook : 하위 클래스에서 선택적으로 오버라이드 할 수 있도록 제공되는 메서드
장점
- 알고리즘 구조를 상위 클래스에 정의하므로, 하위 클래스에서 동일한 알고리즘을 재사용한다.
- 알고리즘의 구조가 명확하게 정의되어 있으므로, 수정이나 변경이 필요할 때 해당 부분만 수정할 수 있다.
- 하위 클래스에서 오버라이딩이 가능하므로 유연성이 높다.
단점
- 상속을 사용하기 때문에 하위 클래스가 상위 클래스에 강하게 결합된다.
- 불필요한 메서드도 강제로 구현해야 하는 경우가 발생할 수 있다.
- 템플릿 메서드에서 구성되는 단계가 많을수록 유지 보수가 힘들어진다.
알고리즘
알고리즘 문제를 풀기 위해 input → sort → search → output 과정을 거쳐야 한다.
그래서 알고리즘 클래스를 아래와 같이 정의하였고, 퀵 정렬과 이분 탐색을 사용하였다.
class MyAlgorithm1
{
public:
void run()
{
input();
quickSort();
binarySearch();
output();
}
private:
void input() { cout << "Input" << endl; }
void quickSort() { cout << "Quick Sort" << endl; }
void binarySearch() { cout << "Binary Search" << endl; }
void output() { cout << "Output" << endl; }
};
문제를 풀기 위해 알고리즘 전략을 바꿀 수도 있다.
아래 경우에는 머지 소트와 선형 탐색을 사용하였다.
class MyAlgorithm2
{
...
private:
void input() { cout << "Input" << endl; }
void mergeSort() { cout << "Merge Sort" << endl; }
void linearSearch() { cout << "Linear Search" << endl; }
void output() { cout << "Output" << endl; }
};
전체 코드는 다음과 같다.
#include <iostream>
using namespace std;
class MyAlgorithm1
{
public:
void run()
{
input();
quickSort();
binarySearch();
output();
}
private:
void input() { cout << "Input" << endl; }
void quickSort() { cout << "Quick Sort" << endl; }
void binarySearch() { cout << "Binary Search" << endl; }
void output() { cout << "Output" << endl; }
};
class MyAlgorithm2
{
public:
void run()
{
input();
mergeSort();
linearSearch();
output();
}
private:
void input() { cout << "Input" << endl; }
void mergeSort() { cout << "Merge Sort" << endl; }
void linearSearch() { cout << "Linear Search" << endl; }
void output() { cout << "Output" << endl; }
};
int main()
{
MyAlgorithm1* al1 = new MyAlgorithm1();
MyAlgorithm2* al2 = new MyAlgorithm2();
cout << "Algorithm 1" << endl;
al1->run();
cout << "\nAlgorithm 2" << endl;
al2->run();
return 0;
}
템플릿 메서드 패턴 적용
MyAlgorithm1, 2 모두 run 메서드 내에서 동일하게 input → sort → search → output 과정을 거친다.
sort와 search는 알고리즘마다 다르므로 virtual로 선언하고,
템플릿 메서드(= run)에서 위의 과정(알고리즘 구조)는 추상화가 가능하다.
sort와 search만 각 알고리즘에서 구체적으로 구현할 수 있도록 virtual로 선언한다.
템플릿 메서드는 알고리즘 구조를 유지하기 때문에 final 키워드로 확장이 불가능하도록 한다.
템플릿 메서드를 적용한 코드는 다음과 같다.
#include <iostream>
using namespace std;
class AlgorithmTemplate
{
public:
virtual ~AlgorithmTemplate() = default;
virtual void run() final // template method 는 재정의 불가
{
input();
sort();
search();
output();
}
protected:
void input() { cout << "Input" << endl; }
virtual void sort() = 0;
virtual void search() = 0;
void output() { cout << "Output" << endl; }
};
class MyAlgorithm1 : public AlgorithmTemplate
{
public:
void sort() override { cout << "Quick Sort" << endl; }
void search() override { cout << "Binary Search" << endl; }
};
class MyAlgorithm2 : public AlgorithmTemplate
{
public:
void sort() override { cout << "Merge Sort" << endl; }
void search() override { cout << "Linear Search" << endl; }
};
int main()
{
MyAlgorithm1* al1 = new MyAlgorithm1();
MyAlgorithm2* al2 = new MyAlgorithm2();
cout << "Algorithm 1" << endl;
al1->run();
cout << "\nAlgorithm 2" << endl;
al2->run();
return 0;
}
Hook
템플릿 메서드를 가지고 있는 추상 클래스는 기본적인 메서드(input, output)와
서브 클래스에서 구현해야 하는 메서드(sort, search)로 나뉜다.
여기에 구상 메서드 hook()을 추가하여 알고리즘 구조를 변경할 수 있다.
여기서 templateMethod는 고수준 구성요소이고, 추상화를 통해 저수준 구성요소를 호출한다. (헐리우드 원칙)
예를 들어 checkSort(= hook)가 false인 경우에 sort를 사용하지 않도록 구조를 변경해 보자.
class AlgorithmTemplate
{
public:
virtual ~AlgorithmTemplate() = default;
virtual void run() final // template method 는 재정의 불가
{
input();
if (checkSort()) sort();
search();
output();
}
protected:
virtual bool checkSort() { return true; }
...
};
예를 들어 MyAlgorithm2는 선형 탐색을 하기 때문에 정렬을 할 필요가 없다.
따라서 checkSort를 false로 재정의하여 다음과 같이 알고리즘 과정에서 sort를 제거할 수 있다.
class MyAlgorithm2 : public AlgorithmTemplate
{
public:
bool checkSort() override { return false; } // hook
void sort() override { cout << "Merge Sort" << endl; }
void search() override { cout << "Linear Search" << endl; }
};
전체 코드는 다음과 같다.
#include <iostream>
using namespace std;
class AlgorithmTemplate
{
public:
virtual ~AlgorithmTemplate() = default;
virtual void run() final // template method 는 재정의 불가
{
input();
if (checkSort()) sort();
search();
output();
}
protected:
void input() { cout << "Input" << endl; }
virtual bool checkSort() { return true; }
virtual void sort() = 0;
virtual void search() = 0;
void output() { cout << "Output" << endl; }
};
class MyAlgorithm1 : public AlgorithmTemplate
{
public:
void sort() override { cout << "Quick Sort" << endl; }
void search() override { cout << "Binary Search" << endl; }
};
class MyAlgorithm2 : public AlgorithmTemplate
{
public:
bool checkSort() override { return false; } // hook
void sort() override { cout << "Merge Sort" << endl; }
void search() override { cout << "Linear Search" << endl; }
};
int main()
{
MyAlgorithm1* al1 = new MyAlgorithm1();
MyAlgorithm2* al2 = new MyAlgorithm2();
cout << "Algorithm 1" << endl;
al1->run();
cout << "\nAlgorithm 2" << endl;
al2->run();
return 0;
}
Algorithm2에서 sort가 실행되지 않은 것을 알 수 있다.
클래스 정렬하기
정렬 또한 일종의 템플릿 메서드 패턴으로 볼 수 있다.
정렬은 각 객체를 비교(compare = hook)하고, 비교 조건에 따라 swap하기 때문이다.
상품 객체는 상품의 이름과 가격을 멤버로 가지고 있다.
class Product
{
public:
Product(string n, int p) : name(n), price(p) { }
int getPrice() { return price; }
string getName() { return name; }
private:
string name;
int price;
};
상품의 목록을 생성해서 정렬해 보자.
vector<Product> products = {
Product("p1", 3000),
Product("p2", 2000),
Product("p3", 1000),
Product("p4", 7000),
Product("p5", 9000),
};
상품을 정렬해도 컴파일 에러가 발생한다.
어떤 상품 클래스가 작고 큰지 알 수 없기 때문이다.
sort(products.begin(), products.end());
따라서 상품에 대한 비교 연산 operator를 재정의 해야 한다.
아래는 가격이 작은 순으로 정렬되도록 재정의하였다.
bool operator<(Product& lhs, Product& rhs)
{
return lhs.getPrice() < rhs.getPrice();
}
또는 compare 함수를 직접 만들어서 sort에 추가해도 된다.
compare 함수는 가격이 큰 순으로 정렬되도록 하였다.
bool compare(Product& lhs, Product& rhs)
{
return lhs.getPrice() > rhs.getPrice();
}
int main()
{
...
sort(products.begin(), products.end(), compare);
return 0;
}
아래 코드를 실행하면 정렬이 정상적으로 이루어지는 것을 알 수 있다.
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
class Product
{
public:
Product(string n, int p) : name(n), price(p) { }
int getPrice() { return price; }
string getName() { return name; }
private:
string name;
int price;
};
bool operator<(Product& lhs, Product& rhs)
{
return lhs.getPrice() < rhs.getPrice();
}
bool compare(Product& lhs, Product& rhs)
{
return lhs.getPrice() > rhs.getPrice();
}
void display(const vector<Product>& products)
{
for (auto p : products)
cout << p.getName() << "] price : " << p.getPrice() << endl;
}
int main()
{
vector<Product> products = {
Product("p1", 3000),
Product("p2", 2000),
Product("p3", 1000),
Product("p4", 7000),
Product("p5", 9000),
};
display(products); cout << endl;
sort(products.begin(), products.end());
display(products); cout << endl;
sort(products.begin(), products.end(), compare);
// or lambda
// sort(products.begin(), products.end(),
// [](Product &lhs, Product &rhs) { return lhs.getPrice() > rhs.getPrice(); });
display(products); cout << endl;
return 0;
}
ClassDiagram.md
```mermaid
classDiagram
class AlgorithmTemplate{
run()
input()
sort()*
search()*
output()
}
class ConcreteAlgorithmA {
sort()
search()
}
class ConcreteAlgorithmB {
sort()
search()
}
AlgorithmTemplate <|-- ConcreteAlgorithmA
AlgorithmTemplate <|-- ConcreteAlgorithmB
```
```mermaid
classDiagram
class AbstractClass {
templateMethod()
primitiveStep1()
hook()
concreteStep2()*
concreteStep3()*
primitiveStep4()
}
class ConcreteClassA {
concreteStep2()
concreteStep3()
}
class ConcreteClassB {
concreteStep2()
concreteStep3()
}
AbstractClass <|-- ConcreteClassA
AbstractClass <|-- ConcreteClassB
note for anything "if(hook() == true) concreteStep2();"
```