@Builder 사용의 여러가지 방법과 안전하게 사용하기
실행 환경: Java 17, Spring Boot 3.1.4
빌더 패턴을 사용하면 객체를 생성할 때 많은 이점을 얻을 수 있다. (링크 참조)
하지만 빌더 패턴을 사용하면 많은 코드를 작성해야 하는 단점이 존재하나 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로 생성자를 추가하여 사용할 수 있다.
@Getter
@Entity
@Builder
@Table(name = "order_info")
@NoArgsConstructor // @Entity는 빈 생성자(기본 생성자)가 필요
@AllArgsConstructor // @NoArgsConstructor가 있기 때문에 명시적으로 @AllArgsConstructor를 추가
public class OrderInfo extends BaseTime {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "product_id", nullable = false)
private Product product;
@Column(name = "quantity", nullable = false)
private Integer quantity;
@Column(name = "total_amount", nullable = false)
private BigDecimal totalAmount;
@Builder.Default
@Column(name = "status", nullable = false)
private String status = ORDER_CREATE_STATUS;
// ...필드 및 메서드 생략
// 주문 정보 검증
public void validateProductInfo(BigDecimal totalAmount, Integer quantity) {
// 검증 로직
}
}이 Entity 클래스를 생성하고 데이터베이스에 저장하기 위해 다음과 같이 사용할 수 있다. 주문 정보를 생성하고 검증 후 저장하는 간단한 서비스 코드이다.
서비스 로직을 살펴보면 크게 세 가지 단계로 나눌 수 있다.
주문 생성
주문 상품 정보 검증
주문 정보 저장
위 코드에서는 2번 단계를 성실하게 수행하여 결과적으로 데이터베이스에 검증이 완료 된 주문 정보만 저장할 수 있게 되었다.
하지만 2번 단계를 생략하여 저장하게 되면 올바르지 않은 주문 정보가 데이터베이스에 저장될 수 있다.(다른 메서드에서 생성하거나 기존 코드가 수정되는 등) 때문에 2번 단계를 서비스 코드에서 수행하는 것이 아닌 Entity 클래스에서 생성할 때 검증하는 것이 더 안전하다고 볼 수 있다.
생성 시 검증 로직 추가
@Builder 어노테이션을 사용하면서 생성 시 검증 로직을 추가하는 방법으로 두 가지를 생각해볼 수 있다.
build()메서드를 재작성생성자에서 검증 로직 수행
build() 메서드 재작성
build() 메서드 재작성@Builder 어노테이션을 사용하면 build() 메서드를 호출하여 인스턴스를 생성하게 되는데, 이 메서드를 재작성할 수 있다.
build() 메서드를 호출 할 때 필요한 매개변수를 추가하여 검증 로직을 수행할 수 있게 되었지만, 많은 단점이 생겼다.
@Builder.Default가 정상적으로 동작하지 않아 기본 값을 직접 할당해야 함해당 클래스의 생성자를 직접 호출하는 경우 검증 로직이 수행되지 않음
코드의 양이 많아지고 굉장히 복잡해짐
2번의 문제 같은 경우엔 @AllArgsConstructor(access = PROTECTED)를 사용하여 외부에서 생성자를 호출하지 못하도록 제한할 수 있지만,
1, 3번의 문제는 여전히 존재하고, 2번의 문제도 여전히 내부에서 생성자를 호출하는 경우에는 검증 로직이 수행되지 않는다.
생성자 직접 추가
위의 문제를 해결하기 위해 더 간단하고 안전한 방법은 생성자를 직접 추가하는 것이다.
이 방법을 사용하면 위애서 언급 된 문제를 해결할 수 있다.
@Builder.Default가 정상적으로 동작해당 클래스의 생성자를 직접 호출하는 경우에도 검증 로직이 수행
생성자 추가로 코드의 양은 비슷하지만,
build()메서드를 재작성하는 것보다는 코드가 간결해짐
이렇게 함으로써 객체가 어느 시점에 생성되든 검증 로직이 수행되도록 할 수 있게 되었다.
추가적으로 @NoArgsConstructor(access = AccessLevel.PROTECTED)를 사용하여 기본 생성자를 외부에서 호출하지 못하도록 제한하였다.
생성자에 @Builder 사용
@Builder 어노테이션은 생성자에도 사용할 수 있는데, 이 방식을 사용해 꼭 필요한 매개변수만 받을 수 있도록 할 수 있다.
이 방식을 사용하면 불필요한 매개변수까지 포함되는 것을 방지하는 것 뿐만 아니라, 어떤 필드가 생성 시 전달 받고 기본 값으로 할당되는지도 명확하게 알 수 있다.
결론
@Builder 어노테이션을 사용하는 경우 롬복을 사용하고 있기 때문에 무의식적으로 @AllArgsConstructor를 사용할 수 있다.
@AllArgsConstructor를 사용하면 모든 필드를 매개변수로 받는 생성자가 생성 될 뿐만 아니라 생성 시 검증 로직을 수행할 수 없게 된다.
때문에 생성자에 @Builder 어노테이션을 사용하여 꼭 필요한 파라미터만 받을 수 있도록 하고, 생성 시 검증 로직을 수행할 수 있도록 하는 것이 좋다.
Last updated
Was this helpful?