
빌더 패턴은 복잡한 객체의 생성 과정과 표현 방법을 분리하여 다양한 구성의 인스턴스를 만드는 생성 패턴이다. 생성자에 들어갈 매개 변수를 메서드로 하나하나 받아들이고 마지막에 통합 빌드해서 객체를 생성하는 방식이다.
이해하기 쉬운 사례로 수제 햄버거를 들 수 있다. 수제 햄버거를 주문할 때 빵이나 패티 등 속재료들은 주문자에 따라 결정된다. 어떤 사람은 치즈를 빼달라고 할 수도 있고, 어느 사람은 토마토를 넣지 말아달라고 할 수 있다. 이처럼 선택적으로 속재료들을 보다 유연하게 받아 다양한 타입의 인스턴스를 생성할 수 있어, 클래스의 선택적 매개변수가 많은 상황에서 유용하게 사용된다.
점층적 생성자 패턴은 필수 매개변수와 함께 선택 매개변수를 받는 형태로, 우리가 다양한 매개변수를 입력받아 인스턴스를 생성하고 싶을때 사용하던 생성자를 오버로딩하는 방식이다.
class Hamburger {
// 필수 매개변수
private int bun;
private int patty;
// 선택 매개변수
private int cheese;
private int lettuce;
private int tomato;
private int bacon;
public Hamburger(int bun, int patty, int cheese, int lettuce, int tomato, int bacon) {
this.bun = bun;
this.patty = patty;
this.cheese = cheese;
this.lettuce = lettuce;
this.tomato = tomato;
this.bacon = bacon;
}
public Hamburger(int bun, int patty, int cheese, int lettuce, int tomato) {
this.bun = bun;
this.patty = patty;
this.cheese = cheese;
this.lettuce = lettuce;
this.tomato = tomato;
}
public Hamburger(int bun, int patty, int cheese, int lettuce) {
this.bun = bun;
this.patty = patty;
this.cheese = cheese;
this.lettuce = lettuce;
}
public Hamburger(int bun, int patty, int cheese) {
this.bun = bun;
this.patty = patty;
this.cheese = cheese;
}
...
}
void Start()
{
// 모든 재료가 있는 햄버거
Hamburger hamburger1 = new Hamburger(2, 1, 2, 4, 6, 8);
// 빵과 패티 치즈만 있는 햄버거
Hamburger hamburger2 = new Hamburger(2, 1, 1);
// 빵과 패티 베이컨만 있는 햄버거
Hamburger hamburger3 = new Hamburger(2, 0, 0, 0, 0, 6);
}
이러한 방식은 클래스 인스턴스 필드들이 많을수록 생성자에 들어갈 인자의 수가 늘어나 몇번째 인자가 어떤 필드였는지 헷갈리는 경우가 생기게 된다. 햄버거를 생성하기 위해서는 Hamburger 생성자의 몇 번째 인수가 양상추의 개수인지 토마토 개수인지 파악할 필요가 있다.
또한 매개변수 특성상 순서를 따라야 하기 때문에 위와 같이 "빵과 베이컨만 있는 햄버거"를 원할 경우 억지로 파라미터에 0을 전달해야 한다. 생성자 만으로는 필드를 선택적으로 생략할 수 있는 방법이 없기 때문이다.
무엇보다 타입이 다양할 수록 생성자 메서드 수가 기하 급수로적으로 늘어나 가독성이나 유지보수 측면에서 좋지 않다.
이러한 단점을 위해 Setter 메소드를 사용한 패턴이 고안 되었다. 매개변수가 없는 생성자로 객체 생성 후 Setter 메소드를 이용해 클래스 필드의 초깃값을 설정하는 방식이다.
class Hamburger {
// 필수 매개변수
private int bun;
private int patty;
// 선택 매개변수
private int cheese;
private int lettuce;
private int tomato;
private int bacon;
public Hamburger() {}
public void setBun(int bun) {
this.bun = bun;
}
public void setPatty(int patty) {
this.patty = patty;
}
public void setCheese(int cheese) {
this.cheese = cheese;
}
public void setLettuce(int lettuce) {
this.lettuce = lettuce;
}
public void setTomato(int tomato) {
this.tomato = tomato;
}
public void setBacon(int bacon) {
this.bacon = bacon;
}
}
void Start()
{
// 모든 재료가 있는 햄버거
Hamburger hamburger1 = new Hamburger();
hamburger1.setBun(2);
hamburger1.setPatty(1);
hamburger1.setCheese(2);
hamburger1.setLettuce(4);
hamburger1.setTomato(6);
hamburger1.setBacon(8);
// 빵과 패티 치즈만 있는 햄버거
Hamburger hamburger2 = new Hamburger();
hamburger2.setBun(2);
hamburger2.setPatty(1);
hamburger2.setCheese(2);
// 빵과 패티 베이컨만 있는 햄버거
Hamburger hamburger3 = new Hamburger();
hamburger3.setBun(2);
hamburger2.setPatty(1);
hamburger3.setBacon(8);
}
기존 생성자 오버로딩에서 나타났던 가독성 문제는 사라지고 Setter 메서드를 별도로 호출함으로써 유연하게 객체 생성이 가능해졌다. 하지만 이런 방식은 객체 생성 시점에 모든 값들을 주입하지 않아 일관성 문제와 불변성 문제가 나타나게 된다.
일관성 문제? : 필수 매개변수란 객체가 초기화 될 때 반드시 설정되어야 하는 값이다. 개발자가 깜빡하고
setBun()메서드를 호출하지 않앗다면 이 객체의 일관성이 무너진 상태가 된다. 즉, 객체가 유효하지 않는 것이며 런타임 예외가 발생할 수도 있다.
이는 객체를 생성하는 부분과 설정하는 부분이 물리적으로 떨어져 있어 발생하는 문제이다. 물론 어느 정도 생성자와 결합하여 극복 할 수 있지만 여전히 불변성의 문제가 존재하기 때문에 자바 빈즈 패턴은 지양해야 한다.
불변성 문제? : 자바 빈즈 패턴의 Setter 메서드는 객체를 처음 생성할때 필드값을 설정하기 위해 존재하는 메서드이다. 하지만 객체를 생성했음에도 여전히 외부적으로 Setter 메소드를 노출하고 있으므로, 협업 과정에서 언제 어디서 누군가 Setter 메서드를 호출해 함부로 객체를 조작할수 있게 된다. 이것을 불변함을 보장할 수 없다고 얘기한다.
마치 이미 완성된 햄버거의 속재료를 다른 사람들이 열고 바꿀 수 있는 상황이라고 생각하면 된다.
이러한 문제를 해결하기 위해 별도의 Builder 클래스를 만들어 메소드를 통해 step-by-step으로 값을 입력 받은 후에 최종적으로 build() 메소드로 하나의 인스턴스를 생성하여 리턴하는 패턴이다.
빌더 패턴 사용법을 살펴보면, 빌더 클래스의 메서드를 체이닝 형태로 호출함으로써 자연스럽게 인스턴스를 구성하고 마지막에 build() 메서드를 통해 최종적으로 객체를 생성하도록 되어있다.
void Start()
{
// 생성자 방식
Hamburger hamburger = new Hamburger(2, 3, 0, 3, 0, 0);
// 빌더 방식
Hamburger hamburger = new Hamburger.Builder(10)
.bun(2)
.patty(3)
.lettuce(3)
.build();
}
빌더 패턴을 사용하면 더 이상 생성자 오버로딩을 열거하지 않아도 되며, 데이터 순서에 상관없이 객체를 만들어내 생성자 인자 순서를 파악할 필요도 없고 잘못된 값을 넣는 실수도 하지 않게 된다. 점층적 생성자 패턴과 자바 빈 패턴 두 가지의 장점만을 취했다고 볼 수 있다.
빌더 패턴 구조 자체는 난이도가 쉬워서 빠르게 구성이 가능하다.
예를 들어 다음과 같은 Student 클래스에 대한 객체 생성만을 담당하는 별도의 빌더 클래스를 만들려고 한다.
class Student {
private int id;
private String name = "아무개";
private String grade = "freshman";
private String phoneNumber = "010-0000-0000";
public Student(int id, String name, String grade, String phoneNumber) {
this.id = id;
this.name = name;
this.grade = grade;
this.phoneNumber = phoneNumber;
}
@Override
public String toString() {
return "Student { " +
"id='" + id + '\'' +
", name=" + name +
", grade=" + grade +
", phoneNumber=" + phoneNumber +
" }";
}
}
먼저 Builder 클래스를 만들고 필드 멤버 구성을 만들고자 하는 Student 클래스 멤버 구성과 똑같이 구성한다.
class StudentBuilder {
private int id;
private String name;
private String grade;
private String phoneNumber;
}
그리고 각 멤버에 대한 Setter 메서드를 구현해준다. 이 때 가독성을 좋게 하면서 기존 Setter와 다른 특성을 가지고 있다는 점을 알리기 위해, set 단어는 빼주고 심플하게 멤버 이름으로만 메서드명을 지어준다.
class StudentBuilder {
private int id;
private String name;
private String grade;
private String phoneNumber;
public StudentBuilder id(int id) {
this.id = id;
return this;
}
public StudentBuilder name(String name) {
this.name = name;
return this;
}
public StudentBuilder grade(String grade) {
this.grade = grade;
return this;
}
public StudentBuilder phoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
}
여기서 주목할 부분은 각 Setter 함수의 마지막 반환 구문인 return this 부분이다. 여기서 this란 StudentBuilder 객체를 말한다. 즉, 빌더 객체 자신을 리턴함으로써 메서드 호출 후 연속적으로 빌더 메서드들을 체이닝 하여 호출할 수 있게 된다. new StudentBuilder().id(값).name(값)
마지막으로 빌더의 목표였던 최종 Student 객체를 만들어주는 build 메서드를 구성해준다. 빌더 클래스의 필드들을 Student 생성자의 인자에 넣어줌으로써 멤버 구성이 완료된 Student 인스턴스를 얻게 되는 것이다.
class StudentBuilder {
private int id;
private String name;
private String grade;
private String phoneNumber;
public StudentBuilder id(int id) { ... }
public StudentBuilder name(String name) { ... }
public StudentBuilder grade(String grade) { ... }
public StudentBuilder phoneNumber(String phoneNumber) { ... }
public Student build() {
return new Student(id, name, grade, phoneNumber); // Student 생성자 호출
}
}
이렇게 구성한 빌더 객체를 실행하면 아래와 같은 성질의 코드가 구현되게 된다.
void Start()
{
Student student = new StudentBuilder()
.id(2016120091)
.name("임꺽정")
.grade("Senior")
.phoneNumber("010-5555-5555")
.build();
}
빌더 패턴의 네이밍 방식은 3가지 정도 존재한다.
Student student = new StudentBuilder(2016120091)
.name("홍길동")
.grade("freshman")
.phoneNumber("010-5555-5555")
.build();
Student student = new StudentBuilder(2016120091)
.setName("홍길동")
.setGrade("freshman")
.setPhoneNumber("010-5555-5555")
.build();
Student student = new StudentBuilder(2016120091)
.withName("홍길동")
.withGrade("freshman")
.withPhoneNumber("010-5555-5555")
.build();
이 중 그냥 멤버 이름으로만 메서드 명을 짓는 첫번째 네이밍 방식이 추천되어 진다.
두 번째 네이밍 방식은 일반 Setter 메서드와 헷갈릴 소지가 있다.
세 번째는 Setter와 구분키 위해 with 키워드를 사용한 것인데 빌더 지연 생성 방식에서 미리 빌더를 설정할 때 쓰이기도 한다.
빌더 패턴을 사용하면 아래와 같은 장점이 있다.
생성자 방식으로 객체를 생성하는 경우는 매개변수가 많아질수록 가독성이 급격하게 떨어진다. 클래스 변수가 4개 이상만 되어도 각 인자 순서마다 이 값이 어떤 멤버에 해당되는지 바로 파악이 힘들다.
반면 다음과 같이 빌더 패턴을 적용하면 직관적으로 어떤 데이터에 어떤 값이 설정되는지 한눈에 파악할 수 있게 된다. 특히 연속된 동일 타입의 매개 변수를 많이 설정할 경우에 발생할 수 있는 설정 오류와 같은 실수를 방지할 수 있다.
// 생성자 방식
Student student1 = new Student(2016120091, "홍길동", "freshman", "010-5555-5555");
// 빌더 방식
Student student2 = new StudentBuilder()
.id(2016120091)
.name("임꺽정")
.grade("Senior")
.phoneNumber("010-5555-5555")
.build();
굳이 생성자에 디폴트 매개변수를 설정하지 않아도 Builder에서 디폴트 매개변수가 설정된 필드를 설정하는 메서드를 호출하지 않는 방식으로 마치 디폴트 매개변수를 생략하고 호출하는 효과를 구현할 수 있다. 이를 통해 생성자의 인자에 직접적으로 디폴트 매개변수 값을 주는 것이 아니라 Builder 패턴에서 원하는 디폴트 매개변수를 넣어줄 수 있다.
class StudentBuilder {
private int id;
private String name;
private String grade = "freshman"; // 디폴트 매개변수 역할
private String phoneNumber;
...
}
// 디폴트 필드인 grade를 제외하고 빌더 구성 및 인스턴스화
Student student1 = new StudentBuilder(2016120091)
.name("홍길동")
.phoneNumber("010-5555-5555")
.build();
객체 인스턴스는 목적에 따라 초기화가 필수인 멤버 변수가 있고 선택적인 멤버 변수가 있을 수 있다.
만일 Student 클래스의 id 필드가 인스턴스화 할때 반드시 필수적으로 값을 지정해 주어야 하는 필수 멤버 변수라고 가정해보자. 이를 기존 생성자 방식으로 구현하려면 초기화가 필수인 멤버 변수만을 위한 생성자를 정의하고 선택적인 멤버 변수에 대응하는 생성자를 오버로딩을 통해 열거하거나, 혹은 전체 멤버를 인자로 받는 생성자만을 선언하고 매개변수에 null을 받는식으로 구성하여야 한다.
class Student {
// 초기화 필수 멤버
private int id;
// 초기화 선택적 멤버
private String name;
private String grade;
private String phoneNumber;
public Student(int id, String name, String grade, String phoneNumber) {
this.id = id;
this.name = name;
this.grade = grade;
this.phoneNumber = phoneNumber;
}
}
Student student = new Student(2010234455, null, null, null);
그냥 봐도 좋지 않은 방법임을 알 수 있다. 따라서 빌더 클래스를 통해 초기화가 필수인 멤버는 빌더의 생성자로 받게 하여 필수 멤버를 설정해주어야 빌더 객체가 생성되도록 유도하고, 선택적인 멤버는 빌더의 메서드로 받게 하면 사용자로 하여금 필수 멤버와 선택 멤버를 구분하여 객체 생성을 유도할 수 있다.
class StudentBuilder {
// 초기화 필수 멤버
private int id;
// 초기화 선택적 멤버
private String name;
private String grade;
private String phoneNumber;
// 필수 멤버는 빌더의 생성자를 통해 설정
public StudentBuilder(int id) {
this.id = id;
}
// 나머지 선택 멤버는 메서드로 설정
public StudentBuilder name(String name) {
this.name = name;
return this;
}
public StudentBuilder grade(String grade) {
this.grade = grade;
return this;
}
public StudentBuilder phoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
public Student build() {
return new Student(id, name, grade, phoneNumber);
}
}
Student student1 =
new StudentBuilder(2016120091) // 필수 멤버
.name("홍길동") // 선택 멤버
.build();
Student student2 =
new StudentBuilder(2016120091) // 필수 멤버
.name("임꺽정") // 선택 멤버
.grade("freshman") // 선택 멤버
.build();
Student student3 =
new StudentBuilder(2016120091) // 필수 멤버
.name("주몽") // 선택 멤버
.grade("Senior") // 선택 멤버
.phoneNumber("010-5555-5555") // 선택 멤버
.build();
객체 생성을 단계별로 구성하거나 구성 단계를 지연하거나 재귀적으로 생성을 처리 할수 있다. 즉, 빌더를 재사용 함으로써 객체 생성을 주도적으로 지연할 수 있는 것이다.
// 1. 빌더 클래스 전용 리스트 생성
List<StudentBuilder> builders = new List<StudentBuilder>();
// 2. 객체를 최종 생성 하지말고 초깃값만 세팅한 빌더만 생성
builders.Add(
new StudentBuilder(2016120091)
.Name("홍길동")
);
builders.Add(
new StudentBuilder(2016120092)
.Name("임꺽정")
.Grade("senior")
);
builders.Add(
new StudentBuilder(2016120093)
.Name("박혁거세")
.Grade("sophomore")
.PhoneNumber("010-5555-5555")
);
// 3. 나중에 빌더 리스트를 순회하여 최종 객체 생성을 주도
foreach (var builder in builders)
{
Student student = builder.Build();
Console.WriteLine(student);
}
만일 생서자로부터 멤버값을 받는 형태면 각 생성자 매개변수에 대한 검증 로직을 생성자 메소드마다 복잡하게 구현해야하고 이는 생성자의 크기가 비대해지게 되는 결과를 가지고 온다.
class Student {
...
// 각 매개변수에 대한 검증을 하나의 생성자 모두 처리하고 있다
public Student(int id, String name, String grade, String phoneNumber) {
if (!grade.equals("freshman") && !grade.equals("sophomore") && !grade.equals("junior") && !grade.equals("senior")) {
throw new IllegalArgumentException(grade);
}
if (!phoneNumber.startsWith("010")) {
throw new IllegalArgumentException(phoneNumber);
}
this.id = id;
this.name = name;
this.grade = grade;
this.phoneNumber = phoneNumber;
}
}
빌더를 이용하면 생성될 객체의 멤버 변수의 초기화와 검증을 각각의 멤버별로 분리해서 작성할 수 있다. 빌더의 각각의 멤버 설정 메서드에서 검증 과정을 분담해서 유지 보수를 용이하게 하는 것이다.
class StudentBuilder {
...
public StudentBuilder(int id) {
this.id = id;
}
public StudentBuilder name(String name) {
this.name = name;
return this;
}
public StudentBuilder grade(String grade) {
if (!grade.equals("freshman") && !grade.equals("sophomore") && !grade.equals("junior") && !grade.equals("senior")) {
throw new IllegalArgumentException(grade);
}
this.grade = grade;
return this;
}
public StudentBuilder phoneNumber(String phoneNumber) {
if (!phoneNumber.startsWith("010")) {
throw new IllegalArgumentException(phoneNumber);
}
this.phoneNumber = phoneNumber;
return this;
}
public Student build() {
return new Student(id, name, grade, phoneNumber);
}
}
물론 이러한 형식은 흔히 Getter & Setter 형식에서도 많이 이용되는 패턴이기도 하다. 하지만 어느 클래스에 Setter 메서드를 구현한다는 말은 객체 멤버의 변경 가능성을 열어둔것과 같아 불변성 문제가 터지게 된다. (바로 다음에 설명)
많은 개발자들이 멤버에 값을 할당할 때 Setter 메서드를 사용하는데, 그 중 클래스 멤버 초기화를 Setter를 통해 구성하는 것은 좋지 않은 방법이다. 위에서 다뤘던 Setter 메서드를 통해 멤버 초기화를 하지 말아야하는 이유에 대한 좀 더 고수준적인 내용이다.
일반적으로 프로그램을 개발하는데 있어 다른 사람과 협업할 때 가장 중요시되는 점 중 하나가 불변 객체이다. 불변 객체란 객체 생성 이후 내부의 상태가 변하지 않는 객체이다. 불변 객체는 오로지 읽기(get) 메소드만을 제공하며 쓰기(set)는 제공하지 않는다. 대표적으로 C#에서 readonly, 자바에서 final 키워드를 붙인 변수가 바로 불변이다.
현업에서 불변 객체를 이용해 개발해야 하는 이유로는 다음과 같다.
따라서 클래스들은 가변적 이여야 하는 매우 타당한 이유가 있지 않는 한 반드시 불변으로 만들어야 한다. 만약 클래스를 불변으로 만드는 것이 불가능하다면 가능한 변경 가능성을 최소화해야 한다.
예를들어 경우에 따라 변수에 readonly 키워드를 붙일수 없는 상황이 생길 수도 있다. 이때는 Setter 메서드 자체를 구현하지 않음으로서 불변 객체를 간접적으로 구성이 가능하다.그러면 결국은 돌도 돌아 생성자를 이용하라는 것인데 역시나 지나친 생성자 오버로딩 문제가 발생하게 된다. 그래서 연구된 것이 빌더 클래스이다.
즉, 최종 정리하자면 빌더 패턴은 생성자 없이 어느 객체에 대해 '변경 가능성을 최소화' 를 추구하여 불변성을 갖게 해주게 되는 것이다.
우선 빌더 패턴을 적용하려면 N개의 클래스에 대해 N개의 새로운 빌더 클래스를 만들어야 해서, 클래수 수가 기하급수적으로 늘어나 관리해야 할 클래스가 많아지고 구조가 복잡해질 수 있다. 또한 선택적 매개변수를 많이 받는 객체를 생성하기 위해서는 먼저 빌더 클래스부터 정의해야한다. 다만 이부분은 여느 디자인 패턴이 가지는 단점이기도 하다.
매번 메서드를 호출하여 빌더를 거쳐 인스턴스화 하기 때문에 어쩌면 당연한 말일지도 모른다. 비록 생성 비용 자체는 크지는 않지만, 어플리케이션의 성능을 극으로 중요시되는 상황이라면 문제가 될수 있다.
클래스의 필드의 개수가 4개 보다 적고, 필드의 변경 가능성이 없는 경우라면 차라리 생성자나 정적 팩토리 메소드Visit Website를 이용하는 것이 더 좋을 수 있다. 빌더 패턴의 코드가 다소 장황하기 때문이다. 따라서 클래스 필드의 갯수와 필드 변경 가능성을 중점으로 보고 패턴을 적용 유무를 가려야한다.
다만 API 는 시간이 지날수록 많은 매개변수를 갖는 경향이 있기 때문에 애초에 빌더 패턴으로 시작하는 편이 나을 때가 많다고 말하는 경향도 있다.
실플 빌더 패턴은 생성자가 많을 경우 또는 불변 객체가 필요한 경우 코드의 가독성과 일관성, 불변성을 유지하는 것에 중점을 둔다. 심플 빌더 패턴은 위에서 우리가 배운 필더 패턴과 차이가 거의 없다. 다만 빌더 클래스가 구현할 클래스의 정적 내부 클래스로 구현 된다는 점이 다르다.
public class NPC
{
private string name;
private int level;
private NPC(Builder builder)
{
this.name = builder.name;
this.level = builder.level;
}
public string Name => name;
public int Level => level;
public class Builder
{
internal string name;
internal int level;
public Builder Name(string name)
{
this.name = name;
return this;
}
public Builder Level(int level)
{
this.level = level;
return this;
}
public NPC Build()
{
return new NPC(this);
}
}
}
빌더 클래스가 정적 내부 클래스로 구현되는 이유는 아래와 같다.
public class NPC
{
// 불변 객체 필드들 (readonly 키워드로 불변 설정)
private readonly string name;
private readonly int level;
private readonly string race;
private readonly string gender;
private readonly string job;
// 정적 내부 빌더 클래스
public class Builder
{
// 필수 파라미터
internal readonly string name; //이름
internal readonly int level; //레벨
// 선택 파라미터
internal string race; // 종족
internal string gender; // 성별
internal string job; // 직업
// 빌더의 필수 파라미터는 생성자로 받는다
public Builder(string name, int level)
{
this.name = name;
this.level = level;
}
// 선택 파라미터 메서드
public Builder Race(string race)
{
this.race = race;
return this;
}
public Builder Gender(string gender)
{
this.gender = gender;
return this;
}
public Builder Job(string job)
{
this.job = job;
return this;
}
// 최종적으로 NPC 객체를 생성하는 빌드 메서드
public NPC Build()
{
return new NPC(this); // 빌더 객체를 넘겨 최종 NPC 생성
}
}
// private 생성자 - 빌더 클래스에서만 호출 가능
private NPC(Builder builder)
{
this.name = builder.name;
this.level = builder.level;
this.race = builder.race;
this.gender = builder.gender;
this.job = builder.job;
}
// NPC 객체 정보 출력
public override string ToString()
{
return $"NPC {{ Name = {name}, Level = {level}, Race = {race}, Gender = {gender}, Job = {job} }}";
}
}
public class Builder : MonoBehaviour
{
void Start()
{
// 빌더 패턴을 이용해 NPC 생성
NPC npc = new NPC.Builder("마을촌장", 50) // 필수 파라미터
.Gender("man") // 선택 파라미터
.Job("Villager")
.Race("Human")
.Build(); // 최종 빌드
Debug.Log(npc); // NPC 정보 출력
}
}

