템플릿 패턴에 대해 자료를 조사하고, 예제를 만들면서 느낀 점은 확실히 전략패턴과 많이 유사한 느낌을 가지고 있다.

팀장님의 손길이 닿아있는 프로젝트 라면 라우팅 하는 대부분의 부분은 이 템플릿 메소드 부분이 적용되어 있는데 

간략한 버전의 프레임워크를 작성해보고자 한다.

 

템플릿 메서드 패턴 이란 ? 

템플릿 메소드 패턴 은 소프트웨어 공학에서 동작 상의 알고리즘의 프로그램 뼈대를 정의하는 행위 디자인 패턴이다.
 알고리즘의 구조를 변경하지 않고 알고리즘의 특정 단계들을 다시 정의할 수 있게 해 준다. - 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의 여부도 가능할뿐더러, 이런 패턴의 적용 또한 분리 추상화가 손쉽게 가능하다. 그게 아니라면... 너무 많은 부분을 수정해야 하고 수정이 많으면 많을수록? 사이드 이펙트는 스노볼 마냥 커지고..

나중에는 감당하지 못해 롤백을...

 

 

 

지난번 어댑터 패턴에 대해서 알아보았다. 어댑터 패턴과 유사하지만 다른 역할을 하는 패턴에 대해 정리해보고자 한다.

구조패턴 이란 것은 구조를 효율적으로 유지하면서 객체들과 클래스 들을 더 큰 구조로 조립하는 방법을 의미한다.

퍼사드 패턴 또한 이런 분류로 속하게 되는 이유에 대해 생각해 보면서 글을 작성해보고자 한다.

 

퍼사드 패턴 이란 ?

퍼사드는 클래스 라이브러리 같은 어떤 소프트웨어의 다른 커다란 코드 부부에 대해 간략화된 인터페이스를 제공하는 객체이다.
- 퍼사드는 소프트웨어 라이브러리를 쉽게 사용할 수 있게 해 준다. 퍼사드는 공통적인 작업에 대해 간편한 메서드들을 제공해 준다.
래퍼가 특정 인터페이스를 준수해야 하며, 폴리모픽 기능을 지원해야 할 경우에는 어댑터 패턴을 쓴다. 단지 쉽고 단순 한 인터페이스를 이용하고 싶을 경우에는 퍼사드를 쓴다. - wiki 퍼사드패턴 - 

퍼사드 패턴은 라이브러리에 대한, 프레임워크에 대한 또는 다른 클래스들의 복잡한 집합에 대한 단순화된 인터페이스를 제공하는 구조적인 디자인 패턴입니다. - guru -

퍼사드패턴 은 서브시스템에 있는 일련의 인터페이스를 통합 인터페이스로 묶어줍니다. 또한 고수준 인터페이스도 정의하므로 서브시스템을 더 편리하게 사용할 수 있습니다. - HeadFirst -

각 사이트 혹은 책에서 읽은 내용의 정의를 종합해 보자면 퍼사드 패턴은 무언가 묶어주 는 인터페이스 역할을 하게 되고 클라이언트는 이 인터페이스와 통신한다 정도로 이해된다. 이에 클라이언트는 오로지 퍼사드를 통해서 통신하게 되어 의존관계가 줄어든다.

 

왜 사용하는가?

 

1. 단순한 인터페이스 

- 클라이언트 와 복잡한 서브시스템 사이에 단순한 인터페이스를 제공한다. 이로 인해 클라이언트 코드는 간결해지고, 사용법을 쉽게 이해할 수 있다.

2. 복잡성 감소

- 복잡한 서브시스템을 단순한 인터페이스로 감싸기 때문에, 클라이언트는 서브시스템의 내부 동작에 대해 걱정할 필요가 없다. 

3. 결합도 감소 

- 서브시스템과 클라이언트 사이의 결합도를 감소시킨다. 서브시스템의 내부 변경이 발생해도 퍼사드의 인터페이스를 유지하기만 하면 클라이언트는 코드를 수정하지 않아도 된다.

4. 클라이언트 편의성

- 퍼사드 패턴은 서브시스템의 기능을 논리적으로 그룹화할 수 있으며, 이는 클라이언트가 필요로 하는 기능을 쉽게 찾을 수 있게 도와준다.

 

퍼사드 패턴 또한 SOLID 원칙 중 개방폐쇄 원칙을 잘 지키고 있다고 볼 수 있다. 왜? 새로운 기능을 추가하거나, 기존 기능을 변경할 때 클라이언트는 코드를 수정하지 않아도 된다. 

 

구조

말 그대로 다양한 구조체에 대해 통합해서 무언가를 한다는 느낌이 그림으로만 봐도 느껴진다. 

 

퍼사드 패턴의 적용

- 퍼사드 패턴은 복잡한 하위 시스템에 대한 제한적이지만 간단한 인터페이스가 필요할 때 사용한다.

- 퍼사드 패턴은 하위시스템을 계층들로 구성하려는 경우 사용한다.

 

package facade

import "fmt"

/**
다른 이유로 어댑터를 변경하는 방법
인터페이스를 단순하게 변가ㅕㅇ하기 위해 사용
"퍼사드 패턴"
*/

type Screen struct{}

func (s *Screen) up() {
	fmt.Println("[Screen] up")
}
func (s *Screen) down() {
	fmt.Println("[Screen] down")
}

type PopcornPopper struct{}

func (p *PopcornPopper) on() {
	fmt.Println("[PopcornPopper] on")
}
func (p *PopcornPopper) off() {
	fmt.Println("[PopcornPopper] off")
}
func (p *PopcornPopper) pop() {
	fmt.Println("[PopcornPopper] pop")
}

type TheaterLights struct{}

func (t *TheaterLights) on() {
	fmt.Println("[TheaterLights] on")
}
func (t *TheaterLights) off() {
	fmt.Println("[TheaterLights] off")
}
func (t *TheaterLights) dim(){
	fmt.Println("[TheaterLights] dim")
}

type StreamingPlayer struct{}

func (s *StreamingPlayer) on() {
	fmt.Println("[StreamingPlayer] on")
}
func (s *StreamingPlayer) off() {
	fmt.Printf("[StreamingPlayer] off\n")
}
func (s *StreamingPlayer) pause() {
	fmt.Printf("[StreamingPlayer] pause\n")
}
func (s *StreamingPlayer) play(target string) {
	fmt.Printf("[StreamingPlayer %s] play\n", target)
}
func (s *StreamingPlayer) setSurroundAudio() {
	fmt.Printf("[StreamingPlayer] setSurroundAudio\n")
}
func (s *StreamingPlayer) setTwoChannelAudio() {
	fmt.Printf("[StreamingPlayer] setTwoChannelAudio\n")
}
func (s *StreamingPlayer) stop() {
	fmt.Printf("[StreamingPlayer] stop\n")
}
func (s *StreamingPlayer) String() string {
	return "StreamingPlayer"
}

type Projector struct {
	player StreamingPlayer
}

