티스토리 뷰
1. 템플릿 메소드 패턴
오늘 포스팅하는 템플릿 메소드 패턴은 우리가 모르게 정말 많이 사용하고 있는 패턴입니다.
1) 정의
메소드에서 알고리즘의 골격을 정의하는 패턴. 알고리즘의 여러 단계 중 일부는 서브 클래스에서 구현할 수 있다. 템플릿 메소드를 이용하면 알고리즘의 구조는 그대로 유지하면서 서브 클래스에서 특정 단계를 재정의할 수 있다. (출처 : 헤드퍼스트 디자인 패턴)
간단하게 말하면 알고리즘의 틀(Template)을 만들기 위한 것입니다.
전체 틀은 동일하고, 바뀌는 부분만 서브클래스에서 재정의해준다면, 실제로 코드의 중복을 크게 줄일 수 있을 겁니다.
이 템플릿 메소드 패턴의 가장 흔하고 간단한 예는, std::sort의 compare 함수입니다. sort를 기본 틀이 아닌 원하는 기준으로 하고 싶다면, compare 함수를 따로 구현하여 sorting을 원하는 기준으로 실행시킬 수 있습니다.
구현코드를 통해 아래의 패턴을 더 정확하게 설명해 보겠습니다.
2. 코드의 구현
제가 구현한 코드는 커피와 차에 관련된 코드입니다.
커피를 내리는 과정과 차를 내리는 과정은 상당히 흡사합니다.
물을 끓이고, 커피/차를 내리고, 이를 컵에 옮겨 담고, 손님이 추가 재료를 요구한다면 추가 재료를 담아주는 알고리즘을 통해 커피/차가 고객에게 전달됩니다. 단지 다른 부분이 있다면, 커피에선 커피를 내리고, 차에선 차를 내린다거나, 추가로 넣을 수 있는 재료가 달라질 순 있을 겁니다.
위의 레시피를 준비하는 알고리즘은 그대로 둔 채, 차, 혹은 커피를 내리는 함수와 재료를 추가하는 알고리즘을 바꾸어 보려 합니다.
<CCaffeineBeverage,h>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | enum EUserRequest { REQUEST_POSITIVE = 0, REQUEST_NEGATIVE = 1, REQUEST_INVALID = 2 }; class CCaffeineBeverage { public: CCaffeineBeverage(): bPutCondiment(false){}; ~CCaffeineBeverage(){}; virtual void PrepareRecipe() final; void BoilWater(); virtual void Brew() = 0; void PourInCup(); virtual void AddCondiments() = 0; EUserRequest GetUserInput(); bool bPutCondiment; }; | cs |
PrepareRecipe() 는 밑의 서브클래스에서 다시 정의될 수 없도록 final을 붙여놓았습니다. 저 함수가 바로 커피와 차의 준비과정의 알고리즘이므로, 서브클래스에서 다시 재정의하면 템플릿메소드 패턴으로서의 의미가 사라집니다.
그리고 추가 재료에 대한 UserRequest의 대답은 총 긍정/부정/잘 못 입력 총 세 가지가 될 것이기 때문에 이에 대한 enum값인 EUserRequest 를 따로 정의해 놓았습니다.
<CCaffeineBeverage.cpp>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | void CCaffeineBeverage::PrepareRecipe() { BoilWater(); Brew(); PourInCup(); AddCondiments(); } void CCaffeineBeverage::BoilWater() { printf("물 끓이는 중...\n"); } void CCaffeineBeverage::PourInCup() { if(bPutCondiment) { printf("컵에 따르는 중...\n"); } } EUserRequest CCaffeineBeverage::GetUserInput() { char userInput[2]; char yInput[2] = {'y', '\0'}; char nInput[2] = {'n', '\0'}; scanf("%s", userInput); if(!strcmp(userInput, yInput)) { return EUserRequest::REQUEST_POSITIVE; } else if(!strcmp(userInput, nInput)) { return EUserRequest::REQUEST_NEGATIVE; } else { return EUserRequest ::REQUEST_INVALID; } } | cs |
PrepareRecipe() 함수는 이 카페인 음료를 상속받는 클래스라면 전부 과정이 동일한 템플릿입니다.
그리고 상속받는 객체라면 모두 동일한 알고리즘을 거치는 함수가 있습니다. 이 함수에 대해서는 기본 클래스에서 선언합니다. 이 코드에선 BoilWater() , PourInCup() 이 위 케이스에 해당하는 함수입니다.
GetUserInput() 의 경우 User의 재료 추가를 물어보기 위해 입력받고, 입력받는 것이 대한 대답을 판단해주는 함수입니다. 대답에 따라 판단하여 적절한 Enum값을 반환시켜 줍니다.
<CCoffee.h>
1 2 3 4 5 6 7 | class CCoffee : public CCaffeineBeverage{ public: CCoffee(){}; ~CCoffee(){}; void Brew(); void AddCondiments(); }; | cs |
커피 클래스입니다.
위의 CCaffeineBeverage 에서 미리 정의했던 함수에 대한 추가 구현은 할 필요가 없고, 변하는 함수에 대해서만 따로 선언하고, 하는 기능에 대해 정의시켜줍니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | void CCoffee::Brew() { printf("커피를 내리는 중...\n"); } void CCoffee::AddCondiments() { printf("커피에 우유와 설탕을 넣어드릴까요? (y/n) : "); EUserRequest userRequest = GetUserInput(); if( userRequest == EUserRequest::REQUEST_POSITIVE ) { printf("설탕, 우유 추가 중...\n"); } else if( userRequest == EUserRequest::REQUEST_NEGATIVE ) { printf("컨디먼트를 추가하지 않습니다.\n"); } else if( userRequest == EUserRequest::REQUEST_INVALID) { printf("IO 오류.\n"); } else { printf("UserInput value is invalid.\n"); } } | cs |
커피를 내리는 Brew() 함수와 AddCondiments() 함수에 대한 구현을 해 놓았습니다.
이 구현 방식은 Tea 객체에 대해서도 동일합니다.
<CTea.h>
1 2 3 4 5 6 7 | class CTea : public CCaffeineBeverage{ public: CTea(){}; ~CTea(){}; void Brew(); void AddCondiments(); }; | cs |
<CTea.cpp>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | void CTea::Brew() { printf("차를 우려내는 중...\n"); } void CTea::AddCondiments() { printf("차에 레몬을 추가해 드릴까요? (y/n) : "); EUserRequest userRequest = GetUserInput(); if( userRequest == EUserRequest::REQUEST_POSITIVE ) { printf("레몬 추가 중...\n"); } else if( userRequest == EUserRequest::REQUEST_NEGATIVE ) { printf("컨디먼트를 추가하지 않습니다.\n"); } else if( userRequest == EUserRequest::REQUEST_INVALID) { printf("IO 오류.\n"); } else { printf("UserInput value is invalid.\n"); } } | cs |
이를 실행시켜 보면,
<main.cpp>
1 2 3 4 5 6 7 8 9 10 | int main() { CCoffee *pCoffee = new CCoffee(); pCoffee->PrepareRecipe(); CTea *pTea = new CTea(); pTea->PrepareRecipe(); return 0; } | cs |
MacBook-Pro:cmake-build-debug$ ./TemplateMethod 물 끓이는 중... 커피를 내리는 중... 커피에 우유와 설탕을 넣어드릴까요? (y/n) : y 설탕, 우유 추가 중... 물 끓이는 중... 차를 우려내는 중... 차에 레몬을 추가해 드릴까요? (y/n) : y 레몬 추가 중... | cs |
이렇게 실행이 되는 것을 확인할 수 있습니다.
3. 할리우드 원칙
- 먼저 연락하지 마세요. 저희가 연락 드리겠습니다.
헐리우드에서 배우들하고 연락하는 것과 비슷하게 수퍼클래스에서 모든 것을 관리하고 필요한 서브클래스를 불러서 써야 한다는 원칙입니다.
이 할리우드 원칙이 지켜지지 않으면, 시스템이 어떻게 디자인된 것인지 아무도 알아볼 수 없게 됩니다.
앞에서 구현한 커피와 차 이야기를 예로 설명해 보겠습니다.
CCaffeineBeverage 는 수퍼클래스입니다. 여기서 핵심 알고리즘인 PrepareRecipe() 를 관리하고 있습니다. 그리고 이 알고리즘에 대해 자질구레한 구현이 필요할 때 서브클래스에서 구현을 시키고 있습니다.
상속받는 클래스들도 CCaffeineBeverage 에 추상화된 부분에 의존하게 됩니다.
템플릿메소드 패턴 말고도 팩토리패턴, 옵저버패턴 등이 이 할리우드 원칙을 지키고 있는 디자인 패턴입니다.
'Design Pattern' 카테고리의 다른 글
디자인 패턴 - 7. Facade Pattern (0) | 2018.10.15 |
---|---|
디자인 패턴 - 6. Adapter Pattern (0) | 2018.10.12 |
디자인 패턴 - 5. Command Pattern (0) | 2018.10.09 |
디자인 패턴 - 4. Singleton Pattern (0) | 2018.10.08 |
디자인 패턴 - 2. Observer Pattern (1) | 2018.09.16 |