GOF에서 정의하고 있는 디자인 패턴은 복잡한 객체의 생성 알고리즘과 조립 방법을 분리하여 빌드 공정을 구축하는것이 목적이다. 빌더를 받아 조립 방법을 정의한 클래스를 Director라고 부른다.
심플 빌더 패턴은 하나의 대상 객체에 대한 생성만을 목적을 두지만, 디렉터 빌더 패턴은 여러가지의 빌드 형식을 유연하게 처리하는 것에 목적을 둔다. 어찌보면 일반적인 빌더 패턴을 고도화 시킨 패턴이라고 볼 수 도 있다.

Builder : 빌더 추상 클래스ConcreteBuilder : Builder의 구현체. Product 생성을 담당한다.Director : Builder에서 제공하는 메서드들을 사용해 정해진 순서대로 Product 생성하는 프로세스를 정의Product : Director가 Builder로 만들어낸 결과물.이러한 구조는 클라이언트가 직접 빌더의 모든 API를 사용하는 게 아닌, Director을 통해서 간단하게 인스턴스를 얻어올 수 있고 코드를 재사용할 수 있도록 한다.
public class Data
{
private string name;
private int age;
public Data(string name, int age)
{
this.name = name;
this.age = age;
}
public string GetName()
{
return name;
}
public int GetAge()
{
return age;
}
}
public abstract class Builder
{
// 상속한 자식 클래스에서 사용하도록 protected 접근제어자 지정
protected Data data;
public Builder(Data data)
{
this.data = data;
}
// Data 객체의 데이터들을 원하는 형태의 문자열 포맷을 해주는 메서드들 (머리 - 중간 - 끝 형식)
public abstract string Head();
public abstract string Body();
public abstract string Foot();
}
// Data 데이터를 평범한 문자열로 변환해주는 빌더
public class PlainTextBuilder : Builder
{
public PlainTextBuilder(Data data) : base(data) { }
public override string Head()
{
return "";
}
public override string Body()
{
StringBuilder sb = new StringBuilder();
sb.Append("Name: ");
sb.Append(data.GetName());
sb.Append(", Age: ");
sb.Append(data.GetAge());
return sb.ToString();
}
public override string Foot()
{
return "";
}
}
// Data 데이터를 JSON 형태로 변환해주는 빌더
public class JSONBuilder : Builder
{
public JSONBuilder(Data data) : base(data) { }
public override string Head()
{
return "{\n";
}
public override string Body()
{
StringBuilder sb = new StringBuilder();
sb.Append("\t\"Name\" : ");
sb.Append("\"" + data.GetName() + "\",\n");
sb.Append("\t\"Age\" : ");
sb.Append(data.GetAge());
return sb.ToString();
}
public override string Foot()
{
return "\n}";
}
}
// Data 데이터를 XML 형태로 변환해주는 빌더
public class XMLBuilder : Builder
{
public XMLBuilder(Data data) : base(data) { }
public override string Head()
{
StringBuilder sb = new StringBuilder();
sb.Append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
sb.Append("<DATA>\n");
return sb.ToString();
}
public override string Body()
{
StringBuilder sb = new StringBuilder();
sb.Append("\t<NAME>");
sb.Append(data.GetName());
sb.Append("</NAME>\n");
sb.Append("\t<AGE>");
sb.Append(data.GetAge());
sb.Append("</AGE>");
return sb.ToString();
}
public override string Foot()
{
return "\n</DATA>";
}
}
// 각 문자열 포맷 빌드 과정을 템플릿화 시킨 디렉터
public class Director
{
private Builder builder;
public Director(Builder builder)
{
this.builder = builder;
}
// 빌드 템플릿 메서드
public string Build()
{
StringBuilder sb = new StringBuilder();
sb.Append(builder.Head());
sb.Append(builder.Body());
sb.Append(builder.Foot());
return sb.ToString();
}
}
public class DirectorBuilder : MonoBehaviour
{
void Start()
{
// 1. Data 객체 생성
Data data = new Data("홍길동", 44);
// 2. 일반 텍스트로 포맷하여 출력하기
Builder plainTextBuilder = new PlainTextBuilder(data);
Director plainTextDirector = new Director(plainTextBuilder);
string plainTextResult = plainTextDirector.Build();
Debug.Log(plainTextResult);
// 3. JSON 형식으로 포맷하여 출력하기
Builder jsonBuilder = new JSONBuilder(data);
Director jsonDirector = new Director(jsonBuilder);
string jsonResult = jsonDirector.Build();
Debug.Log(jsonResult);
// 4. XML 형식으로 포맷하여 출력하기
Builder xmlBuilder = new XMLBuilder(data);
Director xmlDirector = new Director(xmlBuilder);
string xmlResult = xmlDirector.Build();
Debug.Log(xmlResult);
}
}

이처럼 Director는 템플릿화 한 메서드를 통해 일관된 프로세스로 인스턴스를 만드는 빌드 과정을 단순화 하고, 클라이언트 쪽에선 Director가 제공하는 메서드를 호출하므로써 코드를 재사용할 수 있게 된다. 즉, Builder는 부품을 만들고, Director는 Builder가 만든 부품을 조합해 제품을 만든다고 할 수 있다.