개발/!!!!!!!Warning!!!!!!!

Spring - @Builder, @Builder.Default, @NoArgsConstructor에 관하여...

잇(IT) 2023. 9. 11. 09:06
728x90

* 결과부터 얘기하자면

 

1. @Builder + @Builder.Default 사용

- 디컴파일 부분을 확인해보면, page, size의 필드가 초기화 되어 있지 않고, builder에 의한 default에 의한 값이 설정되어 있어 파라미터로 값이 넘어오지 않으면 null이 주입된다.

 

2. @Builder만 사용

- 디컴파일 부분을 확인해보면 @Builder.Default가 빠지게 되면, 기본 필드에 설정한 1, 10 값이 필드에 그대로 남아 있는 것을 확인 할 수 있지만, 생성자 주입에 의해 파라미터로 아무런 값이 넘어오지 않으면 null로 초기화 된다.

 

3. @Builder, @NoArgsConstructor, @AllArgsConstructor 사용

- 기본 생성자가 생성되고, 매개변수로 아무런 값이 넘어오지 않았을 때, getter로 기존 필드에 있는 값을 받아온다.

 

4. @Builder, 기본 생성자, 생성자(모든 값) 사용

- 컴파일에도 문제가 없으며 디컴파일의 결과 값도 @NoArgsConstructor, @AllArgsConstructor 동일하게 나온다.

- @AllArgsConstructor의 경우 안티패턴이기 때문에 사용하지 않는 것을 권장한다.

 

- Spring 페이징 관련 공부를 하던 중 이상한 부분이 발견되었다.

* 생성자와 Setter(수정자)가 함께 있다면 생성자 -> 수정자에 의해 데이터 주입이 일어난다.

 

- PostController.java

.....

@GetMapping("/posts")
    public List<PostResponse> getList(@ModelAttribute PostSearch postSearch) {
        return postService.getList(postSearch);
    }
    
    .....

 

- PostService.java

.....

public List<PostResponse> getList(PostSearch postSearch) {
        return postRepository.getList(postSearch).stream()
                .map(post -> new PostResponse(post))
                .collect(Collectors.toList());
    }
    
    .....

 

- PostRepositoryImpl.java

@RequiredArgsConstructor
public class PostRepositoryImpl implements PostRepositoryCustom {

    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public List<Post> getList(PostSearch postSearch) {
        return jpaQueryFactory.selectFrom(QPost.post)
                .limit(postSearch.getSize())
                .offset(postSearch.getOffset())
                .orderBy(QPost.post.id.desc())
                .fetch();
    }
}

 

- PostSearch.java

@Getter
@Setter
@Builder
//@Builder.Default를 사용하려면 class 단에서 @Builder를 사용해야 한다.
public class PostSearch {

    private static final int MAX_SIZE = 2000;

    @Builder.Default
    private Integer page = 1;

    @Builder.Default
    private Integer size = 10;

.....

 

- PostControllerTest.java

@Test
    @DisplayName("페이지 검색 Test")
    void pageTest() throws Exception {

        List<Post> posts = postRepository.saveAll(List.of(
                Post.builder()
                        .title("Title_1")
                        .content("Content_1")
                        .build(),
                Post.builder()
                        .title("Title_2")
                        .content("Content_2")
                        .build()
        ));

        String json = objectMapper.writeValueAsString(posts);

        //expected
        mockMvc.perform(MockMvcRequestBuilders.get("/posts")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print());
    }

 

- 위 코드와 테스트에 대해 간단하게 설명하자면,

1. url로 요청을 할 때, /posts 뒤에 페이지 관련 데이터를 넣지 않고, /posts로만 요청을 보내게 되면,

2. @ModelAttribute에 의해 PostSearch에 별도의 값이 들어가지 않고,

3. 때문에 getList(postSearch)에 의해 PostRepositoryImpl.java의 쿼리를 조회하는 코드의 postSearch.getSize() 부분에 PostSearch에 @Builder.Default 어노테이션에 의핸 해당 필드의 기본 값이 들어갈 것이라고 예상된다.

...
                .limit(postSearch.getSize())
                .offset(postSearch.getOffset())
                .orderBy(QPost.post.id.desc())
                .fetch();
    }

4. 하지만 테스트를 실행하게 되면, 위와 같이 NullPointerException이 발생하는 것을 확인할 수 있다.

 

- 디컴파일 코드를 통해 확인해보면

- @Builder 어노테이션과 함께, @Builder.Default 어노테이션을 필드에 달아놓고 코드를 실행하게 되면 위와 같이 기본 생성자에 의해 page, size로 넘어온 값이 필드에 저장되는 것을 볼 수 있다.

- 하지만 Test의 경우, 페이지에 관한 정보를 넘기지 않았기 때문에 위의 기본 생성자의 page, size에 들어가는 값은 둘 다 null이 될 것이고, 결과적으로 PostRepositoryImpl.java에 쿼리를 조회하기 위한 getter에 null이 넘어가고 NullPointException이 발생하게 된다.

 

* 즉, 처음 결과를 보여주는 화면과 같이 위의 상황을 해결하기 위해선 @Builder.Default를 없애주고, @AllArgsConstructor, @NoArgsConstructor를 붙여주면 된다.

* @AllArgsConstructor의 경우 안티 패턴이기 때문에 @AllArgsConstructor, @NoArgsConstructor의 어노테이션을 사용하지 않는 대신 클래스에 기본 생성자와, 생성자(모든 값)을 생성해주는 것도 하나의 방법이다.

 

728x90