시행착오 과정이 궁금하지 않은 분들은 아래로 이동하세요
현재 검색 관련 쿼리파라미터를 다음과 같이 받고 있다.
@GetMapping("/worldcups")
public ResponseEntity getWorldCups(@RequestParam(required = false, defaultValue = "1") int page,
@RequestParam(required = false, defaultValue = "5") int size,
@RequestParam(required = false, defaultValue = "playCount") String sort,
@RequestParam(required = false, defaultValue = "DESC") Sort.Direction direction,
@RequestParam(required = false) String keyword) {
PageRequest pageRequest = PageRequest.of(page - 1, size, direction, sort);
Page<WorldCupSimpleResponseDto> responseDtos = worldCupService.searchWorldCups(null, keyword, pageRequest);
return ResponseEntity.ok(new PageResponseDto(responseDtos));
}
사용상에 문제는 없으나 많은 파라미터로 인해 코드가 지저분해 보일 수 있고,
새로운 검색관련 기능을 만들 때마다 모두 입력해줘야 하는 번거로움이 있다.
해당 패턴이 여러번 반복되어 사용된다.
그래서 해당 파라미터들을 dto를 이용하여 간편하게 사용해볼까 한다.
Dto 생성
@Getter
@Builder
public class SearchRequestParamDto {
@Builder.Default
private int page = 1;
@Builder.Default
private int size = 5;
@Builder.Default
private String sort = "playCount";
@Builder.Default
private Sort.Direction direction = Sort.Direction.DESC;
private String keyword;
}
테스트 - 모든 값이 있는 경우
해당 값이 잘들어오는지 확인하기 위해 간단하게 컨트롤러를 만들었다.
@ModelAttribute 어노테이션은 특별히 옵션을 사용할 것이 아니라면 생략해도 관계없으나 가독성과 혼란을 방지하기 위해 사용해 주는 것이 좋다고 한다.
// 테스트용 컨트롤러
@GetMapping("/test")
public ResponseEntity test(@ModelAttribute SearchRequestParamDto paramDto) {
return ResponseEntity.ok(paramDto);
}
@Test
@DisplayName("모든 값이 있는 경우")
void searchRequestParamTest1() throws Exception {
// given
int page = 3;
int size = 10;
String sort = "createdAt";
Sort.Direction direction = Sort.Direction.ASC;
String keyword = "테스트";
// when
ResultActions actions = mockMvc.perform(
get("/test")
.param("page", String.valueOf(page))
.param("size", String.valueOf(size))
.param("sort", sort)
.param("direction", String.valueOf(direction))
.param("keyword", keyword)
);
// then
actions
.andExpect(jsonPath("$.page").value(page))
.andExpect(jsonPath("$.size").value(size))
.andExpect(jsonPath("$.sort").value(sort))
.andExpect(jsonPath("$.direction").value(String.valueOf(direction)))
.andExpect(jsonPath("$.keyword").value(keyword));
}
테스트를 실행하면 정상적으로 통과하는 모습을 볼 수 있다.
테스트 - 기본값 적용 테스트
@Test
@DisplayName("기본값 적용 테스트")
void searchRequestParamTest2() throws Exception {
// given
Sort.Direction direction = Sort.Direction.ASC;
String keyword = "테스트";
// when
ResultActions actions = mockMvc.perform(
get("/test")
// .param("page", String.valueOf(page))
// .param("size", String.valueOf(size))
// .param("sort", sort)
.param("direction", String.valueOf(direction))
.param("keyword", keyword)
);
// then
actions
.andExpect(jsonPath("$.page").value(1))
.andExpect(jsonPath("$.size").value(5))
.andExpect(jsonPath("$.sort").value("playCount"))
.andExpect(jsonPath("$.direction").value(String.valueOf(direction)))
.andExpect(jsonPath("$.keyword").value(keyword));
}
page와 size가 null이기 때문에 int형태로 변환할 수 없다고 한다.
그럼과 동시에 "$.page" 값을 찾을 수 없다고 한다.
andExpect 부분을 하나씩 지워봐도 그 어떤 값도 찾을 수 없다고 한다.
즉, 빌더가 정상적으로 작동하지 않는다는 뜻이다. 혹시나 잘못했나 싶어서 빌더패턴을 사용 시 필드 변수가 정상적으로 초기화가 되는지 확인해 보았다.
SearchRequestParamDto param = SearchRequestParamDto.builder().build();
System.out.println(param);
// SearchRequestParamDto(page=1, size=5, sort=playCount, direction=DESC, keyword=null)
정상적으로 할당한 값으로 초기화되고 있다.
그렇다면 왜 안되는 것일까?
우선 int에서 Integer로 타입을 변경해 보았다.
@Builder.Default
private Integer page = 1;
@Builder.Default
private Integer size = 5;
그런 다음 다시 테스트를 진행해 보았다.
이번엔 값이 null이라고 한다.
즉, @Builder.Default가 작동하지 않는다는 뜻은 빌더패턴을 사용해서 객체를 생성하고 있지 않다는 것이다.
클래스 단위에 @Builder 애너테이션을 단독으로 사용할 경우 자동으로 모든 필드변수가 포함된 생성자가 생기게 된다.
음 ...??
뭔가 느낌이 JavaBeans 느낌이 든다...
가만 생각해 보니 Spring Framework의 많은 기능들이 JavaBeans 규약을 따른다는 것을 잊고 있었다...
그래서 공식문서를 살펴보았다.
그냥 처음부터 공식문서를 봤으면 삽질을 하지 않았을 텐데...
이래서 항상 설명을 잘 읽어봐야 한다.
어쨌든 사용방법을 알았으니 Dto클래스를 수정하여 다시 테스트해보자
@Getter
@Setter
@NoArgsConstructor
public class SearchRequestParamDto {
private int page = 1;
private int size = 5;
private String sort = "playCount";
private Sort.Direction direction = Sort.Direction.DESC;
private String keyword;
}
아주 잘된다.
다만, 자바빈즈패턴을 사용하면 @Setter 메서드를 사용해야 하므로 일관성과 불변성이 깨지게 된다.
스프링이 해당 객체를 생성할 때 사용하는 방법을 보면,
기본 생성자 + Setter를 최우선으로 사용하고 없을 경우 다른 생성자를 사용하는데,
기본 생성자가 없는 상태에서 생성자가 두 개이상 존재할 경우 에러가 발생한다.
정리하자면
- 강제적으로 모든 필드변수가 존재하는 생성자를 사용할 수밖에 없다.
- 생성자에는 원시타입을 사용해서는 안된다.
- 기본값이 정해져 있어야 한다.
물론, 클라이언트 개발자분께 모든 값을 필수로 넣어주셔야 합니다^^ 라고 하면 해결될 문제이긴 한데, 너무 무책임하다.
해당 api를 사용 중인 클라이언트 코드에서 변경이 일어나는 것을 원치 않는다.
그래서 아래와 같은 방식으로 수정해 보았다.
개선
@Getter
public class SearchRequestParamDto {
private int page;
private int size;
private String sort;
private Sort.Direction direction;
private String keyword;
public SearchRequestParamDto(Integer page,
Integer size,
String sort,
Sort.Direction direction,
String keyword) {
this.page = page == null ? 0 : page;
this.size = size == null ? 5 : size;
this.sort = sort == null ? "playCount" : sort;
this.direction = direction == null ? Sort.Direction.DESC : direction;
this.keyword = keyword;
}
}
테스트도 통과한다.
물론, 해당 방법도 여러 문제가 존재하지만, 일관성과 불변성을 깨는 것보다는 좋다고 생각한다.
결론
- 쿼리파라미터가 dto에 바인딩될 때는 오로지 "생성자만" 사용한다
- 기본 생성자 + Setter >>> 그 외 생성자
- 기본 생성자를 제외한, 다른 생성자가 두 개 이상 존재할 경우 에러 발생
- 자바빈즈 패턴이 좋다 -> 기본 생성자 + Setter 사용
- 일관성 불변성이 더 우선이다 -> 적절한 생성자 사용
피드백은 언제나 환영입니다!!!
스프링 공식문서
'개발일지 > 돌픽' 카테고리의 다른 글
Docker + Github Actions로 SpringBoot CI/CD 구축하기 (0) | 2023.06.08 |
---|---|
Docker로 spring-boot EC2에 배포하기 (0) | 2023.06.07 |
spring-boot Cache-Control 설정으로 부하 줄이기 (0) | 2023.05.19 |
ImgBB API 이용해보기 (0) | 2023.05.17 |
Spring-boot 파일 업로드 - 공식문서 따라하기 ! (0) | 2023.05.12 |