new User("김자바", null, null, 25, null, true, false)
생성자에 파라미터가 많아지면 이런 코드가 나온다. null이 몇 개인지, 25가 나이인지 ID인지, true와 false가 뭘 의미하는지 전혀 알 수 없다. Builder는 이 문제를 해결하기 위한 패턴이다.
복잡한 객체를 단계별로 조립하듯 만드는 패턴이다. 필요한 속성만 골라서 이름을 붙여가며 설정할 수 있고, 생성자에 인자를 잔뜩 넣는 것보다 훨씬 읽기 쉽다.
public class User {
private final String name;
private final String email;
private final int age;
private final boolean isAdmin;
// 생성자는 Builder만 호출할 수 있게 private
private User(Builder builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.isAdmin = builder.isAdmin;
}
// 정적 중첩 클래스로 Builder 정의
public static class Builder {
private String name;
private String email;
private int age;
private boolean isAdmin = false; // 기본값 설정 가능
public Builder name(String name) {
this.name = name;
return this; // 메서드 체이닝을 위해 자기 자신 반환
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder isAdmin(boolean isAdmin) {
this.isAdmin = isAdmin;
return this;
}
public User build() {
return new User(this);
}
}
}
User user = new User.Builder()
.name("김자바")
.email("java@example.com")
.age(25)
.build();
각 메서드가 이름을 갖고 있어서 어떤 값이 어디에 들어가는지 한눈에 보인다. 필요 없는 속성은 그냥 생략하면 된다.

return this를 통해 Builder 메서드들을 연결해서 쓸 수 있다. 이걸 메서드 체이닝(Method Chaining) 이라고 한다.
// 체이닝 없이 쓰면
Builder builder = new User.Builder();
builder.name("김자바");
builder.age(25);
User user = builder.build();
// 체이닝으로 쓰면
User user = new User.Builder()
.name("김자바")
.age(25)
.build();
build() 시점에 필수 값이 있는지 검사할 수 있다.
public User build() {
if (name == null || name.isBlank()) {
throw new IllegalStateException("name은 필수입니다.");
}
return new User(this);
}
생성자에서 검증하면 어떤 파라미터가 문제인지 파악하기 어렵지만, Builder에서는 메서드 이름으로 명확하게 알 수 있다.
실제로 매번 Builder 클래스를 직접 작성하는 건 번거롭다. Lombok 라이브러리의 @Builder 어노테이션을 쓰면 위 코드를 자동으로 생성해준다.
import lombok.Builder;
@Builder
public class User {
private String name;
private String email;
private int age;
private boolean isAdmin;
}
User user = User.builder()
.name("김자바")
.email("java@example.com")
.age(25)
.build();
코드 양이 크게 줄어든다. Builder 패턴을 직접 구현할 일은 많지 않지만, 구조를 이해하고 있으면 Lombok이 뭘 대신 해주는지 파악하는 데 도움이 된다.
파라미터가 많은 생성자를 보면서 "이게 뭐지?" 싶었던 적이 있다면, Builder가 그 답이다. 무엇을 만들고 있는지 코드만 봐도 읽히는 것, 그게 Builder가 주는 가장 큰 이점이다.