@Valid, @Min

허준기·2025년 2월 16일
1

스프링

목록 보기
8/10

초록스터디 프로젝트와 관련한 개발을 지속적으로 하고 있다
그러던 중 검증의 책임과 관련해서 고민을 하던 중 글을 쓰게 됐다.

DoPlan을 불러올 때 모든 데이터들을 가져오면 오래 걸릴 수 있어서 페이징을 적용해줬다.
적용을 해주고 나니 페이지가 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 내의 로직을 통한 검증

첫 번째 방법에서 서비스 로직에서의 검증이 아닌 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

마지막으로 택한 방법이 @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 이상이어야 합니다.]] ]

그렇게 찍히는 로그를 보면 이렇게 찍히는 것을 볼 수 있다

profile
나는 허준기

0개의 댓글