우선 행동패턴 이 무엇이기에 하나의 큰 카테고리 가 되었는지 부터 알아보자

행동패턴 이란 ?(Behavioral Patterns)
소프트웨어 엔지니어링에서 행동 디자인 패턴은 개체 간의 일반적인 통신 패턴을 식별하는 디자인 패턴.
알고리즘 및 개체 간의 책임 할당 과 관련이 있다.
목적
객체 간의 상호작용과 책임 분배를 구조화하고, 객체의 행동을 유연하게 조정할 수 있도록 하는 것.
다양한 행동 패턴을 사용하면 객체간의 결합도를 낮추고 재사용과 유연성을 향상할 수 있다.

생성패턴 은 말그대로 객체의 "생성"에 포커싱이 되었다면, 행동패턴 은 객체의 "행동" 다른 말로는 통신에 포커싱이 되어있는 패턴이라고 생각하면 된다.

 

전략패턴 이란? (Strategy Pattern)
실행중 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다.
- 특정한 계열의 알고리즘들을 정의하고
- 각 알고리즘을 캡슐화하며
- 이 알고리즘들을 해당 계열 안에서 상호교체가 가능하게 만든다.
전략은 유연하고 재사용 가능한 객체지향 소프트웨어를 어떻게 설계하는지 기술하기 위해 작성된 디자인패턴 중 하나이다.

위키피디 아 이미지 참조

제공된 간단한 UML을 확인했을 때 음 하나의 프로그램 실행 단위에서 인터페이스를 구현하는 객체들을 특정 시간대에 서로 다른 객체를 호출하는구나라고 생각하고 넘어가자.

 

구루에 작성된 전략패턴 의 정의를 보면 보다 전략패턴이 명확해진다.

전략패턴 은 알고리즘들의 패밀리 를 정의하고, 각 패밀리를 별도의 클래스에 넣은 후 그들의 객체들을 상호교환할 수 있도록 하는 행동 디자인 패턴입니다.

전략패턴은 객체를 교환가능하게 만들어주는 패턴이구나, 패밀리들을 정의한다 추상화를 한다고 생각하면 되는 걸까? 

위키와 구루의 내용을 종합해 보자면
전략패턴은 객체 간의 "통신, 교환 " 가능하며, 이들은 캡슐화되어 특정 객체에 의존적이지 않으며 유연하게 재사용 가능하다.

 

구루에서 제공된 구조이다. 
컨택스트는 오직 Strategy 인터페이스 만을 통해 ConcreteStrategies와 통신을 하고 있다. 

Concrete Strategies는 콘텍스트에서 수행될 다양한 알고리즘들을 구현하고 있다.

클라이언트는 Concrete Strategies 중 원하는 구현체를 선택해 콘텍스트에서 원하는 시점에 원하는 방향성을 가지고 구현이 가능하다.

이러한 전략패턴 은 언제 적용되어야 하는가?

  1. 객체 내에서 한 알고리즘의 다양한 변형들을 사용하고 싶을 때, 런타임 중에 한 알고리즘에서 다른 알고리즘으로 전환하고 싶을 때
  2. 일부 행동을 실행하는 방식에서만 차이가 있는 유사한 클래스들이 많은 경우
  3. 알고리즘 즉 수행하고자 하는 변경하고자 하는 사항 들을 세부 로직 과의 결합성을 낮추고 싶을 때
  4. 알고리즘의 다른 변형들 사이를 전환하는 거대한 조건문이 클래스 내부에 있을 때 

Html 또는 마크다운을 선택적으로 클라이언트에서 호출할 수 있는 전략패턴을 작성해 보자.

 

더보기
type OutputFormat int

const (
	MarkDown OutputFormat = iota
	Html
)

type ListStrategy interface {
	Start(builder *strings.Builder)
	End(builder *strings.Builder)
	AddListItem(builder *strings.Builder, item string)
}

type MarkdownListStrategy struct{}

func (m *MarkdownListStrategy) Start(builder *strings.Builder) {
}

func (m *MarkdownListStrategy) End(builder *strings.Builder) {
}

func (m MarkdownListStrategy) AddListItem(builder *strings.Builder, item string) {
	builder.WriteString(" * " + item + "\n")
}

var _ ListStrategy = (*MarkdownListStrategy)(nil)

type HtmlListStrategy struct{}

func (h *HtmlListStrategy) Start(builder *strings.Builder) {
	builder.WriteString("<ul>\n")
}

func (h *HtmlListStrategy) End(builder *strings.Builder) {
	builder.WriteString("</ul>\n")
}

func (h *HtmlListStrategy) AddListItem(builder *strings.Builder, item string) {
	builder.WriteString("\t <li>" + item + "</li>\n")
}

var _ ListStrategy = (*HtmlListStrategy)(nil)

type TextProcessor struct {
	builder strings.Builder
	list    ListStrategy
}

func NewTextProcessor(list ListStrategy) *TextProcessor {
	return &TextProcessor{builder: strings.Builder{}, list: list}
}

func (t *TextProcessor) SetOutputFormat(fmt OutputFormat) {
	switch fmt {
	case MarkDown:
		t.list = &MarkdownListStrategy{}
	case Html:
		t.list = &HtmlListStrategy{}
	}
}

func (t *TextProcessor) AppendList(items []string) {
	s := t.list
	s.Start(&t.builder)

	for _, item := range items {
		s.AddListItem(&t.builder, item)
	}

	s.End(&t.builder)
}

func (t *TextProcessor) Reset() {
	t.builder.Reset()
}

func (t *TextProcessor) String() string {
	return t.builder.String()
}

리스트 전략 인터페이스를 구성해 전략패턴을 적용한다.
문장의 시작과 끝을 나타내는 함수와, 어떠한 아이템들이 추가되는지에 대한 인터페이스를 정의했다.
마크다운과 Html 은 전략패턴의 구현체가 있으며 Text 프로세스에 의해 어떠한 형태로 데이터가 기입되는지 결정된다.
결과는 아래와 같다.

func Test_02(t *testing.T) {
	tt := NewTextProcessor(&MarkdownListStrategy{})
	tt.AppendList([]string{"park", "gui", "woo"})

	fmt.Println(tt)

	tt.Reset()

	tt.SetOutputFormat(Html)
	tt.AppendList([]string{"park", "gui", "woo"})
	fmt.Println(tt)
}

/**
=== RUN   Test_02
 * park
 * gui
 * woo

<ul>
	 <li>park</li>
	 <li>gui</li>
	 <li>woo</li>
</ul>
*/

실행 시 이런 결과 값이 발생한다. 
전략 패턴은 단순하다. 클라이언트가 원하는 시점에 특정 객체의 원하는 행동을 지정할 수 있다.

알고리즘 이라고 거창하게 되어 있지만 클라이언트 의 호출자 에 의해 프로그램 실행중에 로직의 변경이 필요하다면 전략패턴 은 훌륭한 해결책이 될수 있다.

기존 회사 프로젝트 의 예로 api 호출 grpc 호출 등 모든 서비스 들은 Service interface 에 의해 구현되고 호출된다. 
각 라우터들은 저 Service 인터페이스를 구현하고 특정 라우터의 호출 마다 매번 실행되는 서비스 들은 교체 되어 실행된다 라우터 의 구현체 에 의해 교체 된다는 점에서 전략 패턴이 적용되었다고 볼수 있다.