func (p *Projector) on() {
	fmt.Printf("[%s Projector] on\n", p.player)
}
func (p *Projector) off() {
	fmt.Printf("[%s Projector] off\n", p.player)
}
func (p *Projector) tvMode() {
	fmt.Printf("[%s Projector] tvMode\n", p.player)
}
func (p *Projector) wideScreenMode() {
	fmt.Printf("[%s Projector] wideScreenMode\n", p.player)
}

type Tuner struct{}

func (t *Tuner) on() {
	fmt.Println("[Tuner] on")
}
func (t *Tuner) off() {
	fmt.Println("[Tuner] off")
}
func (t *Tuner) setAm() {
	fmt.Println("[Tuner] setAm")
}
func (t *Tuner) setFm() {
	fmt.Println("[Tuner] setFm")
}
func (t *Tuner) setFrequency() {
	fmt.Println("[Tuner] setFrequency")
}

type Amplifier struct {
	tuner *Tuner
	player *StreamingPlayer
}

func (a *Amplifier) on() {
	fmt.Printf("[%s][%s][Amplifier] on\n", a.tuner, a.player)
}
func (a *Amplifier) off() {
	fmt.Printf("[%s][%s][Amplifier] off\n", a.tuner, a.player)
}
func (a *Amplifier) setStreamingPlayer() {
	fmt.Printf("[%s][%s][Amplifier] setStreamingPlayer\n", a.tuner, a.player)
}
func (a *Amplifier)setSurroundSound() {
	fmt.Printf("[%s][%s][Amplifier] setSurroundSound\n", a.tuner, a.player)
}
func (a *Amplifier)setVolume() {
	fmt.Printf("[%s][%s][Amplifier] setVolume\n", a.tuner, a.player)
}
func (a *Amplifier) String() string {
	return fmt.Sprintf("[%s][%s][Amplifier]", a.tuner, a.player)
}

type HomeTheaterFacade struct {
	amp *Amplifier
	tuner *Tuner
	player *StreamingPlayer
	projector *Projector
	lights *TheaterLights
	screen *Screen
	popcornPopper *PopcornPopper
}
func (h *HomeTheaterFacade)watchMovie(movie string){
	fmt.Printf("Get ready to watch a movie...\n")
	h.popcornPopper.on()
	h.popcornPopper.pop()
	h.lights.dim()
	h.screen.down()
	h.projector.on()
	h.projector.wideScreenMode()
	h.amp.on()
	h.amp.setStreamingPlayer()
	h.amp.setSurroundSound()
	h.amp.setVolume()
	h.player.on()
	h.player.play(movie)	
}

영화 하나 보기 위해서 퍼사드가 없다면 클라이언트에서는 watchmovie를 전부 구현하고, 각 구조체를 들고 있어야 한다. 즉 모든 구조체들과 결합하게 된다는 의미이다. 이걸 중간 인터페이스로 추상화를 시킨다면 

클라이언트는 퍼사드와 만 통신을 하면 된다.

func TestFacade(t *testing.T) {
	homeTheater := NewHomeTheaterFacade(
		NewAmplifier(),
		NewTuner(),
		NewStreamingPlayer(),
		NewProjector(),
		NewTheaterLights(),
		NewScreen(),
		NewPopcornPopper(),
	)

	homeTheater.watchMovie("Inception")
}


Get ready to watch a movie...
[PopcornPopper] on
[PopcornPopper] pop
[TheaterLights] dim
[Screen] down
[{} Projector] on
[{} Projector] wideScreenMode
[Tuner][StreamingPlayer][Amplifier] on
[Tuner][StreamingPlayer][Amplifier] setStreamingPlayer
[Tuner][StreamingPlayer][Amplifier] setSurroundSound
[Tuner][StreamingPlayer][Amplifier] setVolume
[StreamingPlayer] on
[StreamingPlayer Inception] play
--- PASS: TestFacade (0.00s)
PASS

이런 식의 코드 구현이 된다. 막상 구현하다 보면 익숙한 느낌이 든다 왜? 

우리는 함수를 구현할 때 도 이와 유사하게 작성한다. 함수 하나당 한의 기능별로 구분을 하고 그 기능을 통합으로 호출하는 함수를 만드는 경우도 있다.

우리는 알게 모르게 디자인 패턴에서 사용되는 개념들을 사용하면서 코딩을 하고 있었다 왜? 그게 더 편하고 유지보수가 편하니깐 함수가 작은 단위로 나눠지면서 재사용성을 가져가고 유지보수하기 가 편해지기 때문에 이렇게 코딩해 왔다.

 

아 위에서 퍼사드패턴에 대해서 설명할 때 인터페이스를 계속 말했었는데 저기서 언급한 인터페이스는 프로그래밍 언어의 인터페이스가 아닌 객체의 추상화를 의미한다. 즉 상위개념으로 캡슐화한다?라고 이해하면 될 것이다.

인터페이스 라고 해서 꼭 인터페이스 와 구조체의 구조를 가져갈 필요는 없는 것이다.

 

어댑터 패턴, 퍼사드 패턴, 데코레이터 패턴 뭔가 다 비슷하게 감싸고 변환하는 등의 구조적인 변환이 있어 비슷한데 정리를 하고 가자.

 

데코레이터 패턴 => 객체에 추가 요소를 동적으로 더할 수 있다. 데코레이터를 사용하면 서브클래스를 만들 때 보다 훨씬 유연하게 기능을 확장할 수 있다.

 

어댑터 패턴 => 특정 클래스 인터페이스를 클라이언트에서 요구하는 다른 인터페이스로 변환한다. 인터페이스가 호환되지 않아 같이 쓸 수 없었던 클래스를 사용할 수 있게 도와준다.

 

퍼사드 패턴 => 서브시스템에 있는 일련의 인터페이스를 통합 인터페이스로 묶어 준다. 또한 고수준 인터페이스도 정의하므로 서브시스템을 더 편리하게 사용할 수 있다.

 

 

지난번 구조패턴으로 데코레이터 패턴에 대해서 알아보았다, 구조패턴의 두 번째 패턴으로 어댑터 패턴을 정리해 보고 자 한다.

구조패턴 이란?

구조패턴 은 구조를 유연하고 효율적으로 유지하면서 객체들과 클래스들을 더 큰 구조로 조립하는 방법을 설명한다.

 

어댑터 패턴이란 ?

어댑터 패턴(Adapter pattern)은 클래스의 인터페이스를 사용자가 기대하는 다른 인터페이스로 변환하는 패턴으로 호환성 없는 인터페이스 때문에 함께 동작할 수 없는 클래스들이 함께 작동하도록 해준다. -wiki-

어댑터는 호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있도록 하는 구조적 디자인 패턴입니다. -guru-

구루, 위키에서 정의하는 공통적인 목표가 있는 패턴이다. 

바로 "인터페이스를 원하는 인터페이스에 맞도록 동작"에 초점이 맞춰져 있다 는 의미이다. 

 

왜 사용하는가?

