[Design Pattern] 생성패턴 (Builder Pattern)
[빌더 패턴]
빌더 패턴이란 복합 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴이다.
생성패턴 의 근간이 되는 기본 구조에 대해서 다시 한번 상기하고 넘어가자.
생성패턴 은 구현하는 구현체가 있고 구현체 의 추상화 클래스가 존재한다.
빌더패턴 은 다음 과 같은 문제점을 해결하고자 나왔다.
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)
}