소프트웨어 디자인 패턴에서 싱글턴 패턴
(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글턴 패턴이라고 한다.
주로 공통된 객체를 여러 개 생성해서 사용하는 DBCP(DataBase Connection Pool)와 같은 상황에서 많이 사용된다. - wikipedia

 

싱글턴 패턴의 등장 배경에는 아래와 같은 이유가 있다.

유일한 인스턴스 유지:
일부 시스템에서는 특정 클래스의 인스턴스가 하나만 존재해야 할 때가 있습니다. 예를 들어, 시스템의 설정 정보를 담당하는 클래스, 로그를 관리하는 클래스, DB 연결을 관리하는 클래스 등이 있습니다. 이런 경우, 싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 존재하도록 보장합니다.

전역 접근: 또한 싱글턴 패턴은 이 유일한 인스턴스에 대한 전역적인 접근 방법을 제공합니다. 즉, 어디서든 이 인스턴스를 참조할 수 있게 됩니다. 이는 애플리케이션 전반에서 공유해야 하는 데이터나 리소스를 관리하는 데 유용합니다.

제어된 리소스 공유: 한정된 리소스를 공유해야하는 경우에도 싱글턴 패턴이 유용합니다. 예를 들어, 데이터베이스 연결풀, 파일 시스템, 네트워크 소켓 등의 리소스는 한정되어 있기 때문에 효율적으로 관리하고 사용할 필요가 있습니다.

메모리 및 성능 최적화: 싱글턴 패턴을 사용하면 메모리 사용을 최적화하고 성능을 향상할 수 있습니다. 인스턴스를 한 번만 생성하므로 메모리 낭비를 줄이고, 계속해서 같은 인스턴스를 재사용함으로써 성능을 향상할 수 있습니다.

 

예전 Java 에서 Singleton 패턴에 관련해 정리된 내용이 있어 Go에서는 어떻게 활용하는가? 에 초점을 두어 작성하겠다.

(https://guiwoo.tistory.com/24)

 

전통적인 방식

type db struct {
	/**
	db 에 해당하는 값 이 저장
	*/
}

var obj *db

func GetInstance() *db {
	if obj == nil {
		obj = &db{}
	}
	return obj
}

 

위와 같이 구현을 한다. 다만 이렇게 구현 되었을때 문제점이 발생된다. 바로 멀티스레드 환경인데 

(생성 과정 의 단순함 과 Go Routine의 최적화에 따라 문제점 발생이 너무 희박해서 강제로 상황을 만들었다 "runtime.Gosched()를 활용")

func main() {
	wg := &sync.WaitGroup{}

	for i := 0; i < 30; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			singleton.GetInstance()
		}()
	}
	wg.Wait()
}

DB의 인스턴스를 여러 번 생성하는 것을 확인할 수 있다. 

 

이를 방지하기 위해 Go 에서 제공해 주는 특별한 기능들이 있는데

 

