Builder 패턴
by EarlyHail
Builder
복잡한 객체를 생성하는 방법
과 표현하는 방법
을 정의하는 클래스를 별도로 분리하여 서로 다른 표현이라도 이를 생성할 수 있는 동일한 절차를 제공할 수 있도록 합니다.
-
Builder
Product 객체의 요소들을 생성하기 위한 추상 인터페이스를 제공
-
ConcreteBuilder
Builder에 정의된 인터페이스를 구현한다.
-
Director
Builder 인터페이스를 사용하여 Product를 생성, 구성하여 반환한다.
-
Product
Director가 Builder를 사용해서 생성한 결과물
1. 문제 상황
MazeGame의 createMaze
는 미로를 생성하는 클래스이다.
class MazeGame {
createMaze() {
const maze = new Maze();
const room1 = new Room(1);
const room2 = new Room(2);
const Door = new Door(r1, r2);
maze.addRoom(room1);
maze.addRoom(room2);
room1.setSide("all", new Wall());
room1.setSide("West", door);
room2.setSide("all", new Wall());
room2.setSide("East", door);
// 미로 생성 추가 작업...
return maze;
}
}
createMaze
는 미로를 생성하고, 1번 2번 방을 생성하고, 문을 생성하여 1번방과 2번방을 문으로 연결한다.
현재 createMaze는 미로를 어떻게 생성하는지
와 미로가 어떻게 구성되어있는지
에 대한 정보가 모두 들어있다.
미로를 어떻게 생성하는지
에 대한 정보를 빌더패턴을 이용해 분리해보자.
2. 빌더 패턴 적용
createMaze의 미로를 생성하는 로직을 분리하여, createMaze는 단지 미로의 요소를 표현하도록 수정해보자.
미리 Builder를 사용하는 코드를 보면 이해가 편할것 같다.
createMaze(builder: MazeBuilder) {
builder.buildMaze();
builder.buildRoom(1);
builder.buildRoom(2);
builder.buildDoor(1, 2);
return builder.getMaze();
}
위 코드는 Director의 코드이며, builder를 사용하여 미로가 어떻게 구성되어있는지만 표현한다.
-
Builder & ConcreteBuilder
// Builder interface MazeBuilder { buildMaze: () => void; buildRoom: (num: number) => void; buildDoor: (room1: number, room2: number) => void; getMaze: () => Maze; } // ConcreteBuilder class StandardMazeBuilder implements MazeBuilder { private maze: Maze; buildMaze() { this.maze = new Maze(); } buildRoom(num: number) { const room = new Room(num); this.maze.addRoom(room); room.setSide("all", new Wall()); } buildDoor(roomNum1: number, roomNum2: number) { const room1 = this.maze.getRoom(roomNum1); const room2 = this.maze.getRoom(roomNum2); const door = new Door(room1, room2); room1.setSide(getCommonWallDirection(room1, room2), door); room1.setSide(getCommonWallDirection(room1, room2), door); } getMaze() { return this.maze; } }
MazeBuilder
는 Builder Interface이며 미로를 생성하는 인터페이스를 정의한다.미로의 요소를 생성하는 로직은 MazeBuilder를 구현하는 ConcreteBuilder
StandardMazeBuilder
에 구현된다. -
Director
class MazeGame { createMaze(builder: MazeBuilder) { builder.buildMaze(); builder.buildRoom(1); builder.buildRoom(2); builder.buildDoor(1, 2); return builder.getMaze(); } createComplexMaze(builder: MazeBuilder) { builder.buildMaze(); builder.buildRoom(1); // ... builder.buildRoom(100); return builder.getMaze(); } }
MazeGame
는 Director이며 Builder를 이용하여 미로를 완성시킨다.Director는 미로가 어떻게 생성되는지, 방은 어떻게 초기화되고 벽은 어떻게 생성되는지, 문은 어떻게 생성되는지 등 미로가 어떻게 생성되어 있는지 전혀 알 방법이 없다.
단지 미로에 필요한 요소를 builder에게 요청하며 미로가 어떻게 구성되어있는지 표현할 뿐이다.
-
사용
const mazeGame = new MazeGame(); const standardMazeBuilder = new StandardMazeBuilder(); const standardMaze = mazeGame.createComplexMaze(standardMazeBuilder);
장점
-
Product에 대한 내부 표현을 다양하게 변화시킬 수 있다.
Product를 조립할 때에는 Builder에 정의된 추상 인터페이스만을 통해 동작하기 때문에 새로운 재료를 쓰는 미로가 추가되거나, 기존 재료의 사용법이 변경되었을 때 Builder만 수정하면 된다.
-
생성과 표현에 필요한 코드를 분리한다.
Builder에 특정 Product의 부품을 생성하는 생성하는 방법이 구현되어 있기 때문에 재사용하여 다양한 Product를 찍어낼 수 있다.
-
객체를 생성하는 절차를 세밀하게 나눌 수 있다.
Builder패턴은 Director의 통제 아래 하나씩 내부의 구성요소를 만들어나간다.
Product를 되돌려 받을 때 까지 Product를 조립할 수 있고 Director를 통해 세밀하게 나눠진 구성 과정을 확인할 수도 있다.
또다른 Builder 패턴
위의 Builder패턴을 보면 뭔가 어색하다고 느낄 수도 있다. 특히 Java에서 빌더패턴을 사용했다면 더 어색할것 같다.
Builder패턴은 객체 생성을 깔끔하고 유연하게
하기 위해서도 사용된다.
class UserBuilder {
public birthday: Date;
public name: string;
setBirthday(birthday: Date) {
this.birthday = birthday;
}
setName(name: string) {
this.name = name;
}
build() {
return new User(this);
}
}
class User {
private name: string;
private birthday: Date;
constructor(userBuilder: UserBuilder) {
this.name = userBuilder.name;
this.birthday = userBuilder.birthday;
}
getUser() {
return this.name + this.birthday;
}
}
const user = new UserBuilder().setBirthday(new Date()).setName("hojin").build();
console.log(user.getUser());
장점
-
객체 생성과 표현이 분리된다.
객체를 생성하는 setter들이 있는 UserBuilder와 객체가 표현되어져있고 사용되는 User로 나뉘어진다.
-
property 수정에 유연하다.
새로운 property가 추가되면 기존 로직은 그대로 두고, 새로운 setter를 정의하여 사용하면 되기 때문에 수정에 유연하다.