* 결과부터 얘기하자면
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의 어노테이션을 사용하지 않는 대신 클래스에 기본 생성자와, 생성자(모든 값)을 생성해주는 것도 하나의 방법이다.