객체를 생성할 때 빌더 패턴을 사용하는 경우가 많습니다. 저는 도메인 객체, 그 중에서도 특히 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
의 코드입니다. Member
는 InventoryProduct
의 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
@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
는 빌더 패턴을 직접 구현할 때 빌더 클래스 내부의 필드 값으로 기본 값을 넣어주는 것과 동일한 효과를 내는 어노테이션이라고 볼 수 있습니다.