Builder, From

오정빈·2025년 11월 3일

내일배움캠프

목록 보기
21/22

Builderm, From

2025.11.03

1) Builder란

1.1 문제 상황: 텔레스코핑 생성자(파라미터 폭탄)

// 나쁨: 어떤 값이 무엇인지, 순서를 외우기 어렵다
new Product("MacBook", "M3 Max", 3890000, 20, "노트북", null);
  • 가독성↓: 인자의 의미가 불명확
  • 안전성↓: 순서 실수 시 컴파일이 통과해도 런타임 버그
  • 옵셔널 필드가 많아질수록 생성자가 폭발

1.2 해결: Builder 패턴

  • 필드명을 키로 값을 채워 넣음 -> 가독성↑, 안전성↑
  • 옵셔널은 필드만 선택적으로 넣을 수 있음
  • 불변 DTO 조합에 적합하다
@Getter
@Builder
public class ProductDto {
    private final String name;
    private final String description;
    private final int price;
    private final int stockQuantity;
    private final String category;
    private final String imageUrl;
}

// 사용
ProductDto dto = ProductDto.builder()
    .name("MacBook Pro 17")
    .description("M3 Max 칩")
    .price(3_890_000)
    .stockQuantity(20)
    .category("노트북")
    .imageUrl("https://...")
    .build();

2) Lombok @Builder 핵심 옵션

2.1 사용법

  • DTO: 클래스 레벨 @Builder 권장 (불변 DTO 지향)
  • 엔티티(JPA):
    • @NoArgsConstructor(protected) + 빌더 생성자@Builder
      붙이기 권장
    • 무분별한 필드 세터보다 의미 있는 생성 경로를 제공
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "products")
public class Product extends BaseTimeEntity {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable=false) private String name;
    @Column(nullable=false) private String description;
    @Column(nullable=false) private int price;
    @Column(nullable=false) private int stockQuantity;
    @Column(nullable=false) private String category;
    private String imageUrl;

    @Builder // ← 생성자에 붙여 의도된 생성 흐름 강제
    private Product(String name, String description, int price, int stockQuantity,
                    String category, String imageUrl) {
        this.name = name;
        this.description = description;
        this.price = price;
        this.stockQuantity = stockQuantity;
        this.category = category;
        this.imageUrl = imageUrl;
    }
}

2.2 자주 쓰는 추가 기능

  • @Builder.Default : 빌더 미지정 시 기본값 유지

    @Builder
    public class ItemDto {
        @Builder.Default
        private String status = "ACTIVE"; // builder().build() 시에도 ACTIVE 유지
    }
  • @Singular : 컬렉션 필드를 addXxx() 식으로 누적

    @Singular
    private List<String> tags; // .tag("a").tag("b").build()
  • toBuilder=true : 기존 객체를 복제 후 일부만 변경

    ItemDto changed = original.toBuilder().status("PAUSED").build();

2.3 JSON·검증과의 궁합

  • DTO는 @Getter 필수 (JSON 직렬화용)
  • 요청 DTO는 @NotNull, @PositiveBean Validation으로 선제
    검증\
  • 엔티티를 직렬화 대상에 직접 쓰지 말고 DTO로 분리 (N+1, 순환참조 방지)

3) from()이란

3.1 정의

  • 정적 팩토리 메서드로, 보통 엔티티 → DTO 변환 로직을 한 곳에
    모아둠.
  • 서비스/컨트롤러에서 변환 코드를 흩뿌리지 않고 캡슐화.
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PostProductResponse {
    private Long id;
    private String name;
    private String description;
    private int price;
    private int stockQuantity;
    private String category;
    private String status;
    private String imageUrl;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    private String message;
    private String warning;

    public static PostProductResponse from(Product p, String message, String warning) {
        return PostProductResponse.builder()
            .id(p.getId())
            .name(p.getName())
            .description(p.getDescription())
            .price(p.getPrice())
            .stockQuantity(p.getStockQuantity())
            .category(p.getCategory())
            .status(p.getStockQuantity() > 0 ? "AVAILABLE" : "OUT_OF_STOCK")
            .imageUrl(p.getImageUrl())
            .createdAt(p.getCreatedAt())
            .updatedAt(p.getUpdatedAt())
            .message(message)
            .warning(warning)
            .build();
    }
}

3.2 DTO에서 사용하는 이유

  • 중복 제거: 변환 규칙을 한 곳에서 관리
  • 변경 용이: 필드 추가/명 변경 시 from()만 수정하면 전역 반영
  • 테스트 용이: 변환 규칙 단위 테스트 가능

4) 설계 방법

  1. DTO는 불변 + @Builder + @Getter
    • 생성 이후 변경 불가 → 사이드이펙트 최소화
  2. 엔티티 생성 경로 통일
    • @Builder 생성자만 허용(세터 지양)
    • 의미 있는 필수값만 인자로 받게 설계
  3. from()로 변환 로직 캡슐화
    • 서비스/컨트롤러에서 변환 코드 제거, 테스트 쉬움
  4. 검증은 요청 DTO에서
    • @Valid + Bean Validation으로 최대한 앞단에서 걸러내기
  5. HTTP 응답 코드 일관성
    • 중복/권한/검증 실패/리소스 없음 등 표준 상태코드 매핑
  6. DB 제약으로 이중 보호
    • @Table(uniqueConstraints=...) 또는 컬럼 unique = true
    • 동시성 상황을 DB가 보조

5) 한 줄 요약

  • Builder: "필드명 기반"으로 안전하고 읽기 쉬운 객체 생성
    만든다.
  • from(): 엔티티 -> DTO 변환 규칙의 단일 진실(SSOT)
    제공한다.
  • 둘을 함께 쓰면 가독성/유지보수성/테스트 용이성이 크게 향상된다.

0개의 댓글