복잡한 객체를 단계적으로 조립하는 패턴.
무엇을 푸나 — 점층적 생성자의 지옥
// ❌ 인자가 늘어날수록 생성자가 폭발한다
new Pizza(12, true, false, true, false, true); // 무엇이 무엇인지 알 수 없다
new Pizza(12);
new Pizza(12, true);
new Pizza(12, true, false); // 조합마다 오버로드
빌더로
Pizza pizza = new Pizza.Builder(12)
.cheese(true)
.pepperoni(true)
.build(); // 여기서 필수값 검증도 한다
public class Pizza {
private final int size;
private final boolean cheese;
private Pizza(Builder b) { this.size = b.size; this.cheese = b.cheese; }
public static class Builder {
private final int size; // 필수는 생성자로
private boolean cheese = false; // 선택은 기본값으로
public Builder(int size) { this.size = size; }
public Builder cheese(boolean v) { this.cheese = v; return this; }
public Pizza build() {
if (size < 8) throw new IllegalArgumentException("너무 작다");
return new Pizza(this);
}
}
}
필수는 빌더 생성자로, 선택은 메서드로 나누는 것이 관용구다.
얻는 것
- 이름이 있어 무슨 값인지 읽힌다
- 불변 객체를 만들 수 있다 (모든 필드 final)
- build() 에서 한 번에 검증한다 (객체가 반쯤 만들어진 상태로 존재하지 않는다)
- 순서에 자유롭다
언어가 이미 지원하기도 한다
-
명명 인자 + 기본 인자가 있는 언어에서는 빌더가 필요 없는 경우가 많다
-
자바에서도 record + 정적 팩토리로 충분한 경우가 있다
-
필드가 4~5개를 넘고 선택적인 것이 많을 때가 빌더의 자리다
Prototype — 복제로 만든다
// 생성 비용이 크거나 현재 상태를 그대로 복제해야 할 때
GameObject clone = original.clone();
주의: 얕은 복사와 깊은 복사를 구분해야 한다
- 얕은 복사면 내부 객체를 공유해 한쪽 수정이 다른 쪽에 보인다
자바의 Cloneable 은 설계가 나쁘기로 유명하다
- 복사 생성자나 정적 팩토리(copyOf)를 쓰는 편이 낫다
면접 함정
- ❌ "빌더는 setter의 다른 형태" → setter는 객체를 가변으로 만들고, 빌더는 불변 객체를 만드는 수단이다.
- ❌ "빌더를 쓰면 항상 좋다" → 필드가 두세 개면 코드만 늘어난다.
롬복·코틀린에서는
@Builder // 롬복이 빌더를 생성해 준다
public class Pizza { private final int size; private final boolean cheese; }
Pizza.builder().size(12).cheese(true).build();
그런데 롬복 @Builder 는 필수값 검증이 없다
- 아무것도 안 넣고 build() 해도 통과한다
- @NonNull 을 붙이거나 커스텀 build() 를 두어야 한다
"코드를 줄여 주는 도구" 가 "안전성" 까지 주지는 않는다
언어 기능이 대체하는 경우
// 명명 인자 + 기본값이 있으면 빌더가 거의 필요 없다
data class Pizza(
val size: Int,
val cheese: Boolean = false,
val pepperoni: Boolean = false
)
Pizza(size = 12, cheese = true)
빌더 패턴이 필요한 이유의 상당 부분이 "자바에 명명 인자가 없어서" 였다 — 언어가 개선되면 패턴이 사라지는 사례다.
단계가 강제되는 빌더
// 타입으로 순서를 강제할 수도 있다 (step builder)
interface SizeStep { ToppingStep size(int s); }
interface ToppingStep { BuildStep cheese(boolean b); }
// → size 를 안 넣으면 애초에 build() 에 도달할 수 없다