전체 페이지뷰

2017년 3월 8일 수요일

Template Method Pattern

지금까지 객체 생성, 메소드 호출, 복잡한 인터페이스 등 여러가지 것들을 캡슐화 해 봤습니다. 이번에는 알고리즘을 캡슐화 하는 패턴에 대해 배워보고자 합니다.
커피와 홍차를 만드는 과정을 살펴보는 것으로 이번 장을 시작합니다.

커피를 만드는 과정을 코드로 나타내면 다음과 같습니다.
public class Coffee
{
    void PrepareRecipe()
    {
        BoilWater();
        BrewCoffeeGrinds();
        PourInCup();
        AddSugarAndMilk();
    }
    public void BoilWater()
    {
        Console.WriteLine("물 끓이는 중");
    }
    public void BrewCoffeeGrinds()
    {
        Console.WriteLine("커피 추출 중");
    }
    public void PourInCup()
    {
        Console.WriteLine("컵에 따르는 중");
    }
    public void AddSugarAndMilk()
    {
        Console.WriteLine("설탕과 우유 첨가");
    }
}
cs

홍차는 어떻습니까?
public class Tea
{
    void PrepareRecipe()
    {
        BoilWater();
        SteepTeaBag();
        PourInCup();
        AddLemon();
    }
    public void BoilWater()
    {
        Console.WriteLine("물 끓이는 중");
    }
    public void SteepTeaBag()
    {
        Console.WriteLine("차 우려내는 중");
    }
    public void PourInCup()
    {
        Console.WriteLine("컵에 따르는 중");
    }
    public void AddLemon()
    {
        Console.WriteLine("레몬 첨가 중");
    }
}
cs

상당 부분의 과정이 중복되는 것을 알수가 있습니다. 이것을 개선하기 위해 추상화의 도움을 받습니다. 얼핏 다음과 같은 방식의 추상화가 떠오릅니다.
PrepareRecipe()는 서브클래스마다 내용이 다르므로 추상메소드로 선언하고, 겹치는 BoilWater(), PourInCup()을 수퍼클래스에서 정의합니다. 그리고, 각 서브 메소드에서 만드는 방법을 구현하면서 PrepareRecipe()를 오버라이드 하는 방식입니다.

그러나 자세히 살펴보면 좀더 공통점을 찾을 수 있습니다. 일단 두 가지 음료의 만드는 방법은,

  1. 물을 끓인다.
  2. 뜨거운 물을 이용하여 커피 또는 홍차를 우려낸다. 
  3. 만들어진 음료를 컵에 따른다.
  4. 각 음료에 맞는 첨가물을 추가한다.

1,3번은 이미 베이스 클래스에 추상화 되어 있습니다. 그리고 2,4번은 추상화되지 않았지만 결국은 두 음료에서 모두 같은 과정이라고 할 수 있습니다. 그렇다면 이제 PrepareRecipe()를 추상화할 수 있을 것 같습니다.

PrepareRecipe() 추상화


두 음료의 PrepareRecipe() 메소드를 살펴봅시다.

            커피
void PrepareRecipe()
{
    BoilWater();
    BrewCoffeeGrinds();
    PourInCup();
    AddSugarAndMilk();
}

            홍차
void PrepareRecipe()
{
    BoilWater();
    SteepTeaBag();
    PourInCup();
    AddLemon();
}

BrewCoffeeGrinds()SteepTeaBag()의 과정은 우려낸다는 점에서 본질적으로 동일합니다. 그리고 AddSugarAndMilk()AddLemon()역시 첨가물을 넣는다는 점에서 동일하죠.

이제 전자를 Brew(), 후자를 AddCondiments()라는 메소드로 통일하도록 하겠습니다. 그것을 위해 먼저 두 음료의 수퍼클래스인 CaffeineBeverage부터 구현합니다.
public abstract class CaffeineBeverage
{
    public void PrepareRecipe()
    {
        BoilWater();
        Brew();
        PourInCup();
        AddCondiments();
    }
    abstract public void Brew();
    abstract public void AddCondiments();
    void BoilWater()
    {
       Console.WriteLine("물 끓이는 중");
    }
            
    void PourInCup()
    {
        Console.WriteLine("컵에 따르는 중");
    }
}
cs

PrepareRecipe()안의 내용을 위에 말한대로 Brew()AddCondiments()를 넣어 고쳐주었고, 커피와 홍차가 각각 Brew()AddCondiments()를 구현하도록 추상메소드로 선언합니다. 고칠 필요가 없는 BoilWater()PourInCup()은 직접 구현해주었습니다.

이제 Coffee와 Tea 클래스를 손봅니다. 바뀌는 부분만 구현해주면 됩니다.
public class Coffee : CaffeineBeverage
{
    public override void Brew()
    {
        Console.WriteLine("필터로 커피를 우려내는 중");
    }
    public override void AddCondiments()
    {
        Console.WriteLine("설탕과 커피를 첨가하는 중");
    }
}
public class Tea : CaffeineBeverage
{
    public override void Brew()
    {
        Console.WriteLine("차를 우려내는 중");
    }
    public override void AddCondiments()
    {
        Console.WriteLine("레몬을 첨가하는 중");
    }
}
cs

이제 클래스 다이어그램은 다음과 같습니다.

지금까지 한 과정이 바로 템플릿 메소드 패턴이라고 할 수 있습니다. 템플릿은 여러 의미로 쓰이지만 "무엇인가를 만들 때 안내역할을 하는 틀"이라는 뜻으로 쓰입니다. 빈 칸을 채워넣기만 하면되는 서식 같은 것입니다.