- 기존 패턴들보다 명확한 이유가 보인다. 바로 인터페이스를 통한 객체의 통신을 연결해 주는 중간다리로 생각하면 될 것 같다.

- 어댑터 패턴은 SOLID 원칙 중 특히 OCP를 잘 지키고 있다. (기존 인터페이스와 새로운 인터페이스의 연결을 위해 중간다리를 만들어주는거니깐 )

- 리스코프치환 원칙 또한 지켜지고 있다. 인터페이스 의 사용주체 "클라이언트"는 어댑터를 사용하더라도 원래 인터페이스를 사용하던 것과 동일한 방식으로 동작하며, 결과를 반환해야 한다. 프로그램의 정확성을 깨지 않으면서 하위타입의 인스턴스를 상위 타입의 객체로 치환 가능해야 한다는 리스코프 원칙이 어느 정도 반영되었다고 볼 수도 있다.

 

구조

왼쪽은 어댑터 패턴의 구조이고, 오른쪽은 데코레이터 패턴의 구조이다. 구조를 보게 되면 아무래도 데코레이터 도 그렇고, 어댑터도 그렇고 생성패턴? 과 는 달리 인터페이스가 아닌 구조체로 받아서 작성하게 된다. 

이 부분에 대해서는 코드를 작성하면서 추가적으로 예시를 작성하고자 한다.

 

어댑터패턴의 적용

- 어댑터 구조체는 기존 구조체를 사용하고 싶지만 그 인터페이스가 나머지 코드와 호환되지 않을 때 사용한다.

- 이 패턴은 부모 클래스에 추가할 수 없는 어떤 공통 기능들이 없는 여러 기존 자식 클래스들을 재사용하려는 경우에 사용한다.

 

type PostgreSQL interface {
	InsertColumn()
	DeleteColumn()
	UpdateColumn()
	ReadColumn()
}

type PostgreSQL_V15 struct {
	db string
}

func (p PostgreSQL_V15) InsertColumn() {
	fmt.Println("Insert by postgresql version 15")
}

func (p PostgreSQL_V15) DeleteColumn() {
	fmt.Println("Delete by posgresql version 15")
}

func (p PostgreSQL_V15) UpdateColumn() {
	fmt.Println("Update by postgresql version 15")
}

func (p PostgreSQL_V15) ReadColumn() {
	fmt.Println("Read by postgresql version 15")
}

var _ PostgreSQL = (*PostgreSQL_V15)(nil)

func NewPostgreSQL() PostgreSQL {
	return &PostgreSQL_V15{"postgresql version 15"}
}

type MySQL interface {
	InsertData()
	DeleteData()
	UpdateData()
	ReadData()
}

type MySQL_V8 struct {
	db string
}

func (m MySQL_V8) InsertData() {
	fmt.Println("Insert by mysql version 8")
}

func (m MySQL_V8) DeleteData() {
	fmt.Println("Delete by mysql version 8")
}

func (m MySQL_V8) UpdateData() {
	fmt.Println("Update by mysql version 8")
}

func (m MySQL_V8) ReadData() {
	fmt.Println("Read  by mysql version 8")
}

var _ MySQL = (*MySQL_V8)(nil)

func NewMySQL() MySQL {
	return &MySQL_V8{"mysql version 8"}
}

type DbBatch interface {
	BatchInsert()
	BatchDelete()
	BatchUpdate()
	BatchRead()
}

type MySQL_V8_Batch_Adapter struct {
	mysql MySQL
}

func (m MySQL_V8_Batch_Adapter) BatchInsert() {
	m.mysql.InsertData()
}

func (m MySQL_V8_Batch_Adapter) BatchDelete() {
	m.mysql.DeleteData()
}

func (m MySQL_V8_Batch_Adapter) BatchUpdate() {
	m.mysql.UpdateData()
}

func (m MySQL_V8_Batch_Adapter) BatchRead() {
	m.mysql.ReadData()
}

var _ DbBatch = (*MySQL_V8_Batch_Adapter)(nil)

func NewDbBatchMySQLAdapter() DbBatch {
	mysql := NewMySQL()
	return &MySQL_V8_Batch_Adapter{mysql}
}

type PostgreSQL_V15_Batch_Adapter struct {
	postgres PostgreSQL
}

func (p PostgreSQL_V15_Batch_Adapter) BatchInsert() {
	p.postgres.InsertColumn()
}

func (p PostgreSQL_V15_Batch_Adapter) BatchDelete() {
	p.postgres.DeleteColumn()
}

func (p PostgreSQL_V15_Batch_Adapter) BatchUpdate() {
	p.postgres.UpdateColumn()
}

func (p PostgreSQL_V15_Batch_Adapter) BatchRead() {
	p.postgres.ReadColumn()
}

var _ DbBatch = (*PostgreSQL_V15_Batch_Adapter)(nil)

func NewDbBatchPostgreSQLAdapter() DbBatch {
	postgres := NewPostgreSQL()
	return &PostgreSQL_V15_Batch_Adapter{postgres}
}


func Test_DB(t *testing.T) {
	batch := []DbBatch{NewDbBatchMySQLAdapter(), NewDbBatchPostgreSQLAdapter()}

	for _, v := range batch {
		v.BatchInsert()
		v.BatchDelete()
		v.BatchRead()
		v.BatchUpdate()
		fmt.Println("----------------------------------")
	}
}

=== RUN   Test_DB
Insert by mysql version 8
Delete by mysql version 8
Read  by mysql version 8
Update by mysql version 8
----------------------------------
Insert by postgresql version 15
Delete by posgresql version 15
Read by postgresql version 15
Update by postgresql version 15
----------------------------------
--- PASS: Test_DB (0.00s)

PostgreSQL 15 버전과, MySQL8 버전을 배치로 작업하기 위해 어댑터 패턴을 적용해 클라이언트에서는 DbBatch 만 사용하고 있으면 내부구현의 수정이 어떻게 변하던지 동일한 결과를 얻게 된다. 

말도 안 되지만 여기서 만약 PostgreSQL 13 버전, MySQL 5 버전 등의 버전들이 필요하게 되고 이 구조체들은 서로 다른 인터페이스를 따르고 있다면? DbBatch에 맞는 새로운 구조체를 생성해서 각각 필드값을 주입받아 해당 함수를 호출시켜 주면 되는 것이다. 

 

데코레이터 패턴과 뭔가 느낌이 비슷하지 않은가? 정리를 해보자.

 

- 데코레이터 패턴 은 기존 객체에 새로운 기능을 동적으로 추가할 수 있도록 설계된 패턴이다. 객체의 데코레이션을 담당하는 별도의 데코레이터 객체를 만들어, 기본 객체에 새로운 행동을 추가하거나 수정한다. 이에 객체를 확정하거나 변경할 수 있다.

 

- 어댑터패턴 은 한 클래스의 인터페이스를 클라이언트가 사용하고자 하는 다른 인터페이스로 변환하는 패턴이다.

 

두 패턴 모두 특징이 하나 있다.

