Lombok의 @Builder를 사용할 때 필드의 기본값 설정하기

ohzzi·2022년 8월 9일
0

Builder가 필드 기본값을 무시해버렸다

객체를 생성할 때 빌더 패턴을 사용하는 경우가 많습니다. 저는 도메인 객체, 그 중에서도 특히 JPA를 쓸 때 엔티티 객체를 만들 때 빌더 패턴을 애용하는데요, 하지만 빌더 패턴은 직접 구현하기에는 코드량이 상당합니다. 이럴 때 Lombok이 제공하는 @Builder 어노테이션을 활용하면 매우 편리합니다.

그런데 팀 프로젝트에서 @Builder 어노테이션을 쓰던 도중, 필드에 직접 설정하려는 기본값이 제대로 들어가지 않는 문제를 발견하게 되었습니다.

@Entity
@Table(name = "member")
@EntityListeners(AuditingEntityListener.class)
@Getter
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "github_id", nullable = false)
    private String gitHubId;

    @Column(name = "name")
    private String name;

    @Column(name = "image_url", length = 65535, nullable = false)
    private String imageUrl;

    @Column(name = "career_level")
    @Enumerated(EnumType.STRING)
    private CareerLevel careerLevel;

    @Column(name = "job_type")
    @Enumerated(EnumType.STRING)
    private JobType jobType;

    @BatchSize(size = 150)
    @OneToMany(mappedBy = "member")
    private List<InventoryProduct> inventoryProducts = new ArrayList<>();

    protected Member() {
    }

    @Builder
    private Member(final Long id, final String gitHubId, final String name, final String imageUrl,
                   final CareerLevel careerLevel, final JobType jobType,
                   final List<InventoryProduct> inventoryProducts) {
        this.id = id;
        this.gitHubId = gitHubId;
        this.name = name;
        this.imageUrl = imageUrl;
        this.careerLevel = careerLevel;
        this.jobType = jobType;
        this.inventoryProducts = inventoryProducts;
    }
    ...
}

위 코드는 저희 F12팀의 도메인 객체 중 Member의 코드입니다. MemberInventoryProduct의 List를 필드로 가지는데요, 이 때 List<InventoryProduct>는 참조 타입이므로 값을 할당해주지 않으면 초기값은 null입니다. 하지만 저희 팀은 아무런 InventoryProduct를 가지지 않는다고 해도 List 자체가 null이기보다는 빈 리스트를 가지는 것이 맞다고 생각해서 기본값을 빈 리스트로 설정해주려고 했습니다. 하지만 실제로 테스트를 해보니 다음의 테스트를 통과하지 않았습니다.

@Test
void Builder_테스트() {
    Member member = Member.builder()
            .build();

    assertThat(member.getInventoryProducts()).isInstanceOf(ArrayList.class);
}

어떻게 보면 당연한 부분인데요, 클래스 필드에 지정한 기본값이 빌더 클래스가 만들어질 때 그 빌더 클래스의 기본값으로 할당되지 않고 null로 초기화되기 때문입니다. 즉, 다음과 같은 상황이 되는 것이죠.

public class Member {

    ...
    private List<InventoryProduct> inventoryProducts = new ArrayList<>();
    ...
    
    public static class MemberBuilder {
    
        ...
        private List<InventoryProduct> inventoryProducts; // 기본값 null
        ...
        
        public MemberBuilder inventoryProducts(final List<InventoryProduct> inventoryProducts) {
            this.inventoryProducts = inventoryProducts;
            return this;
        }
        ...
    }
}

하지만 @Builder에 대해 제대로 생각해보지 않았던 저는 그 부분을 모르고 사용하고 있었습니다. 그렇다면 어떻게 기본값을 지정해줄 수 있을까요?

@Builder.Default

빌더를 사용할 때 필드의 기본 값을 지정해주고 싶다면 @Builder.Default 어노테이션을 사용해야 합니다. 이 때 @Builder.Default는 기본값을 지정한 필드 위에 붙여줍니다. 위 코드에서는

@Builder.Default
@BatchSize(size = 150)
@OneToMany(mappedBy = "member")
private List<InventoryProduct> inventoryProducts = new ArrayList<>();

로 사용하면 됩니다.

그런데 한가지 더 주의할 점이 있습니다. 위 도메인 코드를 다시 보시면, 저희 팀은 생성자에 @Builder 어노테이션을 붙였는데요, @Builder.Default를 사용하려면 @Builder를 클래스에 붙여줘야 합니다. 생성자에 빌더 어노테이션을 붙이고 @Builder.Default를 사용했더니 컴파일 시에 경고가 발생했습니다.

@Builder.Default requires @Builder or @SuperBuilder on the class for it to mean anything.

혹시나 해서 테스트 코드도 실행해봤지만, 통과하지 못했습니다. 결국 @Builder 어노테이션을 생성자에서 제거하고 클래스에 붙여주고 나서야 정상적으로 테스트를 통과시킬 수 있었습니다.

정리하자면 @Builder.Default는 빌더 패턴을 직접 구현할 때 빌더 클래스 내부의 필드 값으로 기본 값을 넣어주는 것과 동일한 효과를 내는 어노테이션이라고 볼 수 있습니다.

profile
배울 것이 많은 초보 개발자 입니다!

0개의 댓글