템플릿 패턴에 대해 자료를 조사하고, 예제를 만들면서 느낀 점은 확실히 전략패턴과 많이 유사한 느낌을 가지고 있다.
팀장님의 손길이 닿아있는 프로젝트 라면 라우팅 하는 대부분의 부분은 이 템플릿 메소드 부분이 적용되어 있는데
간략한 버전의 프레임워크를 작성해보고자 한다.
템플릿 메서드 패턴 이란 ?
템플릿 메소드 패턴 은 소프트웨어 공학에서 동작 상의 알고리즘의 프로그램 뼈대를 정의하는 행위 디자인 패턴이다.
알고리즘의 구조를 변경하지 않고 알고리즘의 특정 단계들을 다시 정의할 수 있게 해 준다. - wiki-
템플릿 메서드는 부모 클래스에서 알고리즘의 골격을 정의하지만, 해당 알고리즘의 구조를 변경하지 않고 자식 클래스들이 알고리즘의 특정 단계들을 오버라이드(재정의)할 수 있도록 하는 행동 디자인 패턴입니다. -wiki-
기존 전략패턴 에 대해 공부했던 지난번의 기억을 살려보면 무언가 많이 의미가 비슷하다.
"전략패턴" : 실행 중 알고리즘을 선택할 수 있게 하는 행위라고 지난번 위키에서는 정했다. 음? 설명이 엄청 비슷하다고 느껴진다.
UML을 확인해 보자.
지난번 전략패턴을 구현하는 데 있어 우리는 객체에 Strategy 인터페이스를 주입받는 알고리즘 형태에 따라 기존 객체는 변경되는 형태를 가져갔다.
그러나 이번 템플릿 메서드 패턴은 상위 구현체 ? 에서 메소드를 사용하는데 그에 대해 상속된 객체는 해당 메소드를 오버라이드 해서 작성한다. OOP 언어 라면 위와같은 오버라이드 기능이 있겠지만? 우리 고퍼 들에게는 인터페이스 의 주입이 필요하다.
이는 코드 예제를 보면서 확인하자.
왜 템플릿 메소드 패턴이 생겼는가?
1. 여러 클래스가 거의 비슷하거나 같은 로직이나 연산을 수행하지만, 다소 차이점이 존재하는 경우
2. 중복 코드를 피하고 로직을 재사용하려는 경우
3. 알고리즘의 특정 단계를 반드시 오버라이드 해야 하는 경우
이유가 정말 명확하다. 중복되는 로직은 상위단계로 추상화하고 서로 다른 부분의 로직을 하위 상속객체에게 위임한다.
다시 말해 SOLID의 원칙 중 IOC 제어의 역전에 있어 엄청난 강점이 있다고 생각되는 부분이다. 왜?
하위 상속 혹은 오버라이드 하는 객체에서는 해당 함수의 호출 은 상위 클래스에서 전체 흐름을 제어하기 때문이다.
템플릿 메서드의 장점
1. 클라이언트들이 대규모 알고리즘의 특정 부분만 오버라이드하여 다른 부분에 변경하여 발생하는 영향을 덜 받도록 할 수 있다.
2. 중복 코드를 부모 클래스로 가져올 수 있다.
1번의 장점을 읽다 보면 어떤 개념들과 매우 유사하다. 바로 프레임워크이다. 프레임워크는 정해진 틀에 따라 우리가 작성한 코드들을 가져다 쓴다. 그래서 스프링 프레임워크를 공부하다 보면 특징 중 하나를 IOC라고 말한다. 왜? 프레임워크 이기 때문이다.
물론 프레임워크 들은 템플릿 메서드 패턴 과 팩토리 메소드 추상팩토리 가 복합적으로 적용된 복잡한 틀이겠지만, 왜 이런 프레임워크들의 특징 중 하나로 IOC 가 나오는지 이해할 수 있으면 된다고 생각된다.
예제
고 에는 다양한 웹프레임워크 가 존재하지만 자체 http 패키지도 매우 훌륭한데 이 기본 패키지를 이용해서 나만의 작은 프레임워크를 작성해 보자.
개괄적인 콜스택과 구조를 그림으로 표현했다.
http.NewServeMux를 통해 개별적인 핸들러를 가진다. 해당 핸들러는 라우팅을 커스텀하게 할 수 있고
라우팅 할 때 서비스핸들러를 이용해 각각의 요청에 따른 프로세스를 진행시켜 준다. 코드를 확인해 보자.
package template
import (
"fmt"
"log"
"net/http"
"time"
)
/**
무언가 만드는 방법이 유사하다.
*/
type ServiceTemplate interface {
IsRequireAuth() bool
GetRequest() error
GetParam() error
Process() error
}
type ServiceHandler struct {
service ServiceTemplate
}
func (s *ServiceHandler) Init(service ServiceTemplate) *ServiceHandler {
s.service = service
return s
}
func (s *ServiceHandler) ParentsFeature01() {
fmt.Println("부모의 기능01")
}
func (s *ServiceHandler) ParentsFeature02() {
fmt.Println("부모의 기능02")
}
func (s *ServiceHandler) Run(w http.ResponseWriter, r *http.Request) {
s.ParentsFeature01()
s.ParentsFeature02()
if s.service.IsRequireAuth() {
fmt.Println("인증이 필요한경우 여기서 구현")
}
if err := s.service.GetRequest(); err != nil {
fmt.Println("GetRequest 에러 처리")
}
if err := s.service.GetParam(); err != nil {
fmt.Println("GetParam 에러 처리")
}
if err := s.service.Process(); err != nil {
fmt.Println("Process 에러 처리")
}
}
type ServiceGetTest struct{}
func (s ServiceGetTest) IsRequireAuth() bool {
//TODO implement me
panic("implement me")
}
func (s ServiceGetTest) GetRequest() error {
//TODO implement me
panic("implement me")
}
func (s ServiceGetTest) GetParam() error {
//TODO implement me
panic("implement me")
}
func (s ServiceGetTest) Process() error {
//TODO implement me
panic("implement me")
}
var _ ServiceTemplate = (*ServiceGetTest)(nil)
type ServicePostTest struct{}
func (s ServicePostTest) IsRequireAuth() bool {
//TODO implement me
panic("implement me")
}
func (s ServicePostTest) GetRequest() error {
//TODO implement me
panic("implement me")
}
func (s ServicePostTest) GetParam() error {
//TODO implement me
panic("implement me")
}
func (s ServicePostTest) Process() error {
//TODO implement me
panic("implement me")
}
var _ ServiceTemplate = (*ServicePostTest)(nil)
type MyContext struct {
mux *http.ServeMux
service *ServiceHandler
}
func (m *MyContext) ServeHTTP(w http.ResponseWriter, r *http.Request) {
m.mux.ServeHTTP(w, r)
}
func (m *MyContext) InitRouting() {
m.mux.HandleFunc("/get", m.service.Init(ServiceGetTest{}).Run)
m.mux.HandleFunc("/post", m.service.Init(ServicePostTest{}).Run)
}
func ProcessService() {
myHandler := &MyContext{
mux: http.NewServeMux(),
service: &ServiceHandler{},
}
http.NewServeMux()
myHandler.InitRouting()
s := &http.Server{
Addr: ":9300",
Handler: myHandler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())
}
ServiceTemplate라는 인터페이스를 정의하고, IsRequireAuth, GetRequest, GetParam, Process 같은 메서드들이 있다.
이들은 템플릿 메소드 패턴에서 "템플릿 메서드"의 단계들이라 할 수 있다.
type ServiceTemplate interface {
IsRequireAuth() bool
GetRequest() error
GetParam() error
Process() error
}
ServiceHandler 타입은 ServiceTemplate 인터페이스를 멤버로 갖는 구조체다. ServiceHandler는 또한 초기화를 위한 Init 메서드와 ParentsFeature01, ParentsFeature02 (공통 기능을 구현하는 몇 가지 메서드), 그리고 Run 메서드를 가진다.
Run 메서드는 결국 템플릿 알고리즘을 실행하며, 각각의 인터페이스 메서드들을 호출한다.
type ServiceHandler struct {
service ServiceTemplate
}
func (s *ServiceHandler) Init(service ServiceTemplate) *ServiceHandler {
s.service = service
return s
}
func (s *ServiceHandler) ParentsFeature01() {
fmt.Println("부모의 기능01")
}
func (s *ServiceHandler) ParentsFeature02() {
fmt.Println("부모의 기능02")
}
func (s *ServiceHandler) Run(w http.ResponseWriter, r *http.Request) {
s.ParentsFeature01()
s.ParentsFeature02()
if s.service.IsRequireAuth() {
fmt.Println("인증이 필요한경우 여기서 구현")
}
if err := s.service.GetRequest(); err != nil {
fmt.Println("GetRequest 에러 처리")
}
if err := s.service.GetParam(); err != nil {
fmt.Println("GetParam 에러 처리")
}
if err := s.service.Process(); err != nil {
fmt.Println("Process 에러 처리")
}
}
여기서 Init 은 포인터 자체를 반환하는데 빌더패턴의 체이닝 방식을 적용하기 위해서 위와 같이 선언하였다.
func (m *MyContext) InitRouting() {
m.mux.HandleFunc("/get", m.service.Init(ServiceGetTest{}).Run)
m.mux.HandleFunc("/post", m.service.Init(ServicePostTest{}).Run)
}
이렇게 간단한 예제를 작성했다.
전략패턴과 템플릿메서드 패턴의 명확한 차이가 여기서 드러난다. 또한 템플릿과 팩토리 패턴 의 밀접한 연관관계를 느낄 수도 있다.
추상클래스 가 없는 고에서는 인터페이스를 임베딩 해서 사용해야 하는데 상당 부분이 팩토리메서드 패턴을 구현하는 부분과 유사하다.
위에 작성된 예제가 우리 팀장님이 작성한 코드에서 보이는 방식의 라우팅과 핸들러 함수 작성 방법이다.
이 템플릿 메서드의 단점 중 하나는 중간 인터페이스 즉 ServiceTempalte의 인터페이스 변경된다 면 엄청나게 많은 부분을 수정해야 한다. 이미 진행되었던 프로젝트 라면? 상당한 api 가 구현된 상태라면? 모조리 구현해주어야 한다. ㅠ
안 그래도 지난 개발에서 이 문제에 대해서 겪었고 bool 타입을 리턴해 훅? 비슷한 개념으로 하위에서 구현된 지 여부에 따라 상위 함수를 호출할지 아니면 구현체에서 호출할지 분기를 나누어 주었는데 상당히 고생했다.
디자인패턴을 공부하면 할수록 코드의 아키텍처 가 얼마나 중요한지 매번 느낀다.
아키텍처에 따라 mock tdd의 여부도 가능할뿐더러, 이런 패턴의 적용 또한 분리 추상화가 손쉽게 가능하다. 그게 아니라면... 너무 많은 부분을 수정해야 하고 수정이 많으면 많을수록? 사이드 이펙트는 스노볼 마냥 커지고..
나중에는 감당하지 못해 롤백을...
'Go > Design Pattern' 카테고리의 다른 글
[Design Pattern] 구조패턴-퍼사드 패턴 (Facade Pattern) (0) | 2023.07.28 |
---|---|
[Design Pattern] 구조패턴-어댑터 패턴 (Adapter Pattern) (0) | 2023.07.23 |
[Design Pattern] 행동패턴-커맨드 패턴 (Command Pattern) (0) | 2023.07.14 |
[Design Pattern] 구조패턴-데코레이터 패턴 (Decorator Pattern) (0) | 2023.07.10 |
[Design Pattern] 행동패턴-옵저버패턴 (Observer Pattern) (0) | 2023.07.03 |