기존 코드를 건드리지 않고도 확장하거나 변경하는데 유용하다. 이게 제일 핵심인 것 같다. 다만 사용의 목적 이 상당히 다를 뿐이지만 
이래서 디자인패턴은 공부를 하면 할수록 원점으로 돌아가는 나 자신이 느껴진다. 

 

 

 

행동패턴 은 지난번 옵서버 패턴을 정리하면서 작성했다 "객체의 통신"에 목적과 의미를 둔 패턴이라고 생각하면 되겠다.

 

커맨드 패턴이란 ?

요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 메서드 이름, 매게 개변수 등 요청에 필요한 정보를 저장 또는 로깅,취소 할 수 있게 하는 패턴이다. -나무위키-

요청에 대한 모든 정보가 포함된 독립실행형 객체로 변환하는 행동 디자인 패턴이다. 이 변환은 다양한 요청들이 있는 메서드들을 인수화 할수 있도록 하며, 요청의 실행을 지연 또는 대기열에 넣을 수 있도록 하고, 또 실행 쉬 초할 수 있는 작업을 지원할 수 있도록 합니다. - 구루-

커맨드라는 추상회된 요청을 이용하는 방식에서 유래되었다. 커맨드 패턴은 요청 자체를 객체로 캡슐화하고, 호출자와 수신자를 분리한다. 이렇게 하면 호출자는 수신자의 인터페이스를 알 필요 없이 커맨드를 수행할 수 있다. 또한, 이런 방식은 다양한 요청, 큐 또는 로그요청, 그리고 가능한 경우에는 요청의 취소도 지원할 수 있다.

지난 패턴들과 동일하게 하나의 인터페이스 추상화를 두고 어떻게 실행하는가에 목적을 두고 있다. 정의에서 보다시피 여러 개의 커맨드를 받아 수행할 수 있다고 하는데 "공통된 인터페이스를 구현하기 때문에 가능하다."

 

언제 사용해야 하는가?

 

1. 호출자와 수신자의 분리

- 호출자가 요청을 수행하는 객체나 그 방법에 대해 알 필요가 없는 경우

- 웹사이트의 사용자 요청을 처리하는 시스템 => 각 요청을 커맨드 객체로 캡슐화해서 처리 가능

- 사용자는 실제 요청이 어떻게 처리되는지 알 필요가 없다.

2. 요청 매개변수화

- 요청을 수행하는 방법을 매개변수화 하려는 경우 커맨드 패턴이 유용하다. 요청 선택 또는 사용자 입력 등을 캡슐화하면 호출하는 시점에 서 다른 매개변수를 사용하여 요청을 수행할 수 있다.

- 사용자 인터페이스의 버튼, 각 버튼을 누르면 특정 작업을  수행할 수 있도록 설정

- 커맨드 객체는 특정 작업을 캡슐화하고 있다.

3. 요청 저장 및 로깅

-커맨드 패턴은 요청을 캡슐화하여 저장하고 로깅하는 것을 가능하게 한다.

- DB의 트랜잭션을 예로 들 수 있다. 각 트랜잭션은 수행되어야 할 작업의 목록을 가질 수 있다. 이 목록을 통해 트랜잭션의 성공여부를 확인할 수 있다.

4. 취소 및 되돌리기 기능

- 커맨드 자신의 undo 메서드를 구현해, 실행취소의 작업도 손쉽게 가능하다.

- 텍스트 에디터 의 undo 기능 사용자가 작성한 텍스트는 undo를 통해 원래 작업과 반대작업을 모두 알고 있기 때문에 가능하다.

 

기본적인 구조

 

1구루 출처

1번의 인보커 발송자는 요청들의 시작점을 나타낸다. 어떠한 커맨드가 설정될지 혹은 어떻게 execute를 할지를 나타내고, 인보커에서 List 형태로 관리되기도 한다.

 

2번의 커맨드는 구상 커맨드 가 구현해야 할 공통 함수를 명명한다. 보는 바와 같이 통상 1 나의 메서드 만을 표기한다.

 

3번의 구상 커맨드 들은 excute에서 비즈니스 로직을  수행해 준다. 

 

4번의 수신자는 비즈니스 로직이 포함되어 있으며 "실제 비즈니스 로직 수행을 담당하고 있다"

 

5번의 클라이언트는 각각의 호출자 수신자 커맨드 등의 인스턴스를 생성하고 요청한다.

 

코드 예제

더보기
package command

import "fmt"

type CommandHead interface {
	execute()
	undo()
}

type Light struct{}

func (l *Light) on() {
	fmt.Println("전구가 켜집니다.")
}
func (l *Light) off() {
	fmt.Println("전구가 꺼집니다.")
}

type GarageDoor struct{}

func (g *GarageDoor) up() {
	fmt.Println("차고 문 올라가요")
}
func (g *GarageDoor) down() {
	fmt.Println("차고 문 내려가요")
}
func (g *GarageDoor) stop() {
	fmt.Println("차고 문 멈춥니다.")
}
func (g *GarageDoor) lightOn() {
	fmt.Println("차고 에 불이 켜집니다.")
}
func (g *GarageDoor) lightOff() {
	fmt.Println("차고 에 불이 꺼집니다.")
}

type GarageOnCommand struct {
	g *GarageDoor
}

func (g *GarageOnCommand) execute() {
	g.g.lightOn()
	g.g.up()
	g.g.stop()
}
func (g *GarageOnCommand) undo() {
	g.g.lightOff()
	g.g.down()
}
func (g *GarageOnCommand) String() string {
	return "차고 ON 명령"
}

type GarageDoorOffCommand struct {
	g *GarageDoor
}

func (g *GarageDoorOffCommand) execute() {
	g.g.lightOff()
	g.g.down()
}
func (g *GarageDoorOffCommand) undo() {
	g.g.lightOn()
	g.g.up()
	g.g.stop()
}

type LightOnCommand struct {
	light Light
}

func (l *LightOnCommand) execute() {
	l.light.on()
}
func (l *LightOnCommand) undo() {
	l.light.off()
}

type SimpleController struct {
	c CommandHead
}

func (s *SimpleController) SetCommand(c CommandHead) {
	s.c = c
}

func (s *SimpleController) ButtonWasPressed() {
	s.c.execute()
}

type NoCommand struct{}

func (n *NoCommand) execute() {
	fmt.Println("There's no command")
}
func (n *NoCommand) undo() {
	fmt.Println("There's no command")
}

type RemoteController struct {
	OnCommands  []CommandHead
	OffCommands []CommandHead
	undoCommand CommandHead
}

func (r *RemoteController) SetCommand(slot int, onCmd, offCmd CommandHead) {
	r.OnCommands[slot] = onCmd
	r.OffCommands[slot] = offCmd
}
func (r *RemoteController) OnButtonWasPushed(slot int) {
	r.OnCommands[slot].execute()
	r.undoCommand = r.OnCommands[slot]
}
func (r *RemoteController) OffButtonWasPushed(slot int) {
	r.OffCommands[slot].execute()
	r.undoCommand = r.OffCommands[slot]
}
func (r *RemoteController) undoButtonWasPushed() {
	r.undoCommand.undo()
}

