"생성자 인자가 너무 많아서 순서가 헷갈려요."
"어떤 필드는 필수고, 어떤 필드는 선택값으로 하고 싶어요."
이 문제를 해결하기 위해 우리는 빌더 패턴을 사용합니다. 하지만 우리가 흔히 쓰는 방식(Simple Builder)과 디자인 패턴 책에 나오는 정석 방식(Classic Builder)은 목적과 구조가 다릅니다. 이 글을 읽고 나면 "아, 내가 쓰던 건 빌더의 일부분이었구나"를 깨닫게 되실 겁니다.

1. 문제 상황: 점층적 생성자 패턴 (Telescoping Constructor)
빌더 패턴이 없는 세상에서, 필드가 많은 객체를 생성하는 것은 고역입니다.
Java
// 인자가 5개인데, 3번째 null은 뭐고 4번째 0은 뭐지?
User user = new User("James", "Seoul", null, 0, "Developer");
이런 코드는 가독성이 최악이고, 실수하기도 딱 좋습니다. 이를 해결하기 위해 빌더 패턴이 등장했습니다.
2. 방식 1: 심플 빌더 (Simple Builder) - "Joshua Bloch 스타일"
현업에서 99% 사용하는 방식입니다. **"불변성(Immutability)"**과 **"가독성(Fluent API)"**을 확보하는 것이 주 목적입니다. Lombok의 @Builder가 바로 이 방식입니다.
특징
- static 내부 클래스를 만들어 메서드 체이닝(return this)을 구현합니다.
- 객체 생성을 한 줄의 직관적인 문장처럼 만듭니다.
코드 예시
Java
public class User {
private final String name;
private final int age;
// 생성자는 private으로 닫음
private User(Builder builder) {
this.name = builder.name;
this.age = builder.age;
}
public static class Builder {
private String name;
private int age;
// 메서드 체이닝을 위해 this 반환
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public User build() {
return new User(this);
}
}
}
// 사용 (Client)
User user = new User.Builder()
.name("James")
.age(30)
.build();
장단점
- 장점: 읽기 쉽고, 객체 생성 후 값이 변하지 않음(Immutable)을 보장할 수 있습니다.
- 단점: 객체 하나 만들 때마다 별도의 Builder 객체를 먼저 만들어야 하므로 미세한 메모리 비용이 발생합니다.
3. 방식 2: 클래식 빌더 (Classic Builder) - "GoF 스타일"
디자인 패턴의 원형입니다. 단순히 객체를 예쁘게 만드는 게 아니라, **"복잡한 생성 과정을 단계별로 분리하고, 조립(Director)하는 것"**에 초점을 맞춥니다.
구조
- Builder: 부품을 만드는 인터페이스
- ConcreteBuilder: 실제 부품을 조립하는 구현체
- Director (감독관): 빌더에게 "A 넣고, B 넣고, 완성해"라고 순서를 지시하는 관리자
코드 예시 (피자 만들기)
Java
// 1. 빌더 인터페이스
interface PizzaBuilder {
void buildDough();
void buildSauce();
void buildTopping();
Pizza getPizza();
}
// 2. 구체적 빌더 (하와이안 피자 요리사)
class HawaiianPizzaBuilder implements PizzaBuilder {
private Pizza pizza = new Pizza();
public void buildDough() { pizza.setDough("쫄깃한 도우"); }
public void buildSauce() { pizza.setSauce("토마토 소스"); }
public void buildTopping() { pizza.setTopping("파인애플"); }
public Pizza getPizza() { return pizza; }
}
// 3. 디렉터 (매니저 - 만드는 순서를 알고 있음)
class Waiter { // Director
private PizzaBuilder pizzaBuilder;
public void setPizzaBuilder(PizzaBuilder pb) { this.pizzaBuilder = pb; }
public Pizza constructPizza() {
pizzaBuilder.buildDough(); // 1. 도우 깔고
pizzaBuilder.buildSauce(); // 2. 소스 바르고
pizzaBuilder.buildTopping(); // 3. 토핑 올림
return pizzaBuilder.getPizza();
}
}
사용하는 곳 (Client)
Java
Waiter waiter = new Waiter();
PizzaBuilder hawaiianBuilder = new HawaiianPizzaBuilder();
waiter.setPizzaBuilder(hawaiianBuilder);
Pizza pizza = waiter.constructPizza(); // "하와이안 피자 만들어줘"
장단점
- 장점: "생성 알고리즘(순서)"과 "부품 구현(재료)"이 완전히 분리됩니다. 같은 Waiter(Director)에게 PepperoniBuilder만 쥐여주면 페퍼로니 피자가 나옵니다.
- 단점: 구조가 복잡해지며, 단순한 DTO 생성에는 과합니다.
4. 요약: Simple vs Classic
| 구분 | Simple Builder (Lombok) | Classic Builder (GoF) |
| 핵심 목적 | 가독성 및 불변 객체 생성 편의 | 생성 과정(순서)의 캡슐화 및 재사용 |
| Director 유무 | 없음 (클라이언트가 직접 값 세팅) | 있음 (Director가 조립 순서 제어) |
| 형태 | User.builder().set().build() | director.construct() |
| 추천 상황 | DTO, Entity 등 데이터 객체 생성 시 | 문서 변환기(MD->HTML), 복잡한 조립 로직 |
결론
우리가 평소에 쓰는 @Builder는 Simple Builder입니다. 데이터 객체를 깔끔하게 만들기엔 이보다 좋은 건 없습니다.
하지만 면접이나 아키텍처 설계 시 "객체의 생성 순서가 중요하거나, 생성 과정 자체를 재사용해야 한다면?" 이라는 질문을 받는다면, Director가 존재하는 Classic Builder 패턴을 떠올려야 합니다.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| 상태 패턴(State Pattern) 완벽 정리 (0) | 2025.12.07 |
|---|---|
| 템플릿 메서드 패턴(Template Method Pattern) 총정리 (0) | 2025.12.07 |
| 컴포지트 패턴(Composite Pattern) (0) | 2025.12.07 |
| 어댑터 패턴(Adapter Pattern) 완벽 정리 (1) | 2025.12.07 |
| 프록시 패턴(Proxy Pattern) (0) | 2025.12.07 |