
이 내용은 Backend / OOP design in Java 영역에서 “객체 생성(creation)”을 안정적으로 설계하는 대표 패턴이다. 필드가 많아질수록 생성자 기반 생성 방식은 유지보수·가독성·확장성에서 한계가 생기고, 그 문제를 해결하려고 Builder Pattern이 등장했다.
필드가 많아질수록 생성자의 매개변수도 늘어나고, 생성자 호출부가 “긴 한 줄”이 되어 실수 확률이 급격히 올라간다. 롬복을 써서 생성자 코드는 짧아질 수 있어도, @NonNull / final 같은 제약이 들어가면 기본 생성자 생성 불가 같은 문제가 함께 따라온다.
필요한 조합이 달라질 때마다 생성자를 하나씩 늘려야 한다. 예) 예전엔 집전화가 필수 → 지금은 optional. 필드가 optional로 바뀌면 생성자 시그니처도 바꿔야 하고, 기존 호출부도 전부 수정이 필요해진다.
필드 하나 추가/삭제만 해도 AllArgsConstructor 호출부가 전부 깨진다. 메인에서 new 생성자 호출하던 라인들이 전부 오류나면서 수정 범위가 넓어지고, 배포 리스크도 커진다.
생성자 호출만 보면 “이 값이 어떤 필드인지” 알기 어렵다.
Student student = new Student("김일", "김이");
// "김일"이 name인지 school인지, "김이"가 name인지 school인지 모호함
Builder Pattern은 복잡한 생성자 대신 객체를 단계적으로(체이닝) 생성할 수 있게 해주는 “생성(Creational) 디자인 패턴”이다. GoF(Gang of Four) 디자인 패턴 중 하나이며, 목표는 가독성 + 유연성 + 유지보수성이다.
여기서 유연성은 필드를 순서대로 넣지 않아도 되고, 순서를 바꿔도 정확한 필드에 값이 들어가도록 만든다는 의미다.
핵심 구조는 3가지다.
import lombok.ToString;
@ToString
public class Person {
private String name;
private int age;
private String address;
// Builder만 통해 생성되도록 생성자를 private로 잠금
private Person(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.address = builder.address;
}
public static class Builder {
private String name;
private int age;
private String address;
public Builder name(String name) {
this.name = name;
return this; // chaining
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public Person build() {
return new Person(this);
}
}
}
Lombok의 @Builder는 위의 Builder 클래스를 자동 생성해준다. 대신 “자동 생성된 builder() 메서드 + 체이닝 setter + build()” 패턴으로 사용한다.
import lombok.Builder;
import lombok.ToString;
@ToString
@Builder
public class Person2 {
private String name;
private int age;
private String address;
}
public class PersonMain {
public static void main(String[] args) {
// 수동 Builder: 순서 상관없이 세팅 가능
Person person1 = new Person.Builder()
.age(21)
.address("부산광역시 부산진구")
.name("김일")
.build();
System.out.println(person1);
// Builder를 사용하면 "NoArgsConstructor 같은 효과"로도 생성 가능
Person person2 = new Person.Builder().build();
System.out.println(person2);
Person person3 = new Person.Builder()
.name("김삼")
.build();
System.out.println(person3);
// Lombok @Builder
Person2 person4 = Person2.builder()
.name("김사")
.build();
System.out.println(person4);
Person2 person5 = Person2.builder().build();
System.out.println(person5);
Person2 person6 = Person2.builder()
.age(26)
.name("김육")
.address("광주광역시 북구")
.build();
System.out.println(person6);
}
}
빌더의 체이닝 메서드는 “값 세팅 + 자기 자신 반환”이다.
Builder는 편하지만, 아무 값도 안 넣고 build()가 가능해져서 필수값 누락 위험이 있다. 실무에선 보통 다음 방식 중 하나로 제어한다.
subjects, scores 같은 배열/리스트는 외부에서 참조가 공유되면 위험하다(얕은 복사). 필요하면 build() 시점에 복사해서 방어적 복사(defensive copy)하는 방식도 고려한다.
@AllArgsConstructor는 “매개변수 순서 의존” 문제가 그대로 남는다. 필드가 많아지는 DTO라면 @Builder가 더 안전한 선택이다.
Builder Pattern은 “생성자 매개변수 지옥”을 해결하기 위한 생성 패턴이다. 필드가 많아도 명시적이고 안전하게 객체를 만들 수 있고, 필드 추가/변경에도 호출부 수정이 최소화된다.
즉, 빌더 패턴은 단순 편의 문법이 아니라 유지보수성과 실수 방지를 위한 설계다.