🏗️ @Builder
“생성자가 너무 길어요… 더 나은 방법 없나요?” 🤔
private/final → 생성자 → @Builder 순서로 오셨다면, 이미 객체 설계 감각이 생기고 있다는 신호입니다! 👍
🎯 @Builder가 뭔가요?
한 줄 요약: Lombok이 제공하는 빌더 패턴(Builder Pattern) 자동 생성 어노테이션
👉 객체 생성 과정을 단계적으로, 명확하게 표현하게 해줍니다.
😫 빌더 패턴이 없을 때의 문제
Case 1: 파라미터 지옥
public class Member {
private String name;
private int age;
private String email;
private String address;
private String phone;
public Member(String name, int age, String email, String address, String phone) {
this.name = name;
this.age = age;
this.email = email;
this.address = address;
this.phone = phone;
}
}
사용할 때:
Member member = new Member("Kobe", 30, "kobe@test.com", "Seoul", "010-1234-5678");
❌ 문제점:
- 파라미터 순서 헷갈림 (age랑 name 순서가 뭐였더라?)
- 어떤 값이 뭔지 한눈에 안 보임
-
"Seoul"이 주소인지 회사인지 모름 - 실수로 순서 바꿔도 컴파일 에러 안 남 (같은 타입이면)
Case 2: 선택 값(Optional) 처리의 악몽
// 전화번호는 선택사항이라면?
// 방법 1: 생성자 오버로딩
public Member(String name, int age, String email, String address) { }
public Member(String name, int age, String email, String address, String phone) { }
// 방법 2: null 전달
new Member("Kobe", 30, "kobe@test.com", "Seoul", null); // 😱
❌ 문제점:
- 생성자가 기하급수적으로 늘어남
- null 값 전달이 명시적이지 않음
- 조합이 복잡해지면 관리 불가능
✨ @Builder 사용하면?
@Builder
public class Member {
private String name;
private int age;
private String email;
private String address;
private String phone;
}
사용할 때:
Member member = Member.builder()
.name("Kobe")
.age(30)
.email("kobe@test.com")
.address("Seoul")
.phone("010-1234-5678")
.build();
✅ 장점:
- 🎨 가독성 최고 - 필드명이 명확히 보임
- 🔀 순서 상관 없음 - 원하는 순서로 작성 가능
- 🎯 선택 값 생략 가능 - phone 빼도 됨
- 💡 의미 전달 명확 - 코드만 봐도 무슨 객체인지 알 수 있음
🔍 @Builder가 내부적으로 만들어주는 것
@Builder
public class Member {
private String name;
private int age;
}
⬇️ Lombok이 생성하는 코드 (개념)
public class Member {
private String name;
private int age;
// 1. Builder 클래스 생성
public static class MemberBuilder {
private String name;
private int age;
public MemberBuilder name(String name) {
this.name = name;
return this; // 체이닝을 위해 자기 자신 반환
}
public MemberBuilder age(int age) {
this.age = age;
return this;
}
public Member build() {
return new Member(name, age);
}
}
// 2. 정적 팩토리 메서드
public static MemberBuilder builder() {
return new MemberBuilder();
}
// 3. 생성자
private Member(String name, int age) {
this.name = name;
this.age = age;
}
}
핵심 원리: 메서드 체이닝
builder() → name() → age() → build()
각 메서드가 자기 자신을 리턴하므로 계속 이어서 호출 가능!
💼 @Builder는 언제 사용하나요?
1️⃣ 생성자 파라미터가 많을 때 (가장 대표적) ⭐
// ❌ 생성자 방식 - 가독성 최악
Order order = new Order(
memberId,
productId,
price,
quantity,
LocalDateTime.now(),
"Seoul",
"010-1234-5678"
); // 뭐가 뭔지 모르겠어요...
// ✅ Builder 방식 - 가독성 최고
Order order = Order.builder()
.memberId(memberId)
.productId(productId)
.price(price)
.quantity(quantity)
.orderedAt(LocalDateTime.now())
.deliveryAddress("Seoul")
.contactPhone("010-1234-5678")
.build(); // 명확해요!
📌 기준: 파라미터 3개 이상이면 Builder 고려!
2️⃣ 선택 값(Optional field)이 많을 때
@Builder
public class Profile {
private String nickname; // 필수
private String bio; // 선택
private String imageUrl; // 선택
private String website; // 선택
private String location; // 선택
}
사용 예시:
// 필수값만 설정
Profile profile1 = Profile.builder()
.nickname("kobe")
.build();
// 원하는 것만 추가
Profile profile2 = Profile.builder()
.nickname("kobe")
.bio("Backend Developer")
.imageUrl("https://...")
.build();
✨ 생성자 오버로딩 10개 만드는 것보다 훨씬 깔끔!
3️⃣ 불변 객체(final)를 만들 때
@Getter
@Builder
public class MemberDto {
private final Long id;
private final String name;
private final String email;
private final LocalDateTime joinDate;
}
왜 좋은가?
// ✅ setter 필요 없음
MemberDto dto = MemberDto.builder()
.id(1L)
.name("Kobe")
.email("kobe@test.com")
.joinDate(LocalDateTime.now())
.build();
// dto.setName("..."); // setter 없음! 불변 보장
💎 불변성 + 가독성 = 완벽한 조합
4️⃣ 테스트 코드 작성할 때
@Test
void memberTest() {
// ✅ 테스트 의도가 명확
Member member = Member.builder()
.name("test")
.age(20)
.email("test@test.com")
.build();
// 테스트 진행...
}
@Test
void orderTest() {
// ✅ 필요한 값만 설정
Order order = Order.builder()
.memberId(1L)
.status(OrderStatus.COMPLETED) // 이 테스트의 핵심!
.build();
}
👍 테스트 유지보수가 압도적으로 쉬워집니다!
5️⃣ 도메인 의미가 중요한 경우
// ❌ 의미가 불명확
Order order = new Order(1L, 2L, 10000, 2, LocalDateTime.now());
// ✅ 도메인 의미가 코드로 보임
Order order = Order.builder()
.memberId(1L) // 누가 주문했는지
.productId(2L) // 무엇을 주문했는지
.totalPrice(10000) // 얼마를 지불했는지
.quantity(2) // 몇 개를 주문했는지
.orderedAt(LocalDateTime.now()) // 언제 주문했는지
.build();
💡 “이 주문은 이런 의미를 가진다”가 코드로 표현됨!
⚠️ 언제 사용하면 안 좋을까?
❌ 1. 필드가 1~2개뿐일 때
// 이럴 땐 생성자가 더 직관적
public class Point {
private int x;
private int y;
}
// ✅ 생성자가 낫다
Point point = new Point(10, 20);
// ❌ 오버엔지니어링
Point point = Point.builder()
.x(10)
.y(20)
.build();
📌 기준: 파라미터 2개 이하면 생성자 추천
❌ 2. JPA Entity에 무분별하게 사용
// ⚠️ 위험한 패턴
@Entity
@Builder // 조심!
@NoArgsConstructor
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
}
🤔 왜 위험한가?
// 아무나 Entity를 마음대로 생성 가능
Member member = Member.builder()
.id(999L) // ⚠️ ID를 임의로 설정?
.name("해커")
.build();
✅ 올바른 패턴:
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA용
@AllArgsConstructor(access = AccessLevel.PRIVATE) // Builder용
@Builder
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
// 또는 Builder를 특정 생성자에만 적용
@Builder
public Member(String name) { // id는 빠짐!
this.name = name;
}
}
🎯 핵심:
- 외부에서는 Builder로만 생성 가능
- 민감한 필드(id 등)는 Builder에서 제외
❌ 3. 상태 변경이 많은 객체
// 상태 변화가 핵심인 객체
public class GameCharacter {
private int hp;
private int level;
private int exp;
// ❌ Builder보다는 의미 있는 메서드가 낫다
public void levelUp() {
this.level++;
this.hp = level * 100;
}
public void takeDamage(int damage) {
this.hp -= damage;
}
}
💡 변경이 핵심이면 Builder보다 도메인 메서드 사용!
📊 생성자 vs Builder 완전 비교
| 항목 | 생성자 | Builder |
|---|---|---|
| 📝 파라미터 많음 (4개 이상) | ❌ 가독성 떨어짐 | ✅ 명확하고 깔끔 |
| 👀 가독성 | 낮음 | 높음 |
| 🎯 선택 값 처리 | 불편 (오버로딩 필요) | 매우 편함 |
| 💎 불변 객체 | 가능 | 더 적합 |
| 🧪 테스트 코드 | 불편 | 매우 편함 |
| ⚡ 성능 | 약간 빠름 | 약간 느림 (무시 가능) |
| 🎨 필드 2개 이하 | 더 적합 | 오버엔지니어링 |
| 📖 코드 양 | 적음 | 많음 (Lombok이 생성) |
🏆 실무 Best Practice
패턴 1: DTO (API 요청/응답)
@Getter
@Builder
public class MemberResponse {
private final Long id;
private final String name;
private final String email;
private final LocalDateTime joinDate;
}
// Controller에서
@GetMapping("/members/{id}")
public MemberResponse getMember(@PathVariable Long id) {
Member member = memberService.findById(id);
return MemberResponse.builder()
.id(member.getId())
.name(member.getName())
.email(member.getEmail())
.joinDate(member.getJoinDate())
.build();
}
패턴 2: Entity (통제된 생성)
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA가 사용
@AllArgsConstructor(access = AccessLevel.PRIVATE) // Builder가 사용
@Builder
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
private String email;
private LocalDateTime joinDate;
// 정적 팩토리 메서드로 한 번 더 감싸기
public static Member createMember(String name, String email) {
return Member.builder()
.name(name)
.email(email)
.joinDate(LocalDateTime.now())
.build();
}
}
패턴 3: 일부 필드만 Builder 적용
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
private String email;
private LocalDateTime createdAt;
// 특정 생성자에만 @Builder 적용
@Builder
public Member(String name, String email) {
this.name = name;
this.email = email;
this.createdAt = LocalDateTime.now();
}
}
✅ 장점:
- id는 Builder에서 제외 (안전)
- createdAt은 자동 설정
- name, email만 Builder로 설정
패턴 4: @Builder.Default로 기본값 설정
@Builder
public class Order {
private Long memberId;
private Long productId;
@Builder.Default
private OrderStatus status = OrderStatus.PENDING;
@Builder.Default
private LocalDateTime orderedAt = LocalDateTime.now();
}
// 사용
Order order = Order.builder()
.memberId(1L)
.productId(2L)
// status와 orderedAt은 기본값 자동 설정!
.build();
🎨 실전 종합 예제
Case 1: 회원가입 요청 DTO
@Getter
@Builder
public class SignUpRequest {
private final String email;
private final String password;
private final String name;
// 선택 필드
@Builder.Default
private final String nickname = "익명";
private final String phone;
private final String address;
}
// Controller
@PostMapping("/signup")
public void signUp(@RequestBody SignUpRequest request) {
// 어떤 정보가 들어왔는지 명확!
}
Case 2: 복잡한 주문 도메인
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
@Id
@GeneratedValue
private Long id;
private Long memberId;
private Long productId;
private int quantity;
private int totalPrice;
private String deliveryAddress;
private String receiverName;
private String receiverPhone;
@Enumerated(EnumType.STRING)
private OrderStatus status;
private LocalDateTime orderedAt;
private LocalDateTime completedAt;
@Builder
public Order(Long memberId, Long productId, int quantity, int totalPrice,
String deliveryAddress, String receiverName, String receiverPhone) {
this.memberId = memberId;
this.productId = productId;
this.quantity = quantity;
this.totalPrice = totalPrice;
this.deliveryAddress = deliveryAddress;
this.receiverName = receiverName;
this.receiverPhone = receiverPhone;
this.status = OrderStatus.PENDING;
this.orderedAt = LocalDateTime.now();
}
// 비즈니스 로직
public void complete() {
this.status = OrderStatus.COMPLETED;
this.completedAt = LocalDateTime.now();
}
}
// Service에서 사용
Order order = Order.builder()
.memberId(member.getId())
.productId(product.getId())
.quantity(orderRequest.getQuantity())
.totalPrice(product.getPrice() * orderRequest.getQuantity())
.deliveryAddress(orderRequest.getAddress())
.receiverName(orderRequest.getName())
.receiverPhone(orderRequest.getPhone())
.build();
Case 3: 테스트 픽스처(Fixture)
// 테스트용 객체 생성 헬퍼
public class MemberFixture {
public static Member createDefaultMember() {
return Member.builder()
.name("테스트유저")
.email("test@test.com")
.build();
}
public static Member createMemberWithName(String name) {
return Member.builder()
.name(name)
.email("test@test.com")
.build();
}
public static Member createAdmin() {
return Member.builder()
.name("관리자")
.email("admin@test.com")
.role(MemberRole.ADMIN)
.build();
}
}
// 테스트에서 사용
@Test
void memberTest() {
Member member = MemberFixture.createDefaultMember();
// 테스트...
}
💡 실무 팁 모음
✅ DO - 이렇게 사용하세요
// 1. DTO는 거의 항상 Builder
@Builder
public class MemberResponse { }
// 2. 불변 객체는 Builder + final
@Builder
public class Money {
private final BigDecimal amount;
private final String currency;
}
// 3. 복잡한 객체 생성은 Builder
@Builder
public class ReportRequest {
private LocalDate startDate;
private LocalDate endDate;
private List<String> categories;
private ReportType type;
}
// 4. 테스트 픽스처는 Builder
Member testMember = Member.builder()
.name("test")
.build();
❌ DON’T - 이건 피하세요
// 1. 간단한 객체에 Builder 남발
@Builder // ❌
public class Point {
private int x;
private int y;
}
// 2. Entity에 무분별한 Builder
@Entity
@Builder // ❌ 통제 안 됨
public class Member { }
// 3. 모든 필드를 Builder로 노출
@Builder // ❌ id까지 Builder로?
public class Member {
private Long id; // 이건 DB가 생성해야 함
}
🎯 결정 플로우차트

🔥 핵심 정리
💬 한 줄 요약
@Builder는
“이 객체는 이렇게 만들어져야 한다”를 코드로 표현하는 도구
📌 기억 공식
| 상황 | 권장 방법 |
|---|---|
| 🎯 필드 많다 (4개 이상) | @Builder |
| 💎 불변 객체 만들기 | @Builder |
| 🧪 테스트 코드 | @Builder |
| 📦 DTO / VO | @Builder |
| 🗄️ JPA Entity | 통제해서 사용 |
| 📍 필드 2개 이하 | 생성자 |
🎨 최종 체크리스트
객체 생성 시 이것만 확인하세요:
- 파라미터가 4개 이상인가? → Builder
- 선택 값이 많은가? → Builder
- 불변 객체를 만드는가? → Builder
- 테스트 코드에서 쓰는가? → Builder
- JPA Entity인가? → 신중하게 (통제)
- 간단한 객체인가? → 생성자
🚀 실전 적용 순서
- DTO부터 시작 - 제일 안전하고 효과 확실
- 테스트 코드에 적용 - 가독성 향상 체감
- 복잡한 도메인 객체 - 점진적 확대
- Entity는 신중하게 - 통제된 방식으로