Sync.Once(https://pkg.go.dev/sync)

- 싱크 패키지 에서 제공하는 구조체 중에 하나로 Once는 한 번만 수행되도록 도와주는 구조체이다. 제공되는 메서드 중 하나로 Do 메서드가 있으며 Do에서 실행되는 콜백 function 은 오직 한 번의 실행만을 수행한다.

type db struct {
	/**
	db 에 해당하는 값 이 저장
	*/
}

var obj *db
var o sync.Once

func GetInstance() *db {
	o.Do(func() {
		runtime.Gosched()
		obj = &db{}
		fmt.Println("Crated DB Instance")
	})
	return obj
}

동일한 main func 을 수행한다면?

위와 같은 결과를 얻을 수 있다. 

 

init 함수 활용 하기

- go에서는 패키지 import 간 최초 실행 해주는 함수가 있다. 바로 init() 함수인데 제일 좋아하는 방법이다. 바로 활용해 보자.

type db struct {
	/**
	db 에 해당하는 값 이 저장
	*/
}

var obj *db

func init() {
	obj = &db{}
	fmt.Println("Db created")
}

func GetInstance() *db {
	if obj == nil {
		fmt.Println("Three is no db instance")
	}
	return obj
}

개인적으로는 init 함수를 선호한다. 

 

이러한 싱글턴 디자인 패턴 에는 테스트의 어려움 이 있다. 변수를 아무래도 전역적으로 관리하다 보니 목킹 이 생각보다 어렵다. 

이런 경우 인터페이스를 활용해서 추상화 작업을 통해 작성하는 것도 하나의 방법이 될 수 있다.

 

type MyDB interface {
	DoSomething() // DB 작업을 위한 메서드
}

type db struct {
	// db에 해당하는 값이 저장
}

var obj *db

func init() {
	obj = &db{}
	fmt.Println("DB created")
}

func (d *db) DoSomething() {
	// DB 작업을 수행하는 메서드
}

func GetInstance() MyDB {
	if obj == nil {
		fmt.Println("There is no DB instance")
	}
	return obj
}

 

[프로타입 패턴]

인스턴스는 새 객체를 만들기 위해 자신을 복제(clone)하게 된다.

원칙은 "런타임"에 또 다른 "객체"를 생성한다는 것이다. 다시 말해 이 시점에 가서 클로닝(cloning)을 하는 객체의 "실제 복사본"이 만들어지는 것이다.

 

실행 시간 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. -->

ko.wikipedia.org

 

기존 자바를 사용했던 사람들에게 는 매우 익숙한 패턴처럼 보일 것이다.

모든 자바의 클래스는 Object를 상속하고, 이는 Clone()의 메서드를 제공하게 된다. 즉 객체의 복사를 제공한다.

 

왜 필요할까? 

객체의 정확한 복사본을 만들고자 할 때 필요하다고 정의되어있다. 특히나 Go에서는 slice, pointer에서 예외 상황이 발생한다.

아래의 예제를 보자.

결과 값의 3,4번째 줄을 확인해 보자. 동일한 주소값을 가지고 있다. 분명 새로운 키보드를 만들기 위해서 작성했지만 아래와 같이 동일한 주소값을 가지고 있다.

고에서 슬라이스는 포인터의 래퍼이다. 즉 메모리 주소의 포인터 값을 랩핑 한 배열이다. 당연히 이를 복사하면 우리가 가져오는 값들은 저 포인터 값을 복사한 값들을 들고 오게 된다. 이는 반드시 주의해야 한다.

 

	leopold := &Keyboard{
		Layout: vamilo.Layout,
		Switch: []string{"Gateron Mx Red", "Gateron Mx Blue"},
		KeyCap: vamilo.KeyCap,
	}

위와 같은 새로운 슬라이스를 선언하고 값을 넣어줘야 한다. 

 

아래와 같은 방법을 이용해서 복사하는 방법도 하나의 방법 중 하나이다.

gob를 이용해 스트림화시켜 직렬화 역직렬화시키는 방법을 이용해서 복사를 하는 것도 하나의 방법이 될 수도 있다.

 

Interface의 clone을 선언해서 이용하는 방법을 사용해 보자.

 

이번에는 인터페이스의 클론을 활용해서 인터페이스 자신을 반환하게 설정하였다.
이렇게 설정되면 클라이언트는 복사하고자 하는 대상으로 타입어설션이 가능해지며 유연성 있고, 확장가능한 코드구성이 가능해진다.

func Run() {
	sixty := &Keyboard{"60%", []string{"Cherry MX Blue", "Cherry MX Brown", "Cherry MX Red"}, "DSA"}

	keyboardClone := sixty.Clone().(*Keyboard)

	fmt.Println(keyboardClone)
}

 

[빌더 패턴]

빌더 패턴이란 복합 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴이다.

생성패턴 의 근간이 되는 기본 구조에 대해서 다시 한번 상기하고 넘어가자.

생성패턴 은 구현하는 구현체가 있고 구현체 의 추상화 클래스가 존재한다.

 

빌더패턴 은 다음 과 같은 문제점을 해결하고자 나왔다.

1. 하나의 객체에서 한 개 이상의 생성자 필드를 받게 된다면 , 생성함에 있어 깔끔하고 유지가능한 코드를 작성하는 것이 생각보다 어려울 수 있다.

2. 객체의 생성이후 필드값의 불변성이 요구될 때, 한 개 이상의 다양한 많은 속성을 불변으로 만드는 것은 생각보다 어려울 수 있다.

3. 객체의 복합 생성 로직은 메인 클래스 의 유지 및 확장성에 어려움을 야기할수 있다. 

 

빌더 패턴 을 만들어 보자.

type House struct {
	windows            int
	door               int
	floor              string
	rooms              int
	bathroom           int
	swimmingPoolSize   int
	swimmingPoolHeight int
	gardenSize         int
	gardenFlower       string
	gardenTree         string
	garageSize         int
	garageDoor         string
}

물론 작은 필드 들이지만 이것보다 많아질 수 있고 다양해질 수 있다고 가정해 보자. House를 생성하고자 한다면?

house := &House{4, 1, "wooden", 2, 1, 0, 0, 0, "", "", 0, ""}

이런 식의 코드가 생기게 되며 읽기 매우 어렵고 난해하다.

우선 구현하고자 하는 기본 골격 구조를 다음과 같이 잡고자 한다.

 

정통적인 빌더 패턴에서는 다음과 같은 구조를 가지게 된다. 

생성패턴의 기본 골격인 인터페이스와 구현체를 빌더패턴에서도 동일하게 가져간다. 

이제는 생각보다 많이 익숙해진 구조이고 , 다만 디렉터라는 구현체가 빌더의 총괄적인 핸들링을 담당하고 있다.

구체적인 클래스 설계도를 확인해 보자.

House 빌더라는 인터페이스가 있으며, 그거를 구현하는 각각의 빌더 구조체들을 구현하고 이 각각 의 구조체는 House라는 거대한 

구조체를 상속받아서 사용할 것이다.

이렇게 생성된 HouseBuilder는 BuildDirector라는 구현체에 의해 핸들링되고 관리될 것이다. 

바로 코드로 가보자.

인터페이스는 위와 같이 설정해 주었으며, 아래 있는 의문에 대해 생각해 보자. 최종 구현체에서 코드를 사용할 때 이유를 확인할 수 있다.

인터페이스의 구현을 위해 위와 같이 작성했으며, 특히나 go에서 ide를 사용하고 있다면, 

var _ HouseBuilder = (*HouseNormalBuilder)(nil)

위 코드를 이용해 자동완성을 애용하자.

핵심이 되는 디렉터 구조체를 확인해 보자. 

디렉터 구조체는 하우스 빌더를 생성간의 주입받게 된다. 이렇게 주입된 하우스 빌더를 통해 원하는 집을 디렉터가 정의한 순서에 맞춰

생산할 수 있다. 마치 프레임워크가 작동하듯이 말이다.

 

중간 함수를 확인하게 되면. 을 이용해 죄다 체이닝 해버렸다. 보다 직관적인 코드의 방식 구현을 위해 인터페이스에서 다음과 같이 정의를 했다. 본인 자신을 반환하게 된다면 위와 같은 체이닝 방식이 가능해진다.

 

그래서 클라이언트는 어떻게 이거를 사용할까?

빌더를 각각 주입해 주면서 우리는 대형 객체를 보다 손쉽게 선언하고 사용할 수 있다. 

SwimmingPool House is {windows:40 door:10 floor:wooden rooms:0 bathroom:1 swimmingPoolSize:200 swimmingPoolHeight:180 gardenSize:0 gardenFlower: gardenTree: garageSize:0 garageDoor:}
Normal House is {windows:20 door:20 floor:wooden rooms:0 bathroom:3 swimmingPoolSize:0 swimmingPoolHeight:0 gardenSize:0 gardenFlower: gardenTree: garageSize:0 garageDoor:}

이렇게 코드가 작성되고 클라이언트가 사용한다고 가정할 때 빌더패턴 이 나오게 된 이유를 모두 충족하는가? 

 

1. 하나의 객체에서 한 개 이상의 생성자 필드를 받게 된다면 , 생성함에 있어 깔끔하고 유지가능한 코드를 작성하는 것이 생각보다 어려울 수 있다.

- 각각의 빌더를 주입받아서 정해진 순서에 따라 하우스를 생산한다. 순서의 관리는 디렉터 가 담당하고, 각각의 구현체끼리는 느슨한 결합 형태를 가지게 된다.

 

2. 객체의 생성 이후 필드값의 불변성이 요구될 때, 한 개 이상의 다양한 많은 속성을 불변으로 만드는 것은 생각보다 어려울 수 있다.

- 하우스 구조체 의 필드값은 소문자로 임포트 되지 않는다. 즉 특정 메서드가 제공되지 않는다면? 불변하는 필드들이다.

 

3. 객체의 복합 생성 로직은 메인 클래스 의 유지 및 확장성에 어려움을 야기할 수 있다. 

- 하우스 새로운 건축물 혹은 수영장 집의 업그레이드 함에 있어 어디를 수정하고 어디를 고쳐야 하는지 명확한가?

 

빌더패턴 의 특징을 다시 한번 알고 넘어가자.

- Builder Interface 가 모든 구현체에서 구현 함수를 정의한다.- Concrete Builder에서 Builder Interface를 구현하고 실제 로직을 캡슐화한다.- Director는 옵션적인 부분이며, Builder Interface의 함수를 호출해 Build의 대상을 제공한다.- Product에서는 Build pattern을 사용해 실질적인 인스턴스를 담당한다.

 

어느 정도 패턴을 공부하고, 자바 사람이라면 이 빌더패턴 은 너무나도 친숙한 패턴이 아닐 수 없다.Lombok 이  이 빌더패턴을 정말 잘 활용해서  객체의 생성을 도와준다. 고 와는 달리 자바에서는 필드의 생성자 값들을 하나씩만 넣으면서 초기화하기 위해서? 그에 해당하는 모든 생성자 클래스를 선언해주어야 한다. 창문 만 있는 생성자 클래스, 창문 욕조 가 있는 생성자 클래스 등등

갑자기 이 이야기를 왜 하냐고 의문이 들 수 있다.

House.builder().window().door().build();

롬복에서 는 builder라는 어노테이션을 제공해 주는데 이런 체이닝 방식을 이용해서 제공한다. 왜 이렇게 유명한 라이브러리(github star 11.9k)에서 우리가 지금 까지 배운 인터페이스, 함수 호출 외순서를 정하는 방식이 아닌 위와 같은 체이닝 방법을 활용해서 제공하고 빌더패턴을 적용했다고 할까? 우리가 배운 생성패턴의 기본 골격 구조는 인터페이스에 의존하는 구현체인데 방식이 많이 다르지 않은가?

 

이런 부분에 있어 어느 정도 유연성을 둔다고 한다. 그래서 글 초창기에 전통적인 빌더패턴이라는 표현을 사용했다.

빌더패턴의 목적  "유연하고, 효과적인 방법으로 객체 생성을 제공해야한다." 이다. 

처음 제공된 코드처럼 마치 프레임워크 같이 작성하는 전통적인 빌더패턴 이 있고, 롬복 과 같이 다양한 라이브러리 에서는 사용자에게 보다 많은 유연성을 주기 위해 최근에는 이런 인터페이스 가  생략된 간단한 빌더패턴을 제공한다고 한다.

아래 코드를 보자.

 

더보기
package simpleBuilder

type Person struct {
	StreetAddress, Postcode, City string

	CompanyName, Position string
	AnnualIncome          int
}

type PersonBuilder struct {
	person *Person
}

type PersonAddressBuilder struct {
	PersonBuilder
}

func NewPersonBuilder() *PersonBuilder {
	return &PersonBuilder{&Person{}}
}

func (b *PersonBuilder) Lives() *PersonAddressBuilder {
	return &PersonAddressBuilder{*b}
}

func (b *PersonBuilder) Works() *PersonJobBuilder {
	return &PersonJobBuilder{*b}
}

func (p *PersonBuilder) Builder() *Person {
	return p.person
}

func (p *PersonAddressBuilder) At(street string) *PersonAddressBuilder {
	p.person.StreetAddress = street
	return p
}
func (p *PersonAddressBuilder) In(city string) *PersonAddressBuilder {
	p.person.City = city
	return p
}
func (p *PersonAddressBuilder) PostCode(code string) *PersonAddressBuilder {
	p.person.Postcode = code
	return p
}

type PersonJobBuilder struct {
	PersonBuilder
}

func (p *PersonJobBuilder) At(comapny string) *PersonJobBuilder {
	p.person.CompanyName = comapny
	return p
}
func (p *PersonJobBuilder) Asa(job string) *PersonJobBuilder {
	p.person.Position = job
	return p
}
func (p *PersonJobBuilder) Earn(income int) *PersonJobBuilder {
	p.person.AnnualIncome = income
	return p
}

 

 

단순하게 큰구조체 안에 빌더2개를 더만들어서 나아가는 방식으로 작성했다.

그러면 다음과 같은 클라이언트 호출을 가져갈수 있다.

 

다시말해 생성패턴 의 기본 골격 구조가 항상 지켜져야 만 패턴이 성립한다는 생각을 이번기회에 버리자. 

이렇게 됨에 우리는 2가지 선택지가 생긴다.

 

1. 각단계의 순서를 강제해 객체 의 생성 로직을 명확히 하는 전통적인 빌더패턴의 방법

2. 사용자 에게 자유로운 방식의 유연성을 주는 방식의 빌더패턴 방법 

 

상황에 맞는 유연한 코드를 작성하자.

 

코드전문

더보기
프로젝트 구조
package houseImpl

type House struct {
	windows            int
	door               int
	floor              string
	rooms              int
	bathroom           int
	swimmingPoolSize   int
	swimmingPoolHeight int
	gardenSize         int
	gardenFlower       string
	gardenTree         string
	garageSize         int
	garageDoor         string
}


package houseImpl

type HouseBuilder interface {
	Window() HouseBuilder
	Door() HouseBuilder
	Floor() HouseBuilder
	Rooms() HouseBuilder
	BathRoom() HouseBuilder
	GetHouse() House
}


package houseImpl

type HouseGarageBuilder struct {
	House
}

func NewHouseGarageBuilder() *HouseGarageBuilder {
	return &HouseGarageBuilder{}
}

func (h *HouseGarageBuilder) GetHouse() House {
	h.setGarage()
	return House{
		windows:    h.windows,
		door:       h.door,
		floor:      h.floor,
		rooms:      h.rooms,
		bathroom:   h.bathroom,
		garageDoor: h.garageDoor,
		garageSize: h.garageSize,
	}
}
func (h *HouseGarageBuilder) setGarage() {
	h.garageSize = 20000
	h.garageDoor = "Marble"
}

func (h *HouseGarageBuilder) Window() HouseBuilder {
	h.windows = 40
	return h
}

func (h *HouseGarageBuilder) Door() HouseBuilder {
	h.door = 10
	return h
}

func (h *HouseGarageBuilder) Floor() HouseBuilder {
	h.floor = "Marble"
	return h
}

func (h *HouseGarageBuilder) Rooms() HouseBuilder {
	h.rooms = 7
	return h
}

func (h *HouseGarageBuilder) BathRoom() HouseBuilder {
	h.bathroom = 5
	return h
}


package houseImpl

type HouseGardenBuilder struct {
	House
}

func NewHouseGardenBuilder() *HouseGardenBuilder {
	return &HouseGardenBuilder{}
}

func (h *HouseGardenBuilder) GetHouse() House {
	h.setGarden()
	return House{
		windows:      h.windows,
		door:         h.door,
		floor:        h.floor,
		rooms:        h.rooms,
		bathroom:     h.bathroom,
		gardenSize:   h.gardenSize,
		gardenFlower: h.gardenFlower,
		gardenTree:   h.gardenTree,
	}
}
func (h *HouseGardenBuilder) setGarden() {
	h.gardenSize = 400
	h.gardenFlower = "Rose and Sunflower"
	h.gardenTree = "White Oak Tree"
}

func (h *HouseGardenBuilder) Window() HouseBuilder {
	h.windows = 40
	return h
}

func (h *HouseGardenBuilder) Door() HouseBuilder {
	h.door = 10
	return h
}

func (h *HouseGardenBuilder) Floor() HouseBuilder {
	h.floor = "Marble"
	return h
}

func (h *HouseGardenBuilder) Rooms() HouseBuilder {
	h.rooms = 7
	return h
}

func (h *HouseGardenBuilder) BathRoom() HouseBuilder {
	h.bathroom = 5
	return h
}

package houseImpl

type HouseNormalBuilder struct {
	House
}

func NewHouseNormalBuilder() *HouseNormalBuilder {
	return &HouseNormalBuilder{}
}

func (h *HouseNormalBuilder) GetHouse() House {
	return House{
		windows:  h.windows,
		door:     h.door,
		floor:    h.floor,
		rooms:    h.rooms,
		bathroom: h.bathroom,
	}
}

func (h *HouseNormalBuilder) Window() HouseBuilder {
	h.windows = 20
	return h
}

func (h *HouseNormalBuilder) Door() HouseBuilder {
	h.door = 20
	return h
}

func (h *HouseNormalBuilder) Floor() HouseBuilder {
	h.floor = "wooden"
	return h
}

func (h *HouseNormalBuilder) Rooms() HouseBuilder {
	h.rooms = 5
	return h
}

func (h *HouseNormalBuilder) BathRoom() HouseBuilder {
	h.bathroom = 3
	return h
}

package houseImpl

type HouseSwimmingPoolBuilder struct {
	House
}

func NewHouseSwimmingPoolBuilder() *HouseSwimmingPoolBuilder {
	return &HouseSwimmingPoolBuilder{}
}

func (h *HouseSwimmingPoolBuilder) GetHouse() House {
	h.setSwimmingPool()
	return House{
		windows:            h.windows,
		door:               h.door,
		floor:              h.floor,
		rooms:              h.rooms,
		bathroom:           h.bathroom,
		swimmingPoolHeight: h.swimmingPoolHeight,
		swimmingPoolSize:   h.swimmingPoolSize,
	}
}
func (h *HouseSwimmingPoolBuilder) setSwimmingPool() {
	h.swimmingPoolSize = 200
	h.swimmingPoolHeight = 180
}

func (h *HouseSwimmingPoolBuilder) Window() HouseBuilder {
	h.windows = 40
	return h
}

func (h *HouseSwimmingPoolBuilder) Door() HouseBuilder {
	h.door = 10
	return h
}

func (h *HouseSwimmingPoolBuilder) Floor() HouseBuilder {
	h.floor = "wooden"
	return h
}

func (h *HouseSwimmingPoolBuilder) Rooms() HouseBuilder {
	h.rooms = 2
	return h
}

func (h *HouseSwimmingPoolBuilder) BathRoom() HouseBuilder {
	h.bathroom = 1
	return h
}

package main

import (
	"builder/houseImpl"
	"builder/simpleBuilder"
	"fmt"
)

type BuildDirector struct {
	builder houseImpl.HouseBuilder
}

func NewBuildDirector(builder houseImpl.HouseBuilder) *BuildDirector {
	return &BuildDirector{builder: builder}
}

func (b *BuildDirector) BuildHouse() houseImpl.House {
	return b.builder.Window().Door().Floor().BathRoom().GetHouse()
}
func (b *BuildDirector) changeBuilder(builder houseImpl.HouseBuilder) {
	b.builder = builder
}

func main() {
	builder := houseImpl.NewHouseSwimmingPoolBuilder()
	director := NewBuildDirector(builder)

	a := director.BuildHouse()
	fmt.Printf("SwimmingPool House is %+v\n", a)
	director.changeBuilder(houseImpl.NewHouseNormalBuilder())
	b := director.BuildHouse()
	fmt.Printf("Normal House is %+v\n", b)

	pb := simpleBuilder.NewPersonBuilder()
	pb.
		Lives().
		At("123 London").
		In("London").
		PostCode("Mortgatan6").
		Works().
		At("Plea").
		Asa("Programmer").
		Earn(12300)

	person := pb.Builder()
	fmt.Println(person)

}

 

 

[추상 팩토리 메서드 패턴]

 다양한 구성 요소 별로 '객체의 집합'을 생성해야 할 때 유용하다. 이 패턴을 사용하여 상황에 알맞은 객체를 생성할 수 있다.

 

지난번에 본 생성패턴 이 가지고 있는 기본적인 형태이다. 기억하고 넘어가자.

추상팩토리는 말 그대로 객체의 집합을 생성할 때 사용한다.

 

이번 예제는 파스타 를 들어보기로 하자. 지난번 배운 팩토리 메서드 패턴과 함께 조합해서 사용해 보자.

파스타의 핵심 이 되는 면부터 살펴보자.

면을 위한 팩토리 메서드를 적용하는 다이어그램을 먼저 확인해 본다면 

누들팩토리 와 그 구현체로 생산자와 실제 제품 들과의 관계를 분리시켰다

noodle의 중간 abstract를 unimport 구조체가 되어 외부 패키지에서는 접근이 불가능하지만 NoodleFactory를 제공하는 제공자가 있다면 접근가능하도록 설계하였다.

package noodle

type NoodleFacotry interface {
	CreateNoodle(name string) Noodle
}

type noodleFactory struct{}

func (k *noodleFactory) CreateNoodle(name string) Noodle {
	switch name {
	case "Fusilli":
		return &Fusilli{
			noodle{name: "Korean Fusilli"},
		}
	case "Penne":
		return &Penne{
			noodle{name: "Korean Penne"},
		}
	case "Linguine":
		return &Linguine{
			noodle{name: "Korean Linguine"},
		}
	default:
		return &Spaghetti{
			noodle{name: "Korean Spaghetti"},
		}
	}
}

func NewNoodleFactory() NoodleFacotry {
	return &noodleFactory{}
}

누들 패키지에서는 NewNoodleFacotry를 통해 하나의 팩토리만 던져주게 된다. 현재는 하나의 값 만 반환하게 되지만 switch-case를 적용해 특정 팩토리 생산의 명령을 주입받는 방법도 존재한다. 

package noodle

import (
	"blogEx/abstoractFactory"
	"fmt"
)

type Noodle interface {
	abstoractFactory.Vegi
	Boiling()
}

type noodle struct {
	name string
}

func (n *noodle) Boiling() {
	fmt.Println("면 을 삶는 중입니다..")
}

func (n *noodle) String() string {
	return fmt.Sprintf("파스타 면 은 %s", n.name)
}

동일 패키지에 Noodle 인터페이스에 대한 정의와 구현체를 작성해 주었다. 구현체 내부에는 추가적인 인터페이스를 인자로 받아서

이 구현체를 상속하는 하위 구조체에서 구현을 강제하도록 만들었다. 

package noodle

type Fusilli struct {
	noodle
}

func (f *Fusilli) IsVegi() bool {
	return false
}

type Linguine struct {
	noodle
}

func (f *Linguine) IsVegi() bool {
	return false
}

type Spaghetti struct {
	noodle
}

func (f *Spaghetti) IsVegi() bool {
	return false
}

type Penne struct {
	noodle
}

func (f *Penne) IsVegi() bool {
	return true
}

동일 패키지 의 하위 제품의 구현을 작성했다. 기본적인 상속과, 더불어 IsVegi()의 인터페이스를 각각 구현해 주었다.

팩토리 메서드 지난 글(https://guiwoo.tistory.com/category/Go%20Dive/Design%20Pattern)

 

우리 는 파스타를 만드는 데 있어 이런 재료만 필요한 것 이 아닌 추가적인 재료가 필요하다. 마저 확인해 보자. 위와 동일한 구조를 가진 소스이다.

이렇게 동일한 구성을 가지고 Vegitables와 topping 도 선언해 주자.

 

이렇게 생성된 팩토리 들을 각각 의 방식으로 사용할 수도 있지만 보다 클라이언트의 특정 행동에 범위를 제한하기 위해 우리는 집합체를 생성하게 된다면? 클라이언트는 이 집합체를 얻음으로써, 각각의 팩토리를 가져와 객체의 구성을 만들수 있다는 장점이 있다. 아래 그림을 보자.

각각의 팩토리 를 조합하는 파스타 키트를 운용하는 것이다. 각각의 팩토리 인터페이스를 반환해 주는 방식이다 이렇게 했을 때 장점이 무엇인가?

파스타 키트는 다양한 팩토리를 주입받을 수 있으며 심지어 각각의 팩토리에서 또한 다양한 방식으로 팩토리 제공이 가능하다.

누들 팩토리에서 이탈리안 누들을 줄수도 있고, 한국 누들 을 줄수도 있는 등 팩토리를 갈아 끼울 수 있다는 장점 이 존재한다.

코드를 확인해 보자.

type PastaKit interface {
	Noodles() noodle.NoodleFacotry
	Sources() source.SourceFactory
	Vegetables() vegitable.VegetableFactory
	Toppings() topping.ToppingFactory
}

type GuiwooPastKit struct{}

func (p *GuiwooPastKit) Noodles() noodle.NoodleFacotry {
	return noodle.NewNoodleFactory()
}
func (p *GuiwooPastKit) Sources() source.SourceFactory {
	return source.NewSourceFactory()
}
func (p *GuiwooPastKit) Vegetables() vegitable.VegetableFactory {
	return vegitable.NewVegetableFactory()
}
func (p *GuiwooPastKit) Toppings() topping.ToppingFactory {
	return topping.NewToppingFactory()
}

func NewGuiwooPastaKit() PastaKit {
	return &GuiwooPastKit{}
}

파스타 키트에서는 추가적인 팩토리 메서드 적용이 아닌 생성패턴 의 기본 골격만 적용하였다. 인터페이스 와 그것에 대한 구현체

이렇게 선언된 파스타 키트를 이용해 클라이언트는 어떻게 사용할까?

func main() {
	var kit pastakit.PastaKit = pastakit.NewGuiwooPastaKit()
	noodle := kit.Noodles().CreateNoodle("Fusilli")
	source := kit.Sources().CreateSource("Cream")
	topping := kit.Toppings().CreateTopping("Chicken")
	vegetables := kit.Vegetables().CreateVegetable("Paprika")

	fmt.Println("pasta 끝")

	noodle.Boiling()
	fmt.Println(noodle)
	source.Prepare()
	fmt.Println(source)
	topping.PutOn()
	fmt.Println(topping)
	vegetables.Chop()
	fmt.Println(vegetables)
}

파스타 키트를 받아서 정말 다양한 파스타 의 종류 를 만들어 낼수있다. 

만약 원산지가 바뀌어 버렸다면? 그냥 팩토리 내부 구현체 만 추가적으로 작성해서 넣어주면 원산지가 바뀐 파스타 키트 를 클라이언트에서는 지속적으로 사용이 가능하다.


다시 말해 클라이언트는 파스타 키트 의 원산지 재료가 무엇이든 지 간에 궁금하지 않다. 제공되는 파스타 키트에서는 누들 소스 토핑 그리고 야채들이 제공되어야 하기 때문에 정말 코드 를 호출 하는 호출 부와 제공 하는 프로덕트 부 의 느슨한 결합이 완성된 형태라고 볼수 있다.

 

이렇게 느슨한 결합이 주는 장점으로는 테스트 코드의 작성 즉 파스타 키트 에서 생산되는 각각의 팩토리에 대해 테스트하기 쉬우며( 목 객체를 만들기 매우 편하다.), 변화되는 코드에 있어 손쉽게 교체 및 추가할 수 있다는 점이 있다. 

그러면 장점만 있는가? 위의 단순한 코드 제공에만 무려 14개의 파일과 5개의 디렉터리로 나누어져 있다. 다시 말해 복잡하다. 이해하기 어려울 수 있으며 코드의 양이 정말 많아진다는 단점이 존재한다.

 

그래서 그러면 팩토리 메서드 패턴 이랑 추상 팩토리랑 도대체 무엇이 다른가 에 의한 의문점이 남을수 있다.

팩토리 메소드 패턴 은? 상속을 통한 객체 생성과 오직 하나의 제품을 생산하는 쪽에 키워드가 맞춰져 있고,

추상 팩토리는? 객체의 구성을 통해 필요 객체 생성을 만들어 낼 때 즉 제품의 구성 쪽에 키워드가 맞춰져 있다.

 

그렇다면 이런 다른 키워드를 포워딩하고 있는데 언제 사용해야 하는가?

제품군을 만들어야 한다면? 추상팩토리

클라이언트 코드와 인스턴스의 생성 클래스를 분리시켜야 한다면? 팩토리 메서드 패턴을 활용하면 된다.


통합 구조


통합코드

· Noodle Package

더보기
package noodle
### 팩토리 ####
type NoodleFacotry interface {
	CreateNoodle(name string) Noodle
}

type noodleFactory struct{}

func (k *noodleFactory) CreateNoodle(name string) Noodle {
	switch name {
	case "Fusilli":
		return &Fusilli{
			noodle{name: "Korean Fusilli"},
		}
	case "Penne":
		return &Penne{
			noodle{name: "Korean Penne"},
		}
	case "Linguine":
		return &Linguine{
			noodle{name: "Korean Linguine"},
		}
	default:
		return &Spaghetti{
			noodle{name: "Korean Spaghetti"},
		}
	}
}

func NewNoodleFactory() NoodleFacotry {
	return &noodleFactory{}
}
### 하위 상속 구조체 ###
package noodle

type Fusilli struct {
	noodle
}

func (f *Fusilli) IsVegi() bool {
	return false
}

type Linguine struct {
	noodle
}

func (f *Linguine) IsVegi() bool {
	return false
}

type Spaghetti struct {
	noodle
}

func (f *Spaghetti) IsVegi() bool {
	return false
}

type Penne struct {
	noodle
}

func (f *Penne) IsVegi() bool {
	return true
}
### 누들 구현체 ###
package noodle

import (
	"blogEx/abstoractFactory"
	"fmt"
)

type Noodle interface {
	abstoractFactory.Vegi
	Boiling()
}

type noodle struct {
	name string
}

func (n *noodle) Boiling() {
	fmt.Println("면 을 삶는 중입니다..")
}

func (n *noodle) String() string {
	return fmt.Sprintf("파스타 면 은 %s", n.name)
}

·SourcePackage

더보기
package source

type SourceFactory interface {
	CreateSource(name string) Source
}

type sourceFactory struct{}

func (t *sourceFactory) CreateSource(name string) Source {
	switch name {
	case "Tomato":
		return &Tomato{
			source{name: "Lagu source"},
		}
	case "Cream":
		return &Cream{
			source{name: "Creamy milk"},
		}
	case "Arabiata":
		return &Arabiata{
			source{name: "Arabiata"},
		}
	default:
		return &Oil{
			source{name: "Olive Oil"},
		}
	}
}

func NewSourceFactory() SourceFactory {
	return &sourceFactory{}
}

### source ###
package source

import (
	"blogEx/abstoractFactory"
	"fmt"
)

type Source interface {
	abstoractFactory.Vegi
	Prepare()
}

type source struct {
	name string
}

func (s *source) Prepare() {
	fmt.Println(s.name, "준비 완료")
}

func (s *source) String() string {
	return fmt.Sprintf("소스 는 %s 입니다.", s.name)
}

### 하위 상속 구조체 ###
package source

type Tomato struct {
	source
}

func (t *Tomato) IsVegi() bool {
	return false
}

type Cream struct {
	source
}

func (c *Cream) IsVegi() bool {
	return false
}

type Oil struct {
	source
}

func (o *Oil) IsVegi() bool {
	return true
}

type Arabiata struct {
	source
}

func (a *Arabiata) IsVegi() bool {
	return true
}

· Topping

더보기
package topping

type ToppingFactory interface {
	CreateTopping(name string) Topping
}

type toppingFactory struct{}

func (t *toppingFactory) CreateTopping(name string) Topping {
	switch name {
	case "Chicken":
		return &Chicken{
			topping{name: "chicken from South Korea"},
		}
	case "Beef":
		return &Beef{
			topping{name: "beef from Australia"},
		}
	case "Pork":
		return &Pork{
			topping{name: "pork from USA"},
		}
	default:
		return &Scampi{
			topping{name: "Scampi from Norway"},
		}
	}
}

func NewToppingFactory() ToppingFactory {
	return &toppingFactory{}
}

### topping ###
package topping

import (
	"fmt"
)

type Topping interface {
	PutOn()
}

type topping struct {
	name string
}

func (t *topping) PutOn() {
	fmt.Println(t.name, "Putting on the pasta")
}

func (t *topping) String() string {
	return fmt.Sprintf("토핑은 %s 입니다.", t.name)
}

### 하위 상속 구조체 ###
package topping

type Pork struct {
	topping
}

type Beef struct {
	topping
}

type Scampi struct {
	topping
}

type Chicken struct {
	topping
}

· Vegetable

더보기
package vegitable

type VegetableFactory interface {
	CreateVegetable(name string) Vegetable
}

type vegetableFactory struct{}

func (j *vegetableFactory) CreateVegetable(name string) Vegetable {
	switch name {
	case "SpringOnion":
		return &SpringOnion{
			vegetable{name: "Japanese Onion"},
		}
	case "Onion":
		return &Onion{
			vegetable{name: "Japanese Onion"},
		}
	case "Paprika":
		return &Paprika{
			vegetable{name: "Japanese Paprika"},
		}
	default:
		return &Garlic{
			vegetable{name: "Japanese Garlic"},
		}
	}
}

func NewVegetableFactory() VegetableFactory {
	return &vegetableFactory{}
}

### vegetable ###
package vegitable

import (
	"fmt"
)

type Vegetable interface {
	Chop()
	Slice()
}

type vegetable struct {
	name string
}

func (v *vegetable) Chop() {
	fmt.Printf("%s chopped", v.name)
}

func (v *vegetable) Slice() {
	fmt.Printf("%s sliced", v.name)
}


### 하위 상속 구조체 ###
package vegitable

type SpringOnion struct {
	vegetable
}

type Onion struct {
	vegetable
}

type Paprika struct {
	vegetable
}

type Garlic struct {
	vegetable
}

·  Pastakit

더보기
package pastakit

import (
	"blogEx/abstoractFactory/noodle"
	"blogEx/abstoractFactory/source"
	"blogEx/abstoractFactory/topping"
	"blogEx/abstoractFactory/vegitable"
)

type PastaKit interface {
	Noodles() noodle.NoodleFacotry
	Sources() source.SourceFactory
	Vegetables() vegitable.VegetableFactory
	Toppings() topping.ToppingFactory
}

type GuiwooPastKit struct{}

func (p *GuiwooPastKit) Noodles() noodle.NoodleFacotry {
	return noodle.NewNoodleFactory()
}
func (p *GuiwooPastKit) Sources() source.SourceFactory {
	return source.NewSourceFactory()
}
func (p *GuiwooPastKit) Vegetables() vegitable.VegetableFactory {
	return vegitable.NewVegetableFactory()
}
func (p *GuiwooPastKit) Toppings() topping.ToppingFactory {
	return topping.NewToppingFactory()
}

func NewGuiwooPastaKit() PastaKit {
	return &GuiwooPastKit{}
}

· ClientSide

더보기
package main

import (
	"blogEx/abstoractFactory/pastakit"
	"fmt"
)

func main() {
	var kit pastakit.PastaKit = pastakit.NewGuiwooPastaKit()
	noodle := kit.Noodles().CreateNoodle("Fusilli")
	source := kit.Sources().CreateSource("Cream")
	topping := kit.Toppings().CreateTopping("Chicken")
	vegetables := kit.Vegetables().CreateVegetable("Paprika")

	fmt.Println("pasta 끝")

	noodle.Boiling()
	fmt.Println(noodle)
	source.Prepare()
	fmt.Println(source)
	topping.PutOn()
	fmt.Println(topping)
	vegetables.Chop()
	fmt.Println(vegetables)
}

출처:

https://en.wikipedia.org/wiki/Abstract_factory_pattern

 

Abstract factory pattern - Wikipedia

From Wikipedia, the free encyclopedia Software design pattern The abstract factory pattern in software engineering is a design that provides a way to create families of related objects without imposing their concrete classes, by encapsulating a group of in

en.wikipedia.org

https://refactoring.guru/ko/design-patterns/abstract-factory

 

추상 팩토리 패턴

/ 디자인 패턴들 / 생성 패턴 추상 팩토리 패턴 다음 이름으로도 불립니다: Abstract Factory 의도 추상 팩토리는 관련 객체들의 구상 클래스들을 지정하지 않고도 관련 객체들의 모음을 생성할 수 있

refactoring.guru

https://sourcemaking.com/design_patterns/abstract_factory

 

Design Patterns and Refactoring

Design Patterns and Refactoring articles and guides. Design Patterns video tutorials for newbies. Simple descriptions and full source code examples in Java, C++, C#, PHP and Delphi.

sourcemaking.com

https://www.baeldung.com/java-abstract-factory-pattern

https://www.digitalocean.com/community/tutorials/abstract-factory-design-pattern-in-java

 

Abstract Factory Design Pattern in Java | DigitalOcean

 

www.digitalocean.com

 

생성 패턴 이란?

소프트웨어 엔지니어링에서 생성 디자인 패턴은 개체 생성 메커니즘을 다루는 디자인 패턴으로 상황에 적합한 방식으로 개체를 생성하려고 합니다. 개체 생성의 기본 형태는 디자인 문제를 일으키거나 디자인에 복잡성을 더할 수 있습니다. 생성 디자인 패턴은 이 객체 생성을 어떻게든 제어함으로써 이 문제를 해결합니다. --wikipeda--

어떤 아이디어에 착안해서 패턴으로 자리 잡았는가?

2가지 의 메인 콘셉트가 있다. 하나는 시스템 이 사용하는 구체적인 클래스에 대해 캡슐화하는 것이고, 다른 하나는 이런 구체적인 클래스의 인스턴스 생성 결합되는 방식을 숨기는 것이다.

 

나아가 생성패턴 은 클래스와 객체 생성 패턴으로 분류될 수 있다. 객체 생성 패턴은 객체를 , 클래스 생성 패턴은 클래스 인스턴스화를 

처리한다. 

 

Common Structure

왼쪽 은 대부분의 생성 패턴이 공통적으로 가지고 있는 간단한 클래스 다이어그램이다.
다른 생성 패턴에는 추가 및 다른 참여 클래스가 필요하다.

 

간단 한 설명을 보자면

 

· Creator = 개체의 인터페이스를 선언하고, 반환값을 준다. 

· Concrete Creator = 개체 인터페이스를 구현하는 클래스이다.

 

이에 따라 잘 알려진 생성 디자인 패턴은 

추상 팩토리, 빌더, 팩토리 메서드, 프로토 타입, 싱글톤 패턴으로 나뉘고 이중 팩토리 메서드 패턴부터 보자.

 

[팩토리 메서드 패턴]

클래스 기반 프로그래밍에서 정확한 클래스를 지정하지 않고도 객체 생성 문제를 처리하는 방식이다. 생성자를 호출하는 대신

인터페이스에 지정되고 자식클래스에 의해 구현되거나 기본,파생 클래스 에 의해 선택적으로 구현되는 방식이다.

 

"객체 생성을 공장 클래스, 함수로 캡슐화 처리하여 대신 생성 하게 하는 생성 디자인 패턴이다."

"객체 생성에 필요한 과정을 템플릿처럼 미리 구성해 놓고 , 객체 생성에 관한 전, 후 처리를 통해 객체를 유연하게 만들 수 있다"

 

아래와 같은 문제에 직면하게 된다면? 팩토리 메서드 패턴을 적용해 보면 최적의 선택이 될 수 있다.

  • 서브 클래스가 인스턴스 화 할 클래스를 재정의하려면 어떻게 해야 할까?
  • 클래스는 어떻게 인스턴화를 하위 클래스에 위임할 수 있을까?

의 주된 문제를 해결하는 설루션이 팩토리 메서드 패턴이다. 

 

바로 가보자.

피자 가게를 운영한다고 가정해보자. 주문에 해당하는 코드를 아래와 같이 정의해 보자.

type Pizza struct{}

func (p *Pizza) prepare() {
	fmt.Println("Prepare")
}
func (p *Pizza) bake() {
	fmt.Println("Bake")
}
func (p *Pizza) cut() {
	fmt.Println("cut")
}
func (p *Pizza) box() {
	fmt.Println("boxing")
}

func orderPizza(t string) Pizza {
	var p Pizza	

	switch t {
	case "Cheese":
		// 치즈 피자가 p 가 된다.
	case "Hawaiian":
		// 하와이안 피자가 p 가 된다
	case "Margherita":
		// 마르게리타 피자가 p 가 된다.
	default:
		// 디폴트 피자 는 햄 피자가 p 가 된다.
	}
	
	p.prepare()
	p.bake()
	p.cut()
	p.box()

	return p
}

이 코드의 문제점 만약 새로운 피자가 추가되거나 삭제되거나 하게 된다면 기존 코드 의 변경이 필요하다. 즉 저 orderPizza 부분 이 문제가 된다는 소리이다.  Switch에 해당하는 부분을 캡슐화해보자.

더보기
package main

import "fmt"

type Pizza interface {
	prepare()
	bake()
	cut()
	box()
}

type pizza struct {
	name string
}

func (p *pizza) prepare() {
	fmt.Println("피자 소스, 토핑, 치즈 준비중 .....")
	fmt.Println("오븐 달구는중 .......")
	time.Sleep(time.Millisecond * 500)
}
func (p *pizza) bake() {
	fmt.Println("피자 굽는중 ....")
	time.Sleep(time.Second)
}
func (p *pizza) cut() {
	fmt.Println("피자 자르는중 .........")
}
func (p *pizza) box() {
	fmt.Printf("[%s] 박스에 넣기 완료\n", p.name)
}

type Cheese struct {
	pizza
}
type Hawaiian struct {
	pizza
}
type Margherita struct {
	pizza
}
type Ham struct {
	pizza
}

type FactoryPizza struct {
	p Pizza
}

func (f *FactoryPizza) createPizza(t string) Pizza {
	switch t {
	case "Cheese":
		// 치즈 피자가 p 가 된다.
		f.p = &Cheese{pizza{"치즈 피자"}}
	case "Hawaiian":
		// 하와이안 피자가 p 가 된다
		f.p = &Cheese{pizza{"하와이안 피자"}}
	case "Margherita":
		// 마르게리타 피자가 p 가 된다.
		f.p = &Cheese{pizza{"마르게리타 피자"}}
	default:
		// 디폴트 피자 는 햄 피자가 p 가 된다.
		f.p = &Cheese{pizza{"햄 피자"}}
	}
	return f.p
}

조금 많은 과정이 들어갔지만 하나씩 차근차근 살펴보자.

피자에 해당하는 구조체 들을 각각 선언했으며, 피자 인터페이스 와 중간 구현체 그리고 상속을 받는 구조이다.

이렇게 함에 따라 최하위 구현체들에게 생성의 책임을 위임할 수 있으며 중간구현체 가 있음으로 공통되는 부분을 제거해서

사용할 수 있다. 

 

이에 대해 클라이언트 코드 도 수정해 보자.

type PizzaStore struct {
	factory FactoryPizza
}

func (ps *PizzaStore) orderPizza(pizza string) Pizza {
	p := ps.factory.createPizza(pizza)

	p.prepare()
	p.bake()
	p.cut()
	p.box()
	return p
}

팩토리를 피자 스토어에 던져주고 그걸 이용해 피자만 생성해 주면 된다.

코드를 보면 희한한 감이 생긴다 그냥 스위치 부분을 아래 구조체에 그냥 넘긴 게 아닌가? 뭐가 다른지 모를 수도 있다.

만약 이 피자

팩토리를 사용하는 곳이 정말 많고 생성된 피자의 객체를 받아와서 설명하고, 가격을 알려주는 함수가 추가적으로 있다면 하위클래스 모든 곳에서 이에 해당하는 함수를 전부 구현해야 합니다.

 

가게를 확장해 보자. 새로운 지점과 서로 다른 피자를 제공하는 경우가 생긴다면?

단순하게 생각해 본다면 factoryzPizza를 여러 개 만들면 되지 않을까? 바로 가보자.

더보기

메인 타입

type PizzaStore interface {
	orderPizza(name string) Pizza //최상위 인터페이스 강제할 함수
}
type PizzaProducer interface {
	createPizza(name string) Pizza //인터페이스 구현체 에서 사용될 함수 를 강제할 부분
}

type pizzaStore struct {
	p  Pizza
	pp PizzaProducer // 최초 구현체 생성시 인자를 주입받기 위해 생성
}

func (p *pizzaStore) orderPizza(name string) Pizza {
	p.p = p.pp.createPizza(name)

	p.p.prepare()
	p.p.bake()
	p.p.cut()
	p.p.box()

	return p.p
}

 하위 구현체

type NyPizza struct {
	pizzaStore
}

func (n *NyPizza) createPizza(t string) Pizza {
	switch t {
	case "Cheese":
		// 치즈 피자가 p 가 된다.
		n.p = &Cheese{pizza{"뉴욕 스타일 치즈 피자"}}
	case "Hawaiian":
		// 하와이안 피자가 p 가 된다
		n.p = &Cheese{pizza{"뉴욕 스타일 하와이안 피자"}}
	case "Margherita":
		// 마르게리타 피자가 p 가 된다.
		n.p = &Cheese{pizza{"뉴욕 스타일 마르게리타 피자"}}
	default:
		// 디폴트 피자 는 햄 피자가 p 가 된다.
		n.p = &Cheese{pizza{"뉴욕 스타일 햄 피자"}}
	}
	return n.p
}

type LAPizza struct {
	pizzaStore
}

func (n *LAPizza) createPizza(t string) Pizza {
	switch t {
	case "Cheese":
		// 치즈 피자가 p 가 된다.
		n.p = &Cheese{pizza{"LA 스타일 치즈 피자"}}
	case "Hawaiian":
		// 하와이안 피자가 p 가 된다
		n.p = &Cheese{pizza{"LA 스타일 하와이안 피자"}}
	case "Margherita":
		// 마르게리타 피자가 p 가 된다.
		n.p = &Cheese{pizza{"LA 스타일 마르게리타 피자"}}
	default:
		// 디폴트 피자 는 햄 피자가 p 가 된다.
		n.p = &Cheese{pizza{"LA 스타일 햄 피자"}}
	}
	return n.p
}

최종 클라이언트 코드

func main() {
	a := &pizzaStore{pp: &NyPizza{}} // 피자 의 프로듀서 를 각각 주입 해준다.
	b := &pizzaStore{pp: &LAPizza{}} 

	a.orderPizza("Cheese")
	fmt.Println()
	b.orderPizza("Hawaiian")
}

이렇게 특정 피자 가게 의 주입을 넣어줄 수 있으면서 피자 생성이 가능해진다. 이렇게 생산된 피자의 제조 방법 은 모두 캡슐화되어 관리된다. 그림으로 보면 아래와 같다.

이렇게 병렬 적으로 구성된 생산자, 제품 클래스는 무한히 확장 가능한 형태가 된다.

 

"팩토리 메서드 패턴은 객체를 생성할 때 피룡한 인터페이스를 만듭니다."

"어떤 클래스의 인스턴스를 만들지는 서브 클래스에서 결정합니다. "

 

크리에이터에 해당하는 부분은 실질적인 제품의 생산이 아닌 어떤 방식으로 만들어야 하는지에 대한 방식만 결정하고 있다.

이에 반해 제품에서는 실질적은 인스턴스의 반환 이 일어나고 있다.

다시 말해 사용하는 서브클래스에 따라 반환되는 객체 인스턴스가 결정된다.

 

마지막 클라이언트 호출부에서 고민이 생긴다.

함수형태로 반환해서 이름을 주입하는 방법 도 하나의 좋은 방법이 될 거 같아 공유하고자 한다.

type PizzaStore interface {
	orderPizza(p PizzaProducer) func(name string) Pizza
}

func main() {
	// 호출 /....
    var ps PizzaStore
   	pa.orderPizza(&NyPizza{})("cheese")
    
    a := &pizzaStore{pp: &NyPizza{}}
    a.orderPizza("cheese")
}

구현의 방식은 중요하지 않다. 중요한 부분은 생성하는 서브 클래스 의 생성자 주입이고 그거의 형태를 최초 생성 시 넣어주느냐

아니면 함수 호출 과정에 넣어주느냐의 차이이다.


단 이런 추상 팩토리 패턴은 가끔씩 안티패턴으로 불리기도 한다. 왜?

  1. 과도한 추상화: 추상화는 객체 생성에 대한 복잡성을 감추기 위한 목적으로 사용되어야 합니다. 그러나 과도한 추상화는 코드를 이해하기 어렵게 만들고 유지보수를 어렵게 만듭니다.
  2. 복잡성 증가: 팩토리 메서드 패턴은 객체 생성을 처리하는 인터페이스를 정의하기 때문에, 팩토리 클래스와 그 클래스의 서브 클래스 사이의 상호작용이 복잡해질 수 있습니다.
  3. 새로운 타입 추가의 어려움: 팩토리 메소드 패턴을 사용하면 새로운 객체 타입을 추가하는 것이 상대적으로 어려워집니다. 새로운 타입을 추가하려면 팩토리 클래스와 그 클래스의 서브 클래스를 수정해야 합니다.
  4. 성능 저하: 팩토리 메소드 패턴은 객체 생성 시간이 길어지는 경우 성능에 영향을 미칠 수 있습니다. 이는 팩토리 클래스가 객체를 생성하는 과정에서 오버헤드가 발생하기 때문입니다.

1번 관점에서 이유없는 추상화가 발생한다면 이런경우에 해당한다. 추상화 된 인터페이스에 대해 아무 행동이 없다면 ? 굳이 추상화를 거칠 필요가 없다. 다시말해 그냥 일반적으로 구현하는게 보다 직관적 이라는 소리 이다.

 

2번 은 구현하면서 상당히 많이 느꼇다. 실제적으로 인터페이스 팩토리 클래스 사이 에 어떤 연관 을 규정하고 구현 해야할지 상당히 애를 먹었다.

 

3번 의 관점에서 본다면 위에 구현된 예제는 안티패턴이 되어버린다. 왜? OCP를 어기고 있다. 새로운 타입 이 추가되면 기존코드를 고쳐야한다. 예를들어 Switch 문에서 새로운 뉴욕스타일의 피자가 추가된다면 ? 기존 코드를 고쳐야 한다. 이는 OCP 를 어기고 있다. 

그럼에도 팩토리 메서드  패턴이 사용되는 이유로는 기존 클래스 들과 의 연관관계 가 상당히 적어진다.

즉 개별적으로 동작한다는 소리이다. 

새로운 피자가 추가된다고 해서 기존 클래스에 어떤 연관관계 가 있는지 생각해 본다면 보다 명확해지지 않을까 싶다.
예를 들어 기존 뉴욕 치즈 피자에 새로운 스트링이 추가된다면 ? 그 부분만 수정한다고 해서 LA 피자가 바뀌는가 ? 라고 생각해보면 쉽게 답할수 있다.

 

4번 예를 들어 db 의 커넥션 팩토리 메소드 가 있다고 생각해보자. 그 커녁센 이 무조건 있어야 실행되는 어플리케이션 이라면 ? 그 커넥션 을 위해 계속적인 리트라이가 일어나게 된다면 ? 성능의 저하가 발생될수도 있다. 


다시 처음에 설명했던 생성패턴 은 2가지 콘셉트에 대해 살펴보자.

1. 하나는 시스템 이 사용하는 구체적인 클래스에 대해 캡슐화하는 것이고,

: orderPizza 안에 creaetPizza를 숨겨 클라이언트는 피자의 생성 방식을 알지 못한다.

2. 다른 하나는 이런 구체적인 클래스의 인스턴스 생성 결합되는 방식을 숨기는 것이다.

: 각각의 하위 구현체 들은 피자의 생성된다.

 

이로 인해 얻는 장점은?

객체의 생성을 제어하고, 단순화한다.

단순 한 줄이면 우리는 원하는 객체를 얻을 수가 있으며 심지어 다양한 객체를 생성할 수도 있는 힘이 생긴다.

그렇게 생성된 객체에 대해 우리는 특정 행동을 명시하고 제어할 수 있는 제어권 이 생긴다. 

 

또한 생성패턴이 적용되어. 캡슐화되어 있기 때문에 단위 테스트 하는 데 있어 용이해진다.

보이는가? 정말 낮은 단계에서부터 상위까지 이렇게 정말 쉽게 모든 생성자에 대해 손쉽게 생성할 수 있다. 

팩토리 메서드 패턴의 장점이 여기서 드러난다.

이렇게 생성된 객체 들에 대해 추가적인 행동에 대해 검증하면 된다.

정말 손쉽고 작은 단위로 모든 객체에 대해 이렇게 테스트가 쉽다는 것은 정말 큰 장점이라고 생각한다.

 

패턴은 너무 어렵다 ....

 

 

 

 

출처

Encapsulation and Testing

Go DesignPattern Udemy

ChatGpt 예제

고 디자인 패턴

헤드퍼스트 디자인패턴

위키피디아

 

Why Factory method pattern call as a anti-pattern

+ Recent posts