Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Archives
Today
Total
관리 메뉴

사부작사부작

[Spring] @Builder 사용시 주의사항 본문

스프링

[Spring] @Builder 사용시 주의사항

민철킴 2022. 4. 10. 19:52

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();

 

위 세 가지 방법으로 개발자가 지정해둔 초기값을 생성된 객체에 넣을 수 있다. 

 

 


https://projectlombok.org/features/Builder