초록스터디 프로젝트와 관련한 개발을 지속적으로 하고 있다
그러던 중 검증의 책임과 관련해서 고민을 하던 중 글을 쓰게 됐다.
Do
나 Plan
을 불러올 때 모든 데이터들을 가져오면 오래 걸릴 수 있어서 페이징을 적용해줬다.
적용을 해주고 나니 페이지가 0부터 시작하는게 어색해서 해당 요청에서 0
에 대한 검증을 위해서 조치를 취해야 했다
첫 번째로 내가 사용한 방법은 서비스 로직에서 검증하는 방식이었다.
Controller
@GetMapping("/search")
public ResponseEntity<Page<PlanResponse>> searchPlan(
@RequestParam String keyword,
@RequestParam PlanSearchFilter filter,
PageDTO pageDTO
) {
Page<PlanResponse> responses = planService.searchPlan(keyword, filter, pageDTO);
return ResponseEntity.ok(responses);
}
Service
public Page<PlanResponse> searchPlan(String keyword, PlanSearchFilter filter, PageDTO pageDTO) {
if (pageDTO.page() < 1) {
throw new IllegalArgumentException("요청하는 페이지는 1 이상이어야 합니다!");
}
Pageable pageable = PageRequest.of(pageDTO.page() - 1, pageDTO.pageSize(),
Sort.by(Sort.Direction.DESC, "id"));
Page<Plan> searchedPlans = switch (filter) {
case TITLE -> planRepository.findAllByTitleContains(keyword, pageable);
case DESCRIPTION -> planRepository.findAllByDescriptionContains(keyword, pageable);
case TITLE_AND_DESCRIPTION ->
planRepository.findAllByTitleContainsOrDescriptionContains(keyword, keyword, pageable);
};
return searchedPlans.map(PlanResponse::from);
}
이게 첫번째 방법에 대한 코드다
서비스 로직에서 처음 코드를 살펴보자
if(paeDTO.page() < 1) {
throw new IllegalArgumentException("요청하는 페이지는 1 이상이어야 합니다!");
}
이런식으로 로직 안에서 page
의 값에 대한 검증을 하고 있다. 처음 만들때는 이렇게 해서 메인에 머지까지 했는데 나중에 리팩토링을 진행할 때 코드를 다시 보니 객체의 값에 대한 검증을 서비스 로직에서 한다는 부분이 어색하게 다가왔고 방식을 바꿔보기로 했다.
첫 번째 방법에서 서비스 로직에서의 검증이 아닌 DTO
내에서 검증을 해야겠다는 생각이 들었다. 결국 검증의 책임은 해당 값을 가지고 있는 PageDTO
의 몫인 것 같다
아래의 코드는 두 번째 방법에 대한 코드이다
public record PageDTO(
@Schema(description = "페이지 번호 (1부터 시작)", example = "1")
int page,
@Schema(description = "페이지에 들어있는 데이터 수", example = "10")
int pageSize
) {
public PageDTO {
if (page < 1) {
throw new IllegalArgumentException("요청하는 페이지는 1 이상이어야 합니다!");
}
if (pageSize < 1) {
throw new IllegalArgumentException("페이지 크기는 1 이상이어야 합니다!");
}
}
}
위의 코드를 통해서 PageDTO
객체를 생성할 때, 생성자에서의 검증을 통해 원하지 않는 값이 들어오면 예외를 던지는 방법을 생각해서 그렇게 구현을 해봤다.
하지만 이 방법을 썼을때 문제가 있었다!!
바로 이렇게 객체 생성과정에서 오류가 발생해서 이 객체를 사용하는 메서드까지 도달도 못하고 로그를 찍기도 전에 예외를 던진다는 점이었다. 물론 객체 생성 실패와 관련한 로그를 찍긴 하지만 어떤 요청에서 에러가 발생하는 지 까지는 볼 수 없었다
그래서 다음 방법을 생각해봤다.
마지막으로 택한 방법이 @Valid
와 @Min
어노테이션을 사용하여 검증하는 방식이다.
Controller
@GetMapping("/search")
public ResponseEntity<Page<PlanResponse>> searchPlan(
@RequestParam String keyword,
@RequestParam PlanSearchFilter filter,
@Valid PageDTO pageDTO
) {
Page<PlanResponse> responses = planService.searchPlan(keyword, filter, pageDTO);
return ResponseEntity.ok(responses);
}
PageDTO
public record PageDTO(
@Schema(description = "페이지 번호 (1부터 시작)", example = "1")
@Min(value = 1, message = "페이지 번호는 1 이상이어야 합니다.")
int page,
@Schema(description = "페이지에 들어있는 데이터 수", example = "10")
@Min(value = 1, message = "페이지 크기는 1 이상이어야 합니다.")
int pageSize
) {
}
첫 번째 방법의 코드를 보면 PageDTO
부분에 아무것도 없지만 지금은 @Valid
가 있는 것을 볼 수 있다.
그리고 PageDTO
안을 보면 @Min(value = 1, message = " 페이지 번호는 1 이상이어야 합니다.
와 같은 조건을 나타내는 것을 볼 수 있다.
이렇게 변경을 하고 실행을 시켜 에러 메세지를 한 번 살펴보자.
두 번째 방법과 다르게 500번대 에러가 아닌 400번대 에러가 뜨는 것을 볼 수 있다
그 이유는 @Valid
를 통한 유효성 검사를 수행했을 때, 만족하지 못하는 결과가 나온다면 MethodArgumentNotValidException
을 일으키고 400 Bad Request
를 반환하게 되는 것 같다
2025-02-16 15:26:03 [http-nio-8080-exec-1] WARN o.s.w.s.m.s.DefaultHandlerExceptionResolver
- Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [2] in public
org.springframework.http.ResponseEntity<org.springframework.data.domain.Page<com.example.braveCoward.dto.plan.PlanResponse>>
com.example.braveCoward.controller.PlanController.searchPlan(java.lang.String,com.example.braveCoward.util.enums.plan.PlanSearchFilter,com.example.braveCoward.dto.PageDTO):
[Field error in object 'pageDTO' on field 'page': rejected value [0]; codes [Min.pageDTO.page,Min.page,Min.int,Min]; arguments
[org.springframework.context.support.DefaultMessageSourceResolvable: codes [pageDTO.page,page]; arguments []; default message [page],1]; default message [페이지 번호는 1 이상이어야 합니다.]] ]
그렇게 찍히는 로그를 보면 이렇게 찍히는 것을 볼 수 있다