들어가기에 앞서...
이번 패턴의 예제는 지난 글을 참조하자.
추상 팩토리 패턴
추상 팩토리 패턴은 한 객체(factory)에게 서로 연관되어 있는 각 객체(product)들의 생성을 맡기는 패턴이다. 우선 factory 객체가 상속할 인터페이스(Factory)에는 하나 이상의 product 객체들을 생성하는 메소드가 정의되어 있어야 한다. factory 객체는 Factory 인터페이스를 상속하여 해당 인터페이스의 메소드를 구현한다. 사용자는 factory 객체를 생성하고 해당 객체의 메소드를 통해 product 객체들을 생성한다.
장점
- 연관된 객체들(객체군)끼리 생성할 때 문제의 여지가 없어진다. 추상 팩토리 패턴을 사용하는 가장 큰 이유이다. 예를 들어 여러 사이즈의 나사와 볼트가 있을 때 추상 팩토리 패턴을 이용하지 않는다면 다른 사이즈의 나사와 볼트를 생성해 끼우는 문제가 발생할 수 있다. 그러나 추상 팩토리 패턴을 이용하면 같은 사이즈의 나사와 볼트를 생성하는 팩토리 객체를 만들고. 해당 팩토리에서 나사를 생성하는 메소드와 볼트를 생성하는 메소드를 각각 호출하여 끼우기만 하면 된다.
- 객체의 생성과 구현을 분리한다. 이전의 빌더 패턴과 같다.
- 객체군을 다른 객체군으로 쉽게 변경할 수 있다. 위의 비유를 계속하자면 나사와 볼트의 사이즈를 변경하고 싶을 때 팩토리 객체의 생성만 바꾸면 된다는 뜻이다. 나머지 코드는 건들 필요가 없다.
단점
- 객체군에 새로운 객체의 추가가 힘들다. 새로운 객체를 추가하려면 Factory 인터페이스에 해당 객체를 생성하는 메소드를 정의해야 하고 이에 맞춰 다른 모든 factory 객체에도 해당 메소드를 구현해야 한다.
구현
Maze, Wall, Room, Door는 하나의 객체군으로, 서로 긴밀히 연관되어 있다. 이 경우 추상 팩토리 패턴을 쓰기가 좋다. 추상 팩토리 패턴을 사용하기 위해 팩토리 인터페이스를 정의하였다.
package main
type MazeFactory interface {
MakeMaze() Maze
MakeWall() Wall
MakeRoom(idx int) Room
MakeDoor(r1, r2 Room) Door
}
해당 인터페이스에는 팩토리 객체가 구현할 메소드들이 정의되어 있다. 빌더 패턴과는 다르게 객체 자체는 밖에서 다뤄지므로 반환값을 가지고 있다.
simpleMazeFactory는 기본적인 미로의 객체군을 생성한다.
package main
type simpleMazeFactory struct {
}
func NewSimpleMazeFactory() *simpleMazeFactory {
return new(simpleMazeFactory)
}
func (f *simpleMazeFactory) MakeMaze() Maze {
return NewMaze()
}
func (f *simpleMazeFactory) MakeWall() Wall {
return NewWall()
}
func (f *simpleMazeFactory) MakeRoom(idx int) Room {
return NewRoom(idx)
}
func (f *simpleMazeFactory) MakeDoor(r1, r2 Room) Door {
return NewDoor(r1, r2)
}
factory 객체의 메소드들은 객체군의 각 객체를 생성하는 역할을 맡는다. 해당 팩토리는 일반적인 미로의 객체군을 반환한다.
boomMazeFactory는 방마다 폭탄이 있는 미로의 객체군을 생성한다.
package main
type bombMazeFactory struct {
simpleMazeFactory
}
func NewBombMazeFactory() *bombMazeFactory {
return new(bombMazeFactory)
}
func (f *bombMazeFactory) MakeRoom(idx int) Room {
return NewRoomWithBomb(idx)
}
func (f *bombMazeFactory) MakeDoor(r1, r2 Room) Door {
return NewBombedDoor(r1, r2)
}
해당 팩토리는 기본적으로 simpleMazeFactory를 상속받지만 Room 객체와 Door 객체를 생성할 때 다른 객체를 반환한다.
마지막은 functionalMazeFactory이다.
type functionalMazeFactory struct {
MakeMaze func() Maze
MakeWall func() Wall
MakeRoom func(idx int) Room
MakeDoor func(r1, r2 Room) Door
}
func NewFunctionalMazeFactory() *functionalMazeFactory {
f := new(functionalMazeFactory)
f.MakeMaze = func()Maze{return NewMaze()}
f.MakeWall = func()Wall{return NewWall()}
f.MakeRoom = func(idx int)Room{return NewRoom(idx)}
f.MakeDoor = func(r1, r2 Room)Door{return NewDoor(r1, r2)}
return f
}
func NewBombFunctionalMazeFactory() *functionalMazeFactory {
f := NewFunctionalMazeFactory()
f.MakeRoom = func(idx int)Room{return NewRoomWithBomb(idx)}
f.MakeDoor = func(r1, r2 Room)Door{return NewBombedDoor(r1, r2)}
return f
}
해당 팩토리 객체는 함수를 메소드 대신 함수 객체로 만들었다. 함수는 생성 단계에서 새롭게 구현된다. 해당 기술을 이용하면 팩토리 인터페이스 없이 팩토리 클래스 하나로 추상 팩토리의 역할을 할 수 있다. 게다가 클래스 밖에서도 객체의 생성을 변경할 수 있다. 팩토리 객체의 함수 객체만 변경하면 된다. 하지만 필자는 해당 방법을 추천하지 않는다. 해당 방법은 생성 단계에서 함수 객체를 구현한다고 가정한다. 만약 함수 객체를 하나라도 깜빡하고 구현하지 않으면 문제를 일으킬 가능성이 높다. 무엇보다도, 클래스 밖에서 객체의 생성을 변경하는 것은 해당 객체가 객체군과 연관성이 적다는 의미이고, 추상 팩토리 패턴을 쓰는 가장 큰 이유에 위배된다는 의미이다.
위의 세 팩토리 객체를 이용한 예제는 다음과 같다.
//...
func CreateMazeByFactory(factory MazeFactory) Maze {
aMaze := factory.MakeMaze()
r1 := factory.MakeRoom(1)
r2 := factory.MakeRoom(2)
d := factory.MakeDoor(r1, r2)
aMaze.AddRoom(r1)
aMaze.AddRoom(r2)
r1.SetSide(East, d)
r2.SetSide(West, d)
return aMaze
}
func CreateMazeByFunctionalFactory(f *functionalMazeFactory) Maze {
m := f.MakeMaze()
r1 := f.MakeRoom(1)
r2 := f.MakeRoom(2)
d := f.MakeDoor(r1, r2)
m.AddRoom(r1)
m.AddRoom(r2)
r1.SetSide(East, d)
r2.SetSide(West, d)
return m
}
func main() {
simpleM := CreateMaze()
play(simpleM)
simpleMazeFactory := CreateMazeByFactory(NewSimpleMazeFactory())
play(simpleMazeFactory)
bombMazeFactory := CreateMazeByFactory(NewBombMazeFactory())
play(bombMazeFactory)
ff1, ff2 := NewFunctionalMazeFactory(), NewBombFunctionalMazeFactory()
m1, m2 := CreateMazeByFunctionalFactory(ff1), CreateMazeByFunctionalFactory(ff2)
play(m1)
play(m2)
ff3 := ff2
ff3.MakeDoor = func(r1, r2 Room)Door{return NewDoor(r1, r2)}
m3 := CreateMazeByFunctionalFactory(ff3)
play(m3)
}
Entered to room 1
door is Open. Successfully entered.
Entered to room 1
door is Open. Successfully entered.
Entered to room 1
Bomb!
Door was bombed. Successfully entered.
Entered to room 1
door is Open. Successfully entered.
Entered to room 1
Bomb!
Door was bombed. Successfully entered.
Entered to room 1
Bomb!
door is Open. Successfully entered.
마치며...
추상 팩토리 패턴은 코드의 양은 조금 커지지만 버그의 가능성을 현저히 낮춰준다. 특히 관련된 객체들을 생성해야 할 경우 장점이 극대화된다. 만약 긴밀히 연관된 객체들이 있다면 추상 팩토리 패턴을 먼저 고려해보자.
참고 자료
- GoF(에릭 감마, 리처드 헬름, 랄프 존슨, 존 블리시디스), GoF의 디자인 패턴, 프로텍 미디어, 2015
- Dmitri Nesteruk, Design Patterns in Go, Udemy, 2020.8