목차
규칙 2. 생성자 인자가 많으면 Builder를 고려하기
필수 값 두 개와 선택 값 여러 개를 한 생성자에 순서대로 넣으면 new NutritionFacts(240, 9, 100, 0, 25, 12)의 숫자가 각각 무엇을 뜻하는지 읽기 어렵다. 매 선택 값 조합마다 생성자를 늘리는 점층적 생성자 방식은 중복도 생긴다. 반대로 기본 생성자 뒤에 setter를 여러 번 호출하면 필수 값이 빠진 중간 상태를 만들 수 있다. Builder는 필수 값은 처음에, 선택 값은 이름 붙은 메서드로, 검증은 build()에서 처리한다.
완성 예제
public final class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int sodium;
private NutritionFacts(Builder builder) {
this.servingSize = builder.servingSize;
this.servings = builder.servings;
this.calories = builder.calories;
this.sodium = builder.sodium;
}
public static final class Builder {
private final int servingSize;
private final int servings;
private int calories;
private int sodium;
public Builder(int servingSize, int servings) {
if (servingSize <= 0 || servings <= 0)
throw new IllegalArgumentException("필수 수량은 양수여야 합니다");
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int value) {
if (value < 0) throw new IllegalArgumentException("열량은 0 이상이어야 합니다");
this.calories = value;
return this;
}
public Builder sodium(int value) {
if (value < 0) throw new IllegalArgumentException("나트륨은 0 이상이어야 합니다");
this.sodium = value;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
public int calories() { return calories; }
public int sodium() { return sodium; }
}
NutritionFacts facts = new NutritionFacts.Builder(240, 9)
.calories(100)
.sodium(25)
.build();
System.out.println(facts.calories()); // 100
Builder(240, 9)에서 필수 수량을 확인한다. 선택 값은 기본 0이고, setter처럼 보이는 메서드가 빌더 자신을 반환해 호출을 이어 붙인다. build()는 NutritionFacts의 final 필드로 값을 복사한다. 이 예제의 완성 객체는 정수 필드만 갖기 때문에 외부에서 내용을 바꿀 경로가 없다. 목록 같은 가변 값이 들어오면 빌더로 받을 때 또는 완성 객체를 만들 때 방어적으로 복사해야 한다.
중간 상태와 재사용 정책
빌더 자체는 변경 가능한 객체다. Builder b = ...; NutritionFacts a = b.build(); b.calories(200); NutritionFacts c = b.build();를 실행하면 a는 100, c는 200 같은 서로 다른 완성 객체를 만든다. 이 동작을 허용할지, build() 뒤 빌더를 다시 쓰지 못하게 할지는 API 계약이다. 여러 스레드가 같은 빌더를 동시에 바꾸는 것은 별도 동기화가 없다면 안전하지 않다.
필수 값 사이에 sodium <= ... 같은 교차 조건이 있다면 개별 설정 메서드만으로 검사할 수 없으므로 build()에서도 검증한다. 잘못된 객체가 밖으로 나간 뒤에만 실패하는 것보다 완성 시점에 한 번 확인하는 편이 명확하다.
| 상황 | 고려할 방법 |
|---|---|
| 필수 인자가 한두 개, 선택 값이 거의 없음 | 생성자 또는 정적 팩터리 |
| 선택 값이 많고 같은 타입이 반복됨 | Builder |
| 일부 필드를 수정하며 재사용하는 내부 설정 객체 | 가변 객체, 단 중간 상태 관리 |
| 단순 데이터 전달, 변경 불필요 | record와 검증 생성자도 고려 |
빌더는 코드가 길어지고 완성 전에 별도 객체 하나를 만든다. 모든 2~3개 인자 객체에 습관적으로 적용하기보다 호출 코드의 가독성과 불변 조건의 수를 기준으로 정한다. 원문의 점층적 생성자 예제에는 타입이 빠진 sodium 인자와 sodifum 오타가 있어 컴파일되지 않았으므로 완성 가능한 빌더 예제로 바꿨다.