티스토리 뷰

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 에 추상화된 부분에 의존하게 됩니다. 

템플릿메소드 패턴 말고도 팩토리패턴, 옵저버패턴 등이 이 할리우드 원칙을 지키고 있는 디자인 패턴입니다.





최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/03   »
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
글 보관함