func NewRemoteController() *RemoteController {
	size := 7
	onCmd := make([]CommandHead, size)
	offCmd := make([]CommandHead, size)
	for i := range onCmd {
		onCmd[i] = &NoCommand{}
		offCmd[i] = &NoCommand{}
	}
	return &RemoteController{onCmd, offCmd, &NoCommand{}}
}

type LightOffCommand struct {
	light Light
}

func (l *LightOffCommand) execute() {
	l.light.off()
}
func (l *LightOffCommand) undo() {
	l.light.on()
}

type Stereo struct {
	cd, dvd string
	volume  int
}

func (s *Stereo) On() {
	fmt.Println("오디오 를 켭니다.")
}
func (s *Stereo) Off() {
	fmt.Println("오디오 를 끕니다.")
}
func (s *Stereo) SetCd(cd string) {
	s.cd = cd
	fmt.Printf("cd 를 녛습니다 : %s\n", s.cd)
}
func (s *Stereo) SetDvd(dvd string) {
	s.dvd = dvd
	fmt.Printf("dvd 를 넣습니다 : %s\n", s.dvd)
}
func (s *Stereo) SetVolume(v int) {
	s.volume = v
	fmt.Printf("볼륨을 설정합니다 : %d\n", s.volume)
}

type StereoOnWithCDCommand struct {
	stereo Stereo
}

func (s *StereoOnWithCDCommand) execute() {
	s.stereo.On()
	s.stereo.SetCd(s.stereo.cd)
	s.stereo.SetVolume(11)
}
func (s *StereoOnWithCDCommand) undo() {
	s.stereo.Off()
}
func NewStereoOnWithCDCommand(cd string) *StereoOnWithCDCommand {
	return &StereoOnWithCDCommand{
		Stereo{
			cd: cd,
		},
	}
}

type StereoOffWithCDCommand struct {
	stereo Stereo
}

func (s *StereoOffWithCDCommand) execute() {
	s.stereo.Off()
}
func (s *StereoOffWithCDCommand) undo() {
	s.stereo.On()
	s.stereo.SetCd(s.stereo.cd)
	s.stereo.SetVolume(11)
}

const (
	Off = iota
	Low
	Medium
	High
)

type CeilingFan struct {
	Speed    int
	Location string
}

func (c *CeilingFan) high() {
	c.Speed = High
}
func (c *CeilingFan) medium() {
	c.Speed = Medium
}
func (c *CeilingFan) low() {
	c.Speed = Low
}
func (c *CeilingFan) off() {
	c.Speed = Off
}
func (c *CeilingFan) getSpeed() int {
	return c.Speed
}

func NewCeilingFan(location string) *CeilingFan {
	return &CeilingFan{Off, location}
}

type CeilingFanHighCommand struct {
	fan       *CeilingFan
	prevSpeed int
}

func (c *CeilingFanHighCommand) execute() {
	c.prevSpeed = c.fan.getSpeed()
	c.fan.high()
}
func (c *CeilingFanHighCommand) undo() {
	switch c.prevSpeed {
	case High:
		c.fan.high()
	case Medium:
		c.fan.medium()
	case Low:
		c.fan.low()
	default:
		c.fan.off()
	}
}

type CeilingFanMediumCommand struct {
	fan       *CeilingFan
	prevSpeed int
}

func (c *CeilingFanMediumCommand) execute() {
	c.prevSpeed = c.fan.getSpeed()
	c.fan.medium()
}
func (c *CeilingFanMediumCommand) undo() {
	switch c.prevSpeed {
	case High:
		c.fan.high()
	case Medium:
		c.fan.medium()
	case Low:
		c.fan.low()
	default:
		c.fan.off()
	}
}

type CeilingFanOffCommand struct {
	fan       *CeilingFan
	prevSpeed int
}

func (c *CeilingFanOffCommand) execute() {
	c.prevSpeed = c.fan.getSpeed()
	c.fan.off()
}
func (c *CeilingFanOffCommand) undo() {
	switch c.prevSpeed {
	case High:
		c.fan.high()
	case Medium:
		c.fan.medium()
	case Low:
		c.fan.low()
	default:
		c.fan.off()
	}
}

type MacroCommand struct {
	cmd []CommandHead
}

func (m *MacroCommand) execute() {
	for _, v := range m.cmd {
		v.execute()
	}
}
func (m *MacroCommand) undo() {}

실제 동작하는 "light", "grageDoor", "stereo", "ceilingFan"을 두고 interface 커맨드 헤더를 기점으로 각각의 구조체들을 on, off를 모두 생성한다. 

이에 따라  아래와 같은 테스트 코드 작성이 가능해지며, 원하는 기능을 커맨드로 추상화해서 작동하는 것 이 가능해진다.

func Test01(t *testing.T) {
	remote := &SimpleController{&LightOnCommand{Light{}}}

	remote.ButtonWasPressed()

	garage := &GarageOnCommand{&GarageDoor{}}
	remote.SetCommand(garage)

	remote.ButtonWasPressed()
}

func Test02(t *testing.T) {
	rmt := NewRemoteController()

	light := &Light{}
	garage := &GarageDoor{}
	stereo := &Stereo{}

	rmt.SetCommand(0, &LightOnCommand{*light}, &LightOffCommand{*light})
	rmt.SetCommand(1, &GarageOnCommand{garage}, &GarageDoorOffCommand{garage})
	rmt.SetCommand(2, NewStereoOnWithCDCommand("guiwoo"), &StereoOffWithCDCommand{*stereo})

	rmt.OnButtonWasPushed(1)
	rmt.OffButtonWasPushed(1)
}

func Test03(t *testing.T) {
	rmt := NewRemoteController()

	light := &Light{}
	garage := &GarageDoor{}
	stereo := &Stereo{}

	rmt.SetCommand(0, &LightOnCommand{*light}, &LightOffCommand{*light})
	rmt.SetCommand(1, &GarageOnCommand{garage}, &GarageDoorOffCommand{garage})
	rmt.SetCommand(2, NewStereoOnWithCDCommand("guiwoo"), &StereoOffWithCDCommand{*stereo})

	rmt.OnButtonWasPushed(1)

	rmt.undoButtonWasPushed()

	fmt.Println(rmt.undoCommand)
}

func Test04(t *testing.T) {
	rmt := NewRemoteController()

	fan := NewCeilingFan("Living Room")

	rmt.SetCommand(0, &CeilingFanMediumCommand{fan: fan}, &CeilingFanOffCommand{fan: fan})
	rmt.SetCommand(1, &CeilingFanHighCommand{fan: fan}, &CeilingFanOffCommand{fan: fan})
}

