빌더 패턴을 사용하면 객체를 생성할 때 많은 이점을 얻을 수 있다. (링크 참조)
하지만 빌더 패턴을 사용하면 많은 코드를 작성해야 하는 단점이 존재하나 Lombok의 @Builder 어노테이션 을 사용하면 이러한 단점을 보완할 수 있다.(성능 저하라는 단점도 있으나 미미한 편)
@Builder + @AllArgsConstructor
Builder를 사용하면 모든 필드를 매개변수로 받는 생성자를 필요로 한다.
** @Builder 어노테이션 내부 설명
* If a member is annotated, it must be either a constructor or a method. If a class is annotated,
* then a package-private constructor is generated with all fields as arguments
* (as if {@code @AllArgsConstructor(access = AccessLevel.PACKAGE)} is present
* on the class), and it is as if this constructor has been annotated with {@code @Builder} instead.
* Note that this constructor is only generated if you haven't written any constructors and also haven't
* added any explicit {@code @XArgsConstructor} annotations. In those cases, lombok will assume an all-args
* constructor is present and generate code that uses it; this means you'd get a compiler error if this
* constructor is not present.
생성자가 없는 경우엔 @Builder 어노테이션에서 자동 생성해주지만, 다른 생성자가 있는 경우엔 @AllArgsConstructor로 생성자를 추가하여 사용할 수 있다.
위 코드에서는 2번 단계를 성실하게 수행하여 결과적으로 데이터베이스에 검증이 완료 된 주문 정보만 저장할 수 있게 되었다.
하지만 2번 단계를 생략하여 저장하게 되면 올바르지 않은 주문 정보가 데이터베이스에 저장될 수 있다.(다른 메서드에서 생성하거나 기존 코드가 수정되는 등)
때문에 2번 단계를 서비스 코드에서 수행하는 것이 아닌 Entity 클래스에서 생성할 때 검증하는 것이 더 안전하다고 볼 수 있다.
생성 시 검증 로직 추가
@Builder 어노테이션을 사용하면서 생성 시 검증 로직을 추가하는 방법으로 두 가지를 생각해볼 수 있다.
build() 메서드를 재작성
생성자에서 검증 로직 수행
build() 메서드 재작성
@Builder 어노테이션을 사용하면 build() 메서드를 호출하여 인스턴스를 생성하게 되는데, 이 메서드를 재작성할 수 있다.
// entity.java@Getter@Entity@Builder@Table(name ="order_info")@NoArgsConstructor@AllArgsConstructorpublicclassOrderInfoextendsBaseTime {// @Builder.Default가 정상적으로 동작하지 않은 것을 제외하고 위의 코드와 동일publicstaticclassOrderInfoBuilder {privatevoidvalidateProductInfo(BigDecimal totalAmount,Integer quantity) {// 검증 로직 }// build() 메서드 재작성publicOrderInfobuild(BigDecimal totalAmount,Integer quantity) {this.validateProductInfo(totalAmount, quantity);returnnewOrderInfo(this.id,this.user,this.product,this.orderId,this.paymentKey,this.orderName,this.method,this.quantity,this.totalAmount,this.status,this.requestedAt,this.approvedAt,this.lastTransactionKey ); } }}// dto.java@Getter@RequiredArgsConstructorpublicclassOrderCreateRequest {privatestaticfinalString ORDER_CREATE_STATUS ="READY";privatefinalLong userId;privatefinalString orderId;privatefinalBigDecimal amount;privatefinalOrderProduct orderProduct;publicOrderInfotoEntity(User user,Product product) {returnOrderInfo.builder().user(user).product(product).orderId(this.orderId).quantity(this.orderProduct.getQuantity()).totalAmount(this.amount).status(ORDER_CREATE_STATUS) // @Build.Default 동작하지 않아 직접 추가.build(amount,this.orderProduct.getQuantity()); // build() 호출 시 필요한 매개변수 추가 }}
build() 메서드를 호출 할 때 필요한 매개변수를 추가하여 검증 로직을 수행할 수 있게 되었지만, 많은 단점이 생겼다.
@Builder.Default가 정상적으로 동작하지 않아 기본 값을 직접 할당해야 함
해당 클래스의 생성자를 직접 호출하는 경우 검증 로직이 수행되지 않음
코드의 양이 많아지고 굉장히 복잡해짐
2번의 문제 같은 경우엔 @AllArgsConstructor(access = PROTECTED)를 사용하여 외부에서 생성자를 호출하지 못하도록 제한할 수 있지만,
1, 3번의 문제는 여전히 존재하고, 2번의 문제도 여전히 내부에서 생성자를 호출하는 경우에는 검증 로직이 수행되지 않는다.
이 방식을 사용하면 불필요한 매개변수까지 포함되는 것을 방지하는 것 뿐만 아니라, 어떤 필드가 생성 시 전달 받고 기본 값으로 할당되는지도 명확하게 알 수 있다.
결론
@Builder 어노테이션을 사용하는 경우 롬복을 사용하고 있기 때문에 무의식적으로 @AllArgsConstructor를 사용할 수 있다.
@AllArgsConstructor를 사용하면 모든 필드를 매개변수로 받는 생성자가 생성 될 뿐만 아니라 생성 시 검증 로직을 수행할 수 없게 된다.
때문에 생성자에 @Builder 어노테이션을 사용하여 꼭 필요한 파라미터만 받을 수 있도록 하고, 생성 시 검증 로직을 수행할 수 있도록 하는 것이 좋다.