위의 코드 중 PrepareRecipe()가 바로 템플릿 메소드입니다. 차를 만드는 알고리즘에 대한 템플릿 역할을 하기 때문이죠. 알고리즘의 각 단계를 메소드로 표현하고 어떤것은 직접, 어떤 것은 서브클래스에서 처리하게 합니다.


  • 템플릿 메소드 : 알고리즘의 골격을 정의하며, 알고리즘의 여러 단계 중 일부는 서브 클래스에서 구현할 수 있습니다. 템플릿 메소드를 이용하면 알고리즘의 구조는 그대로 유지하면서 서브클래스에서 특정 단계를 재정의할 수 있습니다.

템플릿 메소드 패턴의 일반적 다이어그램은 다음과 같습니다.


템플릿 메소드와 후크

hook는 추상 클래스에서 선언되는 메소드이지만 기본 내용만 구현되어 있거나 아무 코드도 들어있지 않은 메소드입니다. 후크를 사용하여 위에 정해진 알고리즘 진행 방향을 변경시킬수 있습니다. 위의 차 만드는 알고리즘에서 손님이 첨가물을 넣을지 결정하는 것에서 후크를 사용할 수 있습니다.

먼저 후크가 들어간 추상 음료 클래스를 작성해 봅시다.

public abstract class CaffeineBeverageWithHook
{
    public void PrepareRecipe()
    {
        BoilWater();
        Brew();
        PourInCup();
        if (CustomerWantsCondiments())
            AddCondiments();
    }
    abstract public void Brew();
    abstract public void AddCondiments();
    void BoilWater()
    {
        Console.WriteLine("물 끓이는 중");
    }
    void PourInCup()
    {
        Console.WriteLine("컵에 따르는 중");
    }
    public virtual bool CustomerWantsCondiments()
    {
        return true;
    }
}
cs

AddCondiments()가 들어갈 자리에 CustomerWantsCondiments()라는 bool 타입의 메소드가 들어갔습니다. 지금은 단지 true를 반환하는 메소드이지만 서브클래스에서 오버라이드 하여(오버라이드 가능하도록 virtual 수식하였습니다) 후크로 사용할 것입니다.

이것을 상속받는 CoffeeWithHook 클래스를 작성해봅시다.
public class CoffeeWithHook : CaffeineBeverageWithHook
{
    public override void Brew()
    {
        Console.WriteLine("필터로 커피를 우려내는 중");
    }
    public override void AddCondiments()
    {
        Console.WriteLine("설탕과 우유를 첨가하는 중");
    }
    public override bool CustomerWantsCondiments()
    {
        Console.WriteLine("커피에 우유와 설탕을 넣을까요? (y/n) ");
        ConsoleKeyInfo info = Console.ReadKey();
        if (info.KeyChar.Equals('Y'|| info.KeyChar.Equals('y'))
        {
            return true;
        }
        else
        {
            return false;
        }
    }           
}
cs

CustomerWantsCondiments()를 오버라이드 하여 첨가 여부를 물어보는 코드를 추가했습니다. TeaWithHook도 마찬가지로 작성하고 테스트 해보겠습니다.

static void Main(string[] args)
{
    TeaWithHook teaHook = new TeaWithHook();
    CoffeeWithHook coffeeHook = new CoffeeWithHook();
    Console.WriteLine("\n차 준비 중...");
    teaHook.PrepareRecipe();
    Console.WriteLine("\n커피 준비 중...");
    coffeeHook.PrepareRecipe();            
}
cs

결과)

차 준비 중...
물 끓이는 중
차를 우려내는 중
컵에 따르는 중
홍차에 레몬을 넣을까요? (y/n)
y레몬을 첨가하는 중

커피 준비 중...
물 끓이는 중
필터로 커피를 우려내는 중
컵에 따르는 중
커피에 우유와 설탕을 넣을까요? (y/n)
n계속하려면 아무 키나 누르십시오 . . .

후크는 이처럼 알고리즘에서 필수적이지 않은 부분을 뺄지 말지를 물어보는 것과 같은 용도로도 쓰일 수 있고, 서브 클래스에 추상 클래스에서 진행되는 작업에 대한 결정을 내리는 기능을 부여하기 위한 용도로도 쓰일 수 있습니다.

헐리우드 원칙


먼저 연락하지 마세요. 저희가 연락 드리겠습니다.

이것은 대체 무슨 원칙일까요?

이 원칙을 사용하면 의존성 부패(dependency rot)을 방지 할 수 있습니다. 고수준 구성요소와 저수준 구성요소들이 서로에게 의존하여 심하게 꼬여있는 상태를 의존성 부패라고 부릅니다.
헐리우드 원칙을 사용하면 저수준 구성요소에서 시스템에 접속을 할 수는 있지만 언제 어떤 식으로 그 구성요소들을 사용할지를 고수준 구성요소에서 결정하게 됩니다. 고수준 구성요소는 저수준 구성요소에게 "내가 연락할 때까지는 먼저 연락하지 말라"고 하는 것이죠.

이미 앞에서 본 디자인을 다시 한번 보겠습니다.



여기서 CaffeineBeverage가 바로 고수준 구성요소 입니다. Coffee와 Tea는 메소드 구현을 제공하기 위한 용도로만 사용되며, 클라이언트는 구상 클래스가 아닌 CaffeineBeverage에 의존하게 됩니다.

이상으로 템플릿 메소드 패턴에 대한 공부를 마칩니다.

댓글 없음:

댓글 쓰기