func Test05_macro(t *testing.T) {
	light := &Light{}
	grage := &GarageDoor{}
	stereo := &Stereo{}

	//on command 작성
	lightOn := &LightOnCommand{*light}
	grageOn := &GarageOnCommand{grage}
	stereoOn := NewStereoOnWithCDCommand("guiwoo")
	onCmd := []CommandHead{lightOn, grageOn, stereoOn}

	//off command 작성
	lightOff := &LightOffCommand{*light}
	grageOff := &GarageDoorOffCommand{grage}
	stereoOff := &StereoOffWithCDCommand{*stereo}
	offCmd := []CommandHead{lightOff, grageOff, stereoOff}

	//slot 에 넣어주기
	rmt := NewRemoteController()
	rmt.SetCommand(0, &MacroCommand{onCmd}, &MacroCommand{offCmd})

	rmt.OnButtonWasPushed(0)
}

커맨드 패턴을 구성하는 데 있어 지금까지 디자인 패턴 중 추상팩토리 다음으로 구조체를 많이 사용했다. 

구조체가 생각보다 많이 작성되어 실제 서비스에서는 관리하기 생각보다 까다롭지 않을까?

 

음식점에서 주문을 하는 상황에 있다고 가정해 보자. 각 주문은 커맨드의 구상객체 가 될 수 있고,

각 주문은 요리 메서드를 가지고 있다고 하면 이 주문이 요리사에게 전달되어 요리사는 이를 실행할 수 있다.

여기서 자세하게 분리해서 보면

"주문 은 요리로 부터 분리 있고, 요리사는 어떤 주문 이든 바당서 요리를 할수 있다 왜? 요리라는 메서드가 각 주문 별로 있기 때문에"

이러한 것을 요리메서드는 주문커맨드 객체에 의해 매개변수화 되었다고 한다. 

이러한 관점에서 실제 코드 설계 과정에서 매개변수화가 필요한 경우가 종종 있다. 

 

위에서 말했지만 애플리케이션의 api 호출에 따라 다른 행위가 실행되게 설계되도록 할 수 있다. 이 경우 각 행위를 커맨드 객체로 만들고, api 호출에 따라 이를 실행할 수 있다.

type Command interface {
	Execute(w http.ResponseWriter, r *http.Request)
}

type MyHandler struct {
	routes map[string]Command
}

func (m *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	path := r.URL.Path

	if cmd, ok := m.routes[path]; ok {
		cmd.Execute(w, r)
	} else {
		http.NotFound(w, r)
	}
}

type FooHandler struct{}

func (f *FooHandler) Execute(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "This is Foo Handler")
}

func callPureHttp(port string) {

	handler := &MyHandler{
		routes: map[string]Command{
			"/foo": &FooHandler{},
		},
	}

	log.Fatal(http.ListenAndServe(":4000", handler))
}

사실 이렇게 놓고 보자니깐 코드 가 동적으로 변경되는것이 전략패턴과 상당히 유사해 보이지 않는가 ? 

전략패턴 과 커맨드패턴의 구조차제는 매우 유사하다. 다만 해결하려는 문제 와 사용하는 문맥에 따라  이 패턴의 이름이 갈린다. 

 

처리하고자 하는 도작을 캡슐화 하고 있기에 보다 커맨드 패턴에 가깝다고 할수 있다.

전략패턴은 "행동을 동적으로 변경하고, 알고리즘의 변형을 유연하게 관리해야 하는 경우 사용 된다."
위와 동일한 역할을 하는 전략패턴을 적용해서 구현해보자.

type Strategy interface {
	ServeHTTP(w http.ResponseWriter, r *http.Request)
}

type StrategyA struct{}

func (s *StrategyA) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
}

type StrategyB struct{}

func (s *StrategyB) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hi, you've requested: %s\n", r.URL.Path)
}

type Context struct {
	strategy Strategy
}

func (c *Context) ExecuteStrategy(w http.ResponseWriter, r *http.Request) {
	c.strategy.ServeHTTP(w, r)
}

func callStrategy(){
	strategyA := &StrategyA{}
	strategyB := &StrategyB{}

	context := &Context{}

	http.HandleFunc("/a", func(w http.ResponseWriter, r *http.Request) {
		context.strategy = strategyA
		context.ExecuteStrategy(w, r)
	})

	http.HandleFunc("/b", func(w http.ResponseWriter, r *http.Request) {
		context.strategy = strategyB
		context.ExecuteStrategy(w, r)
	})

	http.ListenAndServe(":8080", nil)
}

명확하게 다르다 하나의 문맥을 기준으로 매번 새로운 함수 핸들러를 넣어주지만

커맨드 패턴은 이미 정의되어있는 것을 들고와서 사용하는 참 아다르고 어다른 디자인 패턴이다.

어이가 없다.
전략패턴 은 "어떻게 실행하는 가 ? 에 초점을 두고 있고", 커맨드패턴 은 "실행할 행동 ?" 에 초점을 두고 있다.

 

구조패턴 이란?

구조패턴 은 구조를 유연하고 효율적으로 유지하면서 객체들과 클래스들을 더 큰 구조로 조립하는 방법을 설명한다.

 

그중 데코레이터 패턴에 대해 학습해보고자 한다.

데코레이터 패턴 이란?

데코레이터 패턴 이란 주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴으로, 기능 확장이 필요할 때 서브클래싱 대신 쓸 수 있는 유연한 대안이 될 수 있다. - 위키

데코레이터는 객체들을 새로운 행동들을 포함한 특수 래퍼 객체들 내에 넣어서 위 행동들을 해당 객체들에 연결시키는 구조적 디자인 패턴이다.

데코레이터 패턴으로 객체에 추가 요소를 동적으로 더할 수 있으며, 데코레이터를 사용하면 서브클래스를 만들 때 보다 유연하게 기능을 확장할 수 있다.

데코레이터 말 그대로 장식에 대해 생각하면 된다. 

 

왜 사용하는가?

코드의 유연성, 확장성, 재사용성을 높이고 객체 지향 설계원칙을 준수하기 위해 사용된다.

 

객체의 동적인 기능 확정과 코드의 재사용성, 유연성을 동시에 제공하기 위해 사용된다.

객체지향 설계원칙을 따르고 있으며, 객체 간의 결합도를 낮추고 코드의 유지보수성을 향상한다.

 

구조

 

구루 에서 제공되는 구조,

 

컴포넌트 즉 구현되어야 할 매개체를 인터페이스로 명시하고, 해당 인터페이스를 구현하는 객체 그리고 데코레이터는 인터페이스와 동일한  함수를 구현하게 해 인터페이스 호출이 가능하도록 작성한다.

이렇게 작성된 구조체에 함수들은 주입받은 객체를 호출하고 본인을 호출하면 마치 체이닝 걸린 것처럼 연속적으로 호출되게 된다.

코드로 보면 보다 이해하기 쉽다.

type beverage interface {
	cost() float32
	getDescription() string
}

type HouseBlend struct {
	description string
}

func (h *HouseBlend) cost() float32 {
	return 0.89
}
func (h *HouseBlend) getDescription() string {
	return h.description
}

type DarkRost struct {
	description string
}

func (d *DarkRost) cost() float32 {
	return 1.32
}
func (d *DarkRost) getDescription() string {
	return d.description
}

type Milk struct {
	b beverage
}

