
Java를 개발할 때 개발 편의성을 높이기 위해 대부분 Lombok을 사용합니다. Lombok이 편리한 것은 사실이지만 @Data 주석을 잘못 사용하면 객체 생성 시 정렬이 잘못될 수 있습니다. Getter, Setter, RequiredArgsConstructor, ToString, EqualsAndHashCode 및 Value를 한 번에 적용하는데 위험하지 않은 것이 이상합니다.
세터의 무분별한 사용, ToString으로 인한 순환 참조 문제 등 @Data나 @Setter 애노테이션의 위험성은 알려져 있지만, 함께 사용되는 경우가 많은 @Builder 애노테이션의 위험성은 상대적으로 낮은 것으로 알려져 있다.내 느낌…) 나 같은 사람들이 ‘그건 좀 위험하다’고 해서 그냥 못쓰는 구석이 많다.
빌더 패턴
디자인 패턴 중 Builder 패턴은 객체 생성 단계를 캡슐화하는 데 사용됩니다. 객체 생성 과정과 표현 방식을 분리함으로써 동일한 생성 과정에서도 서로 다른 표현 결과를 생성할 수 있습니다. 주로 복잡한 객체 구조를 구축하는 데 사용됩니다.
빌더 패턴 트리

빌더 패턴 객체
AbstractBuilder – Foo 객체 생성을 위한 추상 인터페이스 정의
ConcreateBuilder – AbstractBuilder 추상 인터페이스를 구현합니다. 빌더 부품을 조립하여 실제 개체를 만들어 복잡한 구조를 만듭니다.
고객 – 클라이언트는 빌더에게 객체 생성을 요청합니다. 빌더 인터페이스를 사용하여 개체를 만듭니다. 마지막으로 getResultFoo() 메서드를 호출하여 완성된 개체를 가져옵니다.
샘플 빌더 패턴 코드
– 사용자 클래스
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
public class User {
private int id;
private String name;
private String emailAddress;
private boolean isVerified;
private LocalDateTime createdAt;
private List<Integer> friendUserIds;
public User(Builder builder) {
this.id = builder.id;
this.name = builder.name;
this.emailAddress = builder.emailAddress;
this.isVerified = builder.isVerified;
this.createdAt = builder.createdAt;
this.friendUserIds = builder.friendUserIds;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public Optional<String> getEmailAddress() {
return Optional.ofNullable(emailAddress);
}
public boolean isVerified() {
return isVerified;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public List<Integer> getFriendUserIds() {
return friendUserIds;
}
public static Builder builder(int id, String name) {
return new Builder(id, name);
}
public static class Builder {
private int id;
private String name;
public String emailAddress;
public boolean isVerified;
public LocalDateTime createdAt;
public List<Integer> friendUserIds = new ArrayList<>(); // 기본값(빈값) 설정
public Builder(Builder builder) {
this.id = builder.id;
this.name = builder.name;
this.emailAddress = builder.emailAddress;
this.isVerified = builder.isVerified;
this.createdAt = builder.createdAt;
this.friendUserIds = builder.friendUserIds;
}
private Builder(int id, String name) {
this.id = id;
this.name = name;
}
// 하나로 합쳐서 build 가능한 메서드
public Builder with(Consumer<Builder> consumer) {
consumer.accept(this);
return this;
}
public Builder withEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
return this;
}
public Builder withIsVerified(boolean isVerified) {
this.isVerified = isVerified;
return this;
}
public Builder withCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
return this;
}
public Builder withFriendUserids(List<Integer> friendUserIds) {
this.friendUserIds = friendUserIds;
return this;
}
public User build() {
return new User(this);
}
}
@Override
public String toString() {
return "User (id=" + id + ", " + (name != null ? "name=" + name + ", " : "")
+ (emailAddress != null ? "emailAddress=" + emailAddress + ", " : "") + "isVerified=" + isVerified
+ ", " + (friendUserIds != null ? "friendUserIds=" + friendUserIds : "") + ")";
}
}
– User 클래스 생성기 사용 예
// 개별적으로 생성
User user1 = User.builder(101, "Alice")
.withEmailAddress("[email protected]")
.withCreatedAt(LocalDateTime.now())
.withIsVerified(true)
.build();
// with 메서드 하나로 생성
User user2 = User.builder(102, "Bob")
.with((builder) -> {
builder.isVerified = true;
builder.friendUserIds = Arrays.asList(101, 103, 104);
builder.emailAddress = "[email protected]";
builder.createdAt = LocalDateTime.now();
}).build();
두 가지 유형이 위에 나와 있습니다. 각각을 개별적으로 입력하고 방법과 함께 사용하는 두 가지 방법이 있습니다. 일반적으로 Lombok으로 자동 생성된 빌더 패턴은 첫 번째 방법과 유사합니다.
롬복 @빌더 노트
이제 lombok의 @Builder 주석을 클래스에 첨부하면 어떤 일이 발생하는지 살펴보겠습니다.
import java.time.LocalDateTime;
import java.util.List;
import lombok.Builder;
@Builder
public class Foo {
private int id;
private String name;
private String emailAddress;
private boolean isVerified;
private LocalDateTime createdAt;
private List<Integer> friendUserIds;
}
다음은 롬복이 컴파일 타임에 바이트코드를 변환하여 빌더 패턴을 주입한 결과입니다.
import java.time.LocalDateTime;
import java.util.List;
public class Foo {
private int id;
private String name;
private String emailAddress;
private boolean isVerified;
private LocalDateTime createdAt;
private List<Integer> friendUserIds;
Foo(int id, String name, String emailAddress, boolean isVerified, LocalDateTime createdAt, List<Integer> friendUserIds) {
this.id = id;
this.name = name;
this.emailAddress = emailAddress;
this.isVerified = isVerified;
this.createdAt = createdAt;
this.friendUserIds = friendUserIds;
}
public static FooBuilder builder() {
return new FooBuilder();
}
public static class FooBuilder {
private int id;
private String name;
private String emailAddress;
private boolean isVerified;
private LocalDateTime createdAt;
private List<Integer> friendUserIds;
FooBuilder() {
}
public FooBuilder id(int id) {
this.id = id;
return this;
}
public FooBuilder name(String name) {
this.name = name;
return this;
}
public FooBuilder emailAddress(String emailAddress) {
this.emailAddress = emailAddress;
return this;
}
public FooBuilder isVerified(boolean isVerified) {
this.isVerified = isVerified;
return this;
}
public FooBuilder createdAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
return this;
}
public FooBuilder friendUserIds(List<Integer> friendUserIds) {
this.friendUserIds = friendUserIds;
return this;
}
public Foo build() {
return new Foo(this.id, this.name, this.emailAddress, this.isVerified, this.createdAt, this.friendUserIds);
}
public String toString() {
return "Foo.FooBuilder(id=" + this.id + ", name=" + this.name + ", emailAddress=" + this.emailAddress + ", isVerified=" + this.isVerified + ", createdAt=" + this.createdAt + ", friendUserIds=" + this.friendUserIds + ")";
}
}
}
생성자에서 매개변수를 받거나 아래와 같이 빌더를 통해 값을 구성하여 객체를 생성할 수 있습니다.
Foo foo = Foo.builder()
.name("달리")
.emailAddress("12345")
.build();
롬복으로 메모만 첨부했는데 너무 편해요!
하지만 너무 편리한 것이 있다면 부작용을 의심해봐야 한다. 그렇다면 고려해야 할 사항은 무엇입니까?
Lombok @Builder 사용 시 주의사항
위에서 볼 수 있듯이 클래스에 @Builder 주석을 작성하면 모든 멤버 변수를 받는 기본 생성자를 생성하고 이에 따라 빌더 샘플 코드가 완성됩니다. DB와의 상호 운용성은 id나 createdAt 값과 같이 DB가 자동으로 생성하는 값으로 귀결되는 경우가 많으며, 이 경우 이러한 값을 지정할 수 있는 개방형 구조가 문제가 될 수 있습니다.
Foo foo = Foo.builder()
.id(100) // id를 자동으로 생성해준다면 이렇게 id를 넣어줄 필요가 없다.
.createdAt(LocalDateTime.now().minusDays(1) // 의도하지 않은 다른 값을 넣을 수 있다.
.build(); // (정책적으로 정해둔) 필수값이 빠져도 생성됨
만약에 객체 생성 시 필수 값이 있어도 모든 멤버 변수를 대상으로 하는 빌더는 필수 값이 제공되지 않아도 오류가 발생할 수 있습니다.
그래서 마지막으로
오퍼레이션에 필요한 파라미터를 설정하는 생성자에 @Builder 어노테이션을 붙여서 사용해보자.
@Builder
public Foo(String name, String emailAddress) {
this.name = name;
this.emailAddress = emailAddress;
this.isVerified = false;
this.createdAt = LocalDateTime.now();
this.friendUserIds = new ArrayList<>();
}
위의 예에서는 초기 고정 값 없이 이름과 이메일만 입력했습니다. 이렇게 컴파일하면

위의 이미지에서와 같이 다른 멤버 변수는 오류를 방지하기 위해 닫혀 있습니다. 완벽한 사람은 없듯이 모든 것에는 타협이 있다는 것을 기억하고, 예상치 못한 사고는 반드시 시스템으로 해결해야 합니다.
혹시.
정리가 잘 되어 있어서 아래 윤님의 글을 추천합니다.
(참조)
GoF 디자인 패턴
머리부터 발끝까지 디자인 패턴 – Hanbit Media