사부작사부작
[Spring] @Builder 사용시 주의사항 본문
Intro
@Builder 어노테이션이 편리하여 자주 사용했습니다.
어느날, 생성한 객체가 초기값이 아니라 Null 값이 나오면서 왜 Null 이 나온건지, 어떻게 해결할 수 있을지 고민했습니다.
비슷한 경험이 있으신 분들에게 아래 글이 도움이 되길 바랍니다.
틀린 내용이 있을 수 있습니다. 피드백 해주신다면 감사하겠습니다.
롬복의 @Builder 를 사용하면, 빌더패턴을 구현할 수 있고 간단하게 객체를 생성할 수 있다. 하지만 편리한 @Builder 어노테이션을 클래스 위에 사용할땐 주의할 점이 있다.
먼저 예시 코드는 블로그 클래스와 유저 클래스로, 유저가 여러개의 블로그를 쓸 수 있는 1:N 구조다.
<블로그 클래스>
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
@Data
@Entity
public class Blog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "title", length = 20, nullable = false)
private String title;
@Column(name = "text", length = 1000)
private String text;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
}
<유저 클래스>
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
@Data
@Entity
public class User {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "user")
List<Blog> blogList = new ArrayList<>();
}
User 클래스에는 유저가 작성한 블로그를 리스트로 가지는 필드가 있다. 객체를 생성하고 위 필드를 호출할 때, 문제가 발생했다. 아래의 테스트 코드를 보자.
@Test
void 초기화_테스트() {
User kim = User.builder()
.id(1L)
.name("kim")
.build();
List<Blog> blogList = kim.getBlogList();
System.out.println("blogList = " + blogList);
List<String> testList = new ArrayList<>();
System.out.println("testList = " + testList);
if (blogList.isEmpty()) {
System.out.println("abc");
}
}
위 테스트 코드를 돌린 결과다.
User 클래스 안의 blogList 와 테스트 코드 안의 testList 는 둘 다 초기화되있다. 하지만 진행한 테스트 코드 결과는 달랐다. blogList가 초기화가 됐다고 믿고 호출했지만 NPE(NullPointerException) 에러가 발생했다.
원인은 @Builder의 위치
엔티티 클래스 위에 @Builder 어노테이션을 걸어뒀다. 클래스에 붙은 어노테이션은 필드 전체에 적용된다. 즉, 아래 코드와 같게 된다.
@Builder
public User(Long id, String name, List<Blog> blogList) {
this.id = id;
this.name = name;
this.blogList = blogList;
}
하지만 테스트에서 작성한 user 빌더에는 blogList 값은 빠져있다. 빌더는 특정 필드에 값을 지정해주지 않으면 0,null,false 를 넣는다. 예를 들어 빌더에 name 값을 빼주면, 똑같이 null 이 나온다.
User kim = User.builder()
.id(1L)
.build();
System.out.println("kim.getName() =" + kim.getName());
빌더로 객체 생성시에 모든 필드값을 채우는 것이 불가능할 수 있다. 그렇다면 어떻게 해결할 수 있을까?
해결책
1. @Builder.default
공식문서를 보면 필드위에 @Builder.Default 어노테이션을 붙혀주라고 쓰여있다. 객체가 생성될때, 값이 지정되지 못한 필드에 대해서 개발자가 지정한 초기값을 지켜준다. 아래와 같이 초기값을 지켜줘야 할 필드위에 쓰면 된다.
@Builder.Default
@OneToMany(mappedBy = "user")
List<Blog> blogList = new ArrayList<>();
2. 생성자 위에 빌더 쓰기
객체가 생성되는 시점에 필요한 필드들만 사용해 생성자를 만들고 그 위에 @Builder 어노테이션을 붙혀준다. 위 방식으로 생성된 객체는 클래스에서 지정해 둔 초기값이 적용된다.
3. 정적 팩토리 메서드로 객체 생성
public static User create(Logn id, String name) {
return new User(id, name, new ArrayList<>());
}
정적 팩토리 메서드로 객체를 생성한다. 생성 시점에 필요한 인자값을 받고, 이를 활용해 객체를 만들 수 있다. 필드에 위와 같은 메소드를 구현한다. 테스트 코드에 아래와 같은 코드로 실제로 객체가 생성되는지 확인하자.
User mincheol = User.create(2L, "mincheol");
System.out.println("mincheol.getBlogList() = " + mincheol.getBlogList();
위 세 가지 방법으로 개발자가 지정해둔 초기값을 생성된 객체에 넣을 수 있다.
'스프링' 카테고리의 다른 글
@OneToOne 관계에서 Lazyloading 이슈는 왜 발생할까 (0) | 2023.06.22 |
---|---|
[Spring] 영속성 컨텍스트 이해하기 (0) | 2022.03.27 |