func (m *Milk) cost() float32 {
	return m.b.cost() + 12.3
}
func (m *Milk) getDescription() string {
	return m.b.getDescription() + " milk"
}

type Whip struct {
	b beverage
}

func (w *Whip) cost() float32 {
	return w.b.cost() + 15.5
}
func (w *Whip) getDescription() string {
	return w.b.getDescription() + " whip"
}

func StartHead() {
	//case 1
	a := &HouseBlend{"house coffee"}
	b := &Milk{a}
	c := &Whip{b}

	fmt.Println(c.cost(), c.getDescription())
	//case2
	var B beverage
	B = &HouseBlend{"house"}
	B = &Milk{B}
	B = &Whip{B}

	fmt.Println(B.cost())
}

음료 인터페이스가 있고, 기존 구현체인 HouseBlend, DarkLost 가 존재하고 있으며 데코레이터로 우유와 휘핑 이 있다. 이렇게 되면 데코레이터의 구현체는 주입받은 인터페이스를 이용해 호출하고 데코레이터에서 하고 싶은 함수를 호출하면 된다. 이렇게 기존함수를 호출 함에 따라 마치 연쇄적으로 함수가 동작하는듯한 효과를 가져온다.

 

실행의 결괏값은 아래와 같다.

- 28.69 house coffee milk whip

하우스블랜드 0.89 => 우유 12.3 => 휘핑 15.5 가 차례대로 호출되며 28.69 가 나오게 된다.

순차적으로 실행되는 듯한 느낌이 마치 장식을 해주는 것과 같은 것이 데코레이터패턴 의 이름은 정말 잘 지은 것 같다.

저위에 작성된 클라이언트 코드에서는 case1과 case2로 나누어서 사용한다. 보면 1번의 경우 하나씩 스택을 쌓아가는 반면 

2번 케이스는 인터페이스하나로 받아가면서 사용한다. 

이렇게 됐을 때 2번의 경우로 작성한다면 다형성을 적용하여 보다 폭넓게 사용가능하다. 

 

고 언어에서 활용되는 기본 패키지 둘 중에 데코레이터 패턴이 적용된 것을 살펴보면

compress 패키지 에는 gzip, flate, zip 등의 패키지에서 io.Reader io.Writer 인터페이스를 구현해 데이터 압축 기능을 추가하는

데코레이터 역할을 한다.

 

지난 포스팅에서 행동패턴"객체의 통신" 에 보다 밀접한 관련이 있다라고 생각했다. 

이 행동패턴이 가지고 있는 컨셉을 가지고 옵저버 패턴을 확인해 보자. 

 

옵저버 패턴 이란?

객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여, 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인패턴 이다. "발행 구독 모델로 알려져 있기도 하다." - 위키-
여러 객체에 자신이 관찰 중인 객체에 발생하는 모든 이벤트에 대하여 알리는 구독 메커니즘을 정의할 수 있도록 하는 행동 디자인 패턴이다. - 구루-

위 두 사이트에서 공통적으로 말하고자 하는 바가 있다. "구독", "상태변화, 이벤트 알림" 이 가장 중복되는 의미로 작성되어 있다.

옵저버 패턴을 사용하면 하나의 관리자가 있고 그 관리자는 여러 개의 옵저버 들을 관리할 수 있는 경우를 말하는구나라고 이해하고 넘어갈 수 있다.

왜 사용하는가? 
-
관리자와 관리자에게 관리당하는 객체 들 간의 관계성에서 시작된다. 관리자는 interface를 관리하게 된다. 이 말은 즉 관리자는 관리당하는 객체가 누구인지 모르지만 "interface에서 정의한 행동"은 한다라고 만 알고 있는 상태가 되어버린다. 
이는 다시 말해 느슨한 결합이 되고, 느슨한 결합은 코드의 확장에 있어 엄청난 이점을 가져다준다.

  1. 주제는 옵저버가 특정 인터페이스를 구현한다는 사실만 압니다.
  2. 옵저버는 언제든지 새로 추가할 수 있습니다.
  3. 새로운 형식의 옵저버를 추가할 때도 주제를 변경할 필요가 전혀 없습니다.
  4. 주제와 옵저버는 서로 독립적으로 재사용할 수 있습니다.
  5. 주제나 옵저버가 달라져도 서로에게 영향을 미치지  않습니다.(인터페이스 수정이 아닌 다른 부분)

느슨하게 결합된다면 변경사항 이 생겨도 유연한 객체지향 시스템을 구축할 수 있다.

 

구조 

위키, 구루 구조 이미지 참조

두 개의 구조가 상당히 유사하다. 당연히 비슷해야 한다. 옵저버 패턴의 구조니 깐
Subject는 Publisher와 , Observer는 Subscriber와 각각 대응된다.

 

위에서 말한 관리자 즉 subject, publisher는 옵저버 들을 특정 자료구조에 저장하고 저장된 자료구조를 loop를 돌면서 구현된 인터페이스의 함수를 이용해 객체 간의 통신이 진행된다. 

 

위의 구조가 상당히 익숙한데 채팅을 메시지-큐 없이 사용할 때 유저의 관리와 소켓의 데이터 발행과 수신을 위해 저렇게 loop를 돌면서 방접속자 혹은 채팅 접속자에게 메시지를 보내는 경우가 해당된다.

이건 전략패턴의 기본 구조중 하나이다. 구조 자체는 상당히 비슷하다.

인터페이스의 추상화를 기본 골대로 가져 실제 구현된 사항들을 캡슐화 하여 클라이언트에게 제공한다. 그러나 동작하는 목적의 자체가 다르다.

 

전략패턴

알고리즘을 캡슐화하고, 이를 동적으로 교환할 수 있는 구조를 제공하며, 클라이언트는 전략인터페이스를 통해 알고리즘을 호출한다.

클라이언트 즉 전략패턴의 소유권자는 어떤 전략 객체를 사용할지 선택하는 주체이다.

(전략패턴 또한 전략 객체와 컨택스트는 느슨하게 결합되어 있다.  컨택스트 는 특정 전략 객체를 호출하는 것이 아닌 인터페이스를 통해 호출한다.)

 

옵저버패턴

클라이언트 즉 옵저버들의 관리자 와 옵저버 간의 상호작용을 나타내는 패턴이다. 옵저버 들의 관리자(소유권자)는 소유권자의 상태 변화에 따라 옵저버 들에게 이벤트를 알려주고, 이러한 이벤트에 따라 각각의 알고리즘 혹은 작업을 수행한다.

소유권자, 관리자는 옵저버 들이 누구인지 알지 못하며, 이는 느슨한 결합을 의미한다.

(옵저버 패턴에서 유달리 느슨한 결합이 강조되는 이유는 "주체" 와 "옵저버들" 간의 유연성과 확장성을 강조하기 때문)

 

각각의 패턴이 지향하고자 하는 바는 명확하게 다르다. 

 

옵저버 패턴 사용시기

- 한 객체의 상태가 변화에 여러 객체가 반응해야 하는 경우

