템플릿 메소드 패턴
Last updated
Last updated
알고리즘의 골격을 정의합니다. 알고리즘의 일부 단계를 서브클래스에서 구현할 수 있고, 알고리즘의 구조는 그대로 유지하면서 알고리즘의 특정 단계를 서브클래스에서 재정의 할 수도 있습니다.
커피와 홍차는 만드는 방법이 유사합니다.
대략적으로 코드로 구현한다면 아래와 같을 겁니다.
생각해보면 커피와 홍차 클래스를 더 추상화할 수 있을 것 같습니다. 커피를 우려내나 홍차 티백을 우려내나 어차피 우려내는 것이고, 설탕과 우유를 추가하나 레몬을 추가하나 어차피 첨가물인 무엇인가를 추가하는 것입니다.
기존에 달랐던 메소드를 삭제하고 brew(), addCondiments()으로 수정합니다.
그리고 각 클래스에 다르게 구현해야 하는 brew(), addCondiments() 메소드를 추상메소드로 선언합니다.
여기서 원래는 final func prepareRecipe() 로 지정해서 오버라이드를 방지해야 하나, Swift는 protocol 에서 final 키워드를 사용하지 못하므로 제외합니다. 서브클래스에서 오버라이드가 가능할 수 있습니다.
알고리즘의 템플릿(틀)을 만듭니다. 틀은 그저 여러 단계로 알고리즘을 정의한 메소드이죠.
여러 단계 가운데 하나 이상의 단계가 추상 메소드로 정의되고, 서브클래스에서 구현됩니다.
템플릿 메소드 패턴 적용 전
Coffee와 Tea 클래스가 각자 알고리즘을 수행합니다.
Coffee와 Tea 클래스에 중복된 코드가 있습니다.
알고리즘이 바뀌면 서브클래스를 열어서 하나하나 다 고쳐야 합니다.
알고리즘 지식과 구현 방법이 여러 클래스에 분산되어 있습니다.
템플릿 메소드 패턴 적용 후
CaffeineBeverage 클래스에서 작업을 처리합니다. 알고리즘을 독점하죠.
Coffee와 Tea 클래스에 중복된 코드가 없습니다.
추상메소드를 제외한 공통 부분이 바뀌면 인터페이스 한 군데만 고치면 됩니다.
CaffeineBeverage 클래스에 알고리즘 지식이 집중되어 있고, 일부 구현만 서브클래스에 의존합니다.
후크는 추상 클래스에서 선언되지만, 기본적인 내용만 구현되어 있거나 아무 코드도 들어있지 않은 메소드 입니다.
알고리즘에서 메소드가 선택이라면 후크를 쓰면 되고, 필수라면 추상 메소드를 쓰면 될 겁니다.
후크를 사용하면 상황에 따라 알고리즘 진행을 변경할 수 있습니다. 아래 코드에서는 서브클래스가 추상 클래스에서 진행되는 작업을 처리할지 말지 결정하게 하는 기능을 부여하는 용도로 쓰였습니다.
템플릿 메소드에서는 알고리즘의 단계를 너무 자잘하게 쪼개놓으면 코드가 복잡해지고, 그렇다고 너무 큼직하게 쪼개놓으면 유연성이 떨어질 수 있습니다. 여기서 필수가 아닌 부분은 후크로 구현하면 코드가 조금 줄어들 수도 있을테니 잘 생각하고 설계를 해야 합니다.
먼저 연락하지 마세요. 저희가 연락 드리겠습니다.
저수준 구성요소가 시스템에 접속할 수는 있지만, 그 구성요소를 언제/어떻게 사용할 지는 고수준 구성요소가 결정합니다.
할리우드 원칙을 활용하며 의존성 부패(dependency rot)을 방지할 수 있습니다.
의존성 부패(dependency rot)란, 어떤 고수준 구성요소가 저수준 구성요소에 의존하고, 그 저수준 구성요소는 다시 고수준 구성요소에 의존하고, 그 고수준 구성요소는 또 다른 구성요소에 의존하고, 그 또 다른 구성요소는 또 저수준 구성요소에 의존하고...와 같이 의존성이 복잡하게 꼬여있는 상황을 말합니다.
이렇게 되버리면, 시스템이 어떻게 디자인 되어 있는지 아무도 알아볼 수 없게 되죠.
할리우드 원칙은 저수준 구성요소가 시스템에 접속할 수는 있지만, 그 구성요소를 언제/어떻게 사용할 지는 고수준 구성요소가 결정합니다.
템플릿 메소드 패턴을 사용하면, 할리우드 원칙을 지키게 됩니다.
CaffeineBerverage는 고수준 구성요소입니다. 음료를 만드는 방법에 해당하는 알고리즘을 독점하고 있죠(final로 오버라이드가 불가능하다는 전제하에). 그리고 각 서브클래스에서 메소드 구현이 필요한 경우에만 서브클래스를 불러냅니다.
우리가 구현한 코드와 아래 그림을 예시로 들면, 저수준 구성요소인 Coffee/Tea 서브클래스는 특화된 메소드 구현을 제공하는데만 쓰입니다. 고수준 구성요소인 CaffeineBerverage에서 호출 "당하기" 전까지는 추상 클래스를 직접 호출하지 않죠.
사실 위에 있는 코드에서도 boilWater(), pourInCup()과 같은 코드를 호출할 수 없는 것은 아니죠. 하지만, 이러면 순환 의존성이 생겨버리는 거죠. 그래서 호출 "당하는"게 중요합니다.
템플릿 메소드 패턴 말고도 팩토리 메소드, 옵저버 패턴이 할리우드 원칙을 준수합니다.
의존성 뒤집기 원칙과 할리우드 원칙
DIP는 될 수 있으면 구상 클래스를 줄이고 추상화된 인터페이스를 사용하라는 원칙이죠. 할리우드 원칙이나 DIP나 객체를 분리한다는 목표는 공유하지만, 의존성을 피하는 방법에 있어서 DIP가 훨씬 더 강하고 일반적인 내용을 담고 있습니다. 할리우드 원칙은 저수준 구성요소를 다양하게 사용할 수 있으면서도, 다른 클래스가 구성요소에 너무 의존하지 않게 만들어 주는 디자인 구현 기법을 제공합니다.
변형된 템플릿 메소드가 있답니다. Swift에서 기본적으로 제공해주는 sort() 같은 것들인데요.
음, 무슨 말이냐면 우리가 위에서 구현한 코드의 템플릿 메소드는 prepareRecipe() 였죠? 얘는 final로 되어 있어 우리는 절대로 수정할 수 없었어요. 오버라이드가 불가능하니까. (다른 언어에서는 말이죠.)
그리고, Coffee/Tea 서브클래스에서 특화된 메소드를 구현해줬죠. 이것을 아래의 코드와 대응을 해보면 됩니다.
swift 에서 class/struct/enum 등의 대소비교를 위해서는 Comparable 프로토콜을 준수해야 하죠. 객체를 비교할 건데, 객체의 어떤 속성을 비교해야 하는지 알려줘야 하니까요.
Comparable은 대소비교를 위한 거고, Comparable이 준수하고 있는 Equatable도 준수되어야 합니다. 즉, static func < (), static func == () 이 2가지 메소드는 우리가 만드는 서브 클래스에서 구현을 해줘야 된다는 거죠.
이런 부분이 sort()가 템플릿 메소드 라는 것입니다. sort()는 알고리즘이고 내부에는 mergeSort(), swap() 같은 내부 로직이 있겠죠? 우리가 구현한 prepareRecipe() 처럼요. 그 내부 로직에 static func < (), static func == () 도 있는겁니다. 대소 비교나 같은지 여부를 판단해야 정렬을 할 수 있을테니까요.
템플릿 메소드 패턴(Template Method Pattern)
알고리즘의 개요를 정의하는 일을 한다. 진짜 작업 중 일부는 밑에 있는 서브클래스에서 처리한다. 알고리즘의 각 단계에서 다른 구현을 사용하면서도 알고리즘의 구조 자체는 유지할 수 있다.
알고리즘을 구현할 때 상속을 사용한다.
알고리즘을 꽉 잡고 있고, 코드 중복도 거의 없다. 알고리즘이 전부 똑같고 코드 한 줄만 다르다면, 템플릿 메소드 패턴이 전략 패턴보다 효율적이다. 중복되는 코드는 슈퍼클래스에 들어있으니 서브 클래스도 공유할 수 있다.
상대적으로 의존성이 크다. 알고리즘의 일부를 슈퍼클래스에서 구현한 메소드에 의존해야 한다.
전략 패턴(Strategy Pattern)
일련의 알고리즘군을 정의하고 그걸 바꿔가면서 쓸 수 있게 해준다. 알고리즘은 캡슐화 되어 있어 손쉽게 서로 다른 알고리즘을 사용할 수 있다.
알고리즘을 구현할 때 클라이언트에게 구성으로 구현 할지 말지 선택할 기회를 준다.
객체 구성을 사용해서 더 유연하다. 클라이언트에서 다른 전략 객체를 사용해서 알고리즘 변경도 가능하다.
상대적으로 의존성이 적다. 어떤 것에도 의존하지 않는다. 알고리즘이 캡슐화 되어있기 때문.
https://www.hanbit.co.kr/store/books/look.php?p_code=B6113501223