소프트웨어 공학

[Design Go] 원형 패턴

들어가기에 앞서...

예제는 전과 마찬가지로 지난 글의 예제를 이용했다.

원형 패턴

원형 패턴은 미리 만들어진 객체(prototype)의 복사를 통해 새로운 객체를 생성하는 패턴이다. prototype 객체는 보통 팩토리 객체(factory) 내부의 인스턴스로 존재하며, factory 객체는 요청한 prototype 객체의 복사본을 반환한다. prototype 객체를 복사할 때에는 깊은 복사를 이용해야 한다.

장점

  • 런타임 단계에서 새로운 객체의 생성자를 만들 수 있다. 이는 컴파일 단계에서 생성할 객체의 클래스를 정해야 하는 다른 생성 패턴에 비해 유연성이 보장된다.
  • 클래스의 개수가 줄어든다. 다른 생성 패턴은 인터페이스를 만족하는 다양한 클래스들을 생성해야 하지만 원형 패턴은 하나의 클래스에 내부의 prototype 객체만 다르게 하면 된다.
  • 복사 자체를 응용할 수 있다. 원형 패턴을 위해 구현한 복사 메소드는 다른 곳에 쓰일 때에도 도움이 될 수 있다.

단점

  • 복사 메소드의 구현이 어렵다. 원형 패턴은 객체를 복사하여 새로운 객체를 만드는 패턴이다. 즉, 원형 패턴의 핵심은 복사 메소드이다. 이 때, 복사된 값들은 복사된 이후에 각자의 특성에 맞춰 변해야 하는데, 이 때 다른 객체에는 영향이 없어야 한다. 즉, 깊은 복사를 이용해야 한다. 이것이 무슨 말인지 모르겠다면 해당 글을 참조하자.
  • 만약 내부 값을 설정해야 할 경우 해당 작업을 담당할 메소드가 따로 필요하다. Copy()에 매개변수로 집어넣는 방법도 있기는 하지만 일관성이 떨어지게 된다.

구현

원형 패턴은 보통 다음과 같이 추상 팩토리 패턴과 섞어서 사용된다.

package main

type MazeFactory interface {
	MakeMaze() Maze
	MakeWall() Wall
	MakeRoom(idx int) Room
	MakeDoor(r1, r2 Room) Door
}
package main

type prototypeMazeFactory struct {
	m Maze
	w Wall
	r Room
	d Door
}

func NewPrototypeMazeFactory(m Maze, w Wall, r Room, d Door) MazeFactory {
	f := new(prototypeMazeFactory)
	f.m = m
	f.w = w
	f.r = r
	f.d = d
	return f
}

func (f *prototypeMazeFactory) MakeMaze() Maze {
	return f.m.Copy()
}

func (f *prototypeMazeFactory) MakeWall() Wall {
	return f.w.Copy()
}

func (f *prototypeMazeFactory) MakeRoom(idx int) Room {
	r := f.r.Copy()
	r.Set(idx) // 객체의 값을 설정하는 함수이다.
	return r
}

func (f *prototypeMazeFactory) MakeDoor(r1, r2 Room) Door {
	d := f.d.Copy()
	d.Set(r1, r2)
	return d
}

func NewSimplePrototype() MazeFactory {
	return NewPrototypeMazeFactory(NewMaze(), NewWall(), NewRoom(0), NewDoor(nil, nil))
}

func NewBombPrototype() MazeFactory {
	return NewPrototypeMazeFactory(NewMaze(), NewWall(), NewRoomWithBomb(0), NewBombedDoor(nil, nil))
}

일반적인 추상 팩토리 패턴은 종류에 따라 다른 클래스로 서브클래싱해야 한다. 하지만 프로토타입 패턴을 이용하면 다음과 같이 하나의 클래스만으로 다양한 표현을 구사할 수 있다. 만약 자주 사용하는 프리셋이 있다면 NewSimplePrototype()과 같이 팩토리 메소드 패턴을 이용해 별도의 생성 함수를 만들면 된다.

실제 적용은 추상 팩토리 패턴의 그것과 비슷하다.

//...
func (r *room) Set(idx int) {
	r.idx = idx
}
//...
func (d *door) Set(r1, r2 Room) {
	d.r1 = r1
	d.r2 = r2
}
//...
func CreateMazeByFactory(factory MazeFactory) Maze {
	aMaze := factory.MakeMaze()
	r1 := factory.MakeRoom(1)
	r2 := factory.MakeRoom(2)
	door := factory.MakeDoor(r1, r2)

	aMaze.AddRoom(r1)
	aMaze.AddRoom(r2)

	r1.SetSide(East, door)
	r2.SetSide(West, door)

	return aMaze
}

func main() {
	simpleM := CreateMaze()
	play(simpleM)

	m := CreateMazeByFactory(NewSimplePrototype())
	play(m)
	m = CreateMazeByFactory(NewBombPrototype())
	play(m)
    
    // 새로 정의된 미로
	m = CreateMazeByFactory(NewPrototypeMazeFactory(NewMaze(), NewWall(), NewRoomWithBomb(0), NewDoor(nil, nil)))
	play(m)
}

여기서 main부분의 아래 두 줄은 이전에 정의되지 않은 새로운 미로를 정의하여 사용했다. 이렇듯 팩토리를 생성하는 메소드를 이용하면 런타임에 새로운 객체를 생성할 수 있을 것이다.

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
Bomb!
door is Open. Successfully entered.

마치며...

원형 패턴은 복잡한 클래스 구조를 단순화시킨다. 원형 패턴은 생성 패턴 중에서 적용하기 제일 까다롭지만 그만한 리턴을 보장한다. 만약 인터페이스(특히 팩토리 객체)의 서브클래스가 컴파일 타임에 결정되지 않아야 하거나 그 수가 지나치게 많을 경우 원형 패턴은 좋은 해결책이 될 것이다.

참고 자료