- 상호작용(위의 케이스) 하는 객체들이 느슨한 결합이 필요할 때(언제든지 확장의 가능성이 있을 때)

- 이벤트 기반 시스템에서 주체 객체와 구독 객체로 시스템 구성을 해야 할 때


구현해 보기

WeatherDate의 관리자는 현재날씨 화면, 통계, 예보 화면 등에 대해 관리하고 있으며 notify를 통해 변경된 날씨에 따라 각각의 화면이 반응하게 된다. 
당연히 새로운 옵저버가 추가되던, 화면 노출이 아닌 다른 로직이 생기더라도 기존 코드의 수정없이 업데이트 가능하다. WeatherDate 또한 동일하다. 

더보기
package observer

import "fmt"

type Subject interface {
	registerObserver(o Observer2)
	removeObserver(o Observer2)
	notifyObserver()
}

type Observer2 interface {
	update()
}
type DisplayElement interface {
	display()
}

type WeatherData struct {
	observers                []Observer2
	temp, humidity, pressure float32
}

func (w *WeatherData) registerObserver(o Observer2) {
	if w.observers == nil {
		w.observers = make([]Observer2, 0)
	}
	w.observers = append(w.observers, o)
}

func (w *WeatherData) removeObserver(o Observer2) {
	for i, v := range w.observers {
		if v == o {
			if i != len(w.observers)-1 {
				// 일반 슬라이스
				tmp := w.observers[:i]
				w.observers = append(tmp, w.observers[i+1:]...)
			} else {
				w.observers = w.observers[:len(w.observers)-1]
			}
		}
	}
}

func (w *WeatherData) notifyObserver() {
	for _, v := range w.observers {
		v.update()
	}
}

func (w *WeatherData) getTemperature() {

}
func (w *WeatherData) getHumidity() {

}
func (w *WeatherData) getPressure() {

}
func (w *WeatherData) measurementChanged() {
	w.notifyObserver()
}
func (w *WeatherData) setMeasurement(tmp, humidity, pressure float32) {
	w.temp = tmp
	w.humidity = humidity
	w.pressure = pressure
	w.measurementChanged()
}

var _ Subject = (*WeatherData)(nil)

type CurrentConditionDisplay struct {
	temperature, humidity float32
	weatherData           *WeatherData
}

func (c *CurrentConditionDisplay) update() {
	c.temperature = c.weatherData.temp
	c.humidity = c.weatherData.humidity
	c.display()
}
func (c *CurrentConditionDisplay) display() {
	fmt.Printf("현재 상태 온도 : %f, 습도 : %f\n", c.temperature, c.humidity)
}
func NewCurrentConditionDisplay(w *WeatherData) *CurrentConditionDisplay {
	ob := &CurrentConditionDisplay{weatherData: w}
	w.registerObserver(ob)
	return ob
}

type StatisticDisplay struct {
	average, highest, lowest float32
	weatherData              *WeatherData
}

func (s *StatisticDisplay) update() {
	temp := s.weatherData.temp
	s.average = (s.average + temp) / 2

	if s.highest < temp {
		s.highest = temp
	}
	if s.lowest > temp {
		s.lowest = temp
	}
	s.display()
}
func (s *StatisticDisplay) display() {
	fmt.Printf("평균/최고/최저 온도 : %f / %f / %f \n", s.average, s.highest, s.lowest)
}
func NewStatisticDisplay(w *WeatherData) *StatisticDisplay {
	ob := &StatisticDisplay{weatherData: w}
	w.registerObserver(ob)
	return ob
}

type ForecastDisplay struct {
	prevTemp, prevHumidity, prevPressure float32
	announcement                         string
	weatherData                          *WeatherData
}

func (f *ForecastDisplay) update() {
	temp := f.weatherData.temp
	humidity := f.weatherData.humidity
	pressure := f.weatherData.pressure
	if temp > f.prevTemp && humidity > f.prevHumidity && pressure > f.prevPressure {
		f.announcement = "날씨가 무척 더워질 예정입니다. 조심하세요"
	} else if temp < f.prevTemp && humidity < f.prevHumidity && pressure < f.prevPressure {
		f.announcement = "날씨가 무척 추워질 예정입니다. 조심하세요"
	} else {
		f.announcement = "어제와 유사한 날씨 입니다."
	}
	f.prevTemp = temp
	f.prevHumidity = humidity
	f.prevPressure = pressure
	f.display()
}
func (f *ForecastDisplay) display() {
	fmt.Println(f.announcement)
}
func NewForecastDisplay(w *WeatherData) *ForecastDisplay {
	ob := &ForecastDisplay{weatherData: w}
	w.registerObserver(ob)
	return ob
}



func Test_02(t *testing.T) {
	w := &WeatherData{}

	cur := NewCurrentConditionDisplay(w)
	stat := NewStatisticDisplay(w)
	fore := NewForecastDisplay(w)

	fmt.Println(cur, stat, fore)

	w.setMeasurement(80, 22.2, 32.7)
	w.setMeasurement(60, 20.2, 30.7)
	w.setMeasurement(90, 22.2, 32.9)
}
=== RUN   Test_02
&{0 0 0x1400006c540} &{0 0 0 0x1400006c540} &{0 0 0  0x1400006c540}
현재 상태 온도 : 80.000000, 습도 : 22.200001
평균/최고/최저 온도 : 40.000000 / 80.000000 / 0.000000 
날씨가 무척 더워질 예정입니다. 조심하세요
현재 상태 온도 : 60.000000, 습도 : 20.200001
평균/최고/최저 온도 : 50.000000 / 80.000000 / 0.000000 
날씨가 무척 추워질 예정입니다. 조심하세요
현재 상태 온도 : 90.000000, 습도 : 22.200001
평균/최고/최저 온도 : 70.000000 / 90.000000 / 0.000000 
날씨가 무척 더워질 예정입니다. 조심하세요

테스트의 결과 위와 같이 나오고 있으며 매번 변경되는 값이 생길 때마다 관리되고 있는 옵저버 들은 변경된 값에 대해 응답을 하게 된다.

 

여기서 궁금증이 생긴다. 

옵저버 패턴을 적용하여, 주체객체에 생각보다 많은 옵저버들이 등록되어 관리되고 있다면 어떻게 처리할까? 

고라는 언어특성상 비동기처리 하기 매우 쉽다. 고 루틴과 채널링을 이용해 처리해버리면 된다.

옵저버들은 각각의 고루틴을 가지고 채널을 열어서 대기하며 주체 객체 에서 발생된 이벤트에 대해 각 옵저버 별로 채널에 값을 넣어주면 된다. 마치 채팅의 메시지 수신 후 채팅방 구성원들에게 메시지를 뿌려주듯이

 

최근 동시성 프로그래밍과 채팅 관련해서 공부하다 보니 생각보다 옵저버 패턴과 유사하게 구현된 부분이 많아 생각보다 반가운 패턴이었다. 구조가 같다는 게 아니라 동작하는 방식이 유사하다는 뜻이다. 

 

 

 

+ Recent posts