C++로 템플릿 패턴 이해하기

Kang Chang Hwan·2024년 6월 10일

CS - Design-Pattern

목록 보기
6/6

커피와 홍차 이야기

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

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

즉, 이렇게 되면 중복되는 코드(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. Subclass에게 위임하는 메서드(순수 가상 함수로 선언된 primitiveOperation( ))
  2. 추상 클래스에서 정의된 concreteOperation
  3. hook()

내용을 읽어 오면 1, 2는 이해가 됐지만 3은 모를 거다.

hook 이란?

추상 클래스에 정의되어 있는 정의가 비었거나 아주 간단한 메서드다. 이는 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;
  }
};
  1. 우리가 키오스크에서 주문을 하듯이 "우유랑 설탕을 추가하시겠어요?" 라고 물어본 후 고객에게 답변을 받는다.
  2. 그 답변을 input에 저장해뒀다가 소문자로 변환하고 대답이 y로 시작하는지 체크한다.
  3. 아니라면 false가 반환되고 맞다면 true가 반환된다.
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이라고 하는데, 이는 다른 편에서 다루도록 한다.

Hollywood 원칙

"나한테 전화 걸지마세요. 제가 드릴텡께"

위 그림과 같이 High level 구성 요소가 low-level 구성 요소를 제어하고 low-level component는 직접적으로 호출할 수 없다는 원칙이 할리우드 원칙이다.

이게 템플릿 메소드랑 무슨 관련이 있을까?

템플릿 메서드와 할리우드 원칙

  • High-level-component(Beverage):

    • High level component로 알고리즘을 관리함.
    • 알고리즘(prepareRecipe)안에서 필요할 때 subclass의 steep이나 AddCondiment를 호출함.
  • Low-level-component(Coffee and Tea):

    • 먼저 Beverage를 호출하지 않음!
    • 음료 인터페이스에서 호출 시에만 호출됨.

이렇게 음료가 "내가 전화할 때만 받어 먼저 전화하지 마쇼!" 를 구현할 수 있는데 이를 할리우드 원칙이라고 한다.

Q1. 할리우드 원칙과 의존성 뒤집기 원칙

Q2. 저수준 구성 요소에서는 고수준 구성 요소에 있는 메서드를 호출할 수 없는 건가?

전략(Strategy) 패턴과의 차이

  • 템플릿 메소드 패턴은 어떤 문제를 해결하기 위한 알고리즘을 하나의 메서드로 캡슐화하고 이를 통해 반복되는 메서드들을 줄일 수 있음.
    • 전략 패턴과의 차이점은 알고리즘을 담은 메서드 안에서 필요한 경우, 실제 객체에게 메서드 호출을 위임한다는 것!
  • 전략 패턴은 객체 사이에 반복되는 행동이 있을 시 이를 하나로 묶어서 인터페이스를 만들고 이를 구현한 실제 행동 객체에게 실제 객체가 행동을 위임하는 형태.
    • 템플릿 메소드 패턴과의 차이점은 템플릿 메소드는 상속을 통해 자식 클래스가 필요한 기능을 확장하고 전략 패턴은 구성(Composition)을 통해 행동을 위임함.
profile
아쉬움 없이 살자. 모든 순간을 100%로!

0개의 댓글