티스토리 뷰

배경

request body 필드들을 유효성 체크를 하고 싶었습니다.

java spring boot 환경에서는 다르게 코프링에서는 몇 가지 문제가 발생해서 이번 포스팅에서 해당 문제들을 정리하려고 합니다.

개발 환경은 다음과 같습니다.

  • Spring Boot Version : 3.0.1
  • Java Version : 17
  • Kotlin Version : 1.8.21
  • Kotest Version : 5.5.5

추가로, 아래 의존성을 추가해주셔야 합니다.

// validator
implementation("org.springframework.boot:spring-boot-starter-validation")

1. NotBlank not working

첫 번째로는 jakarta.validation.constraints 라이브러리에서 제공하는 어노테이션들이 동작을 하지 않았습니다.

@Schema(name = "카페 생성 Request")
data class CreateCafeRequest(
    @NotBlank(message = "카페 이름은 필수값입니다.")
    @Schema(description = "카페 이름", defaultValue = "댕겸의 커피집")
    val name: String,
    @Schema(description = "카페 위도", defaultValue = "37.56667")
    val latitude: String?,
    @Schema(description = "카페 경도", defaultValue = "126.97806")
    val longitude: String?,
    @Schema(description = "카페 주소", defaultValue = "서울특별시 중구 세종대로 110 서울특별시청")
    val mainAddress: String?,
    @Schema(description = "카페 상세 주소", defaultValue = "1층")
    val detailAddress: String?,
    @Schema(description = "카페 오픈 시간", defaultValue = "09:00:00")
    val openedAt: LocalTime?,
    @Schema(description = "카페 클로즈 시간", defaultValue = "18:00:00")
    val closedAt: LocalTime?,
)

위 RequestDto에서 적어둔대로 "카페 이름은 필수값입니다." 라는 에러 메시지를 기대했지만, 다음과 같은 결과값을 받아볼 수 있었습니다.

여기서 두 가지 문제가 있습니다.

어떤 문제인지 살펴보기 전에 어떤 예외가 발생하는지 GlobalExceptionHandler에서 디버그를 통해 잡아봅시다.

디버그를 살펴보니 ServerWebInputException 예외가 발생했습니다.

예외가 발생한 원인에 대해서도 상세히 나와있는데, 원인은 다음과 같았습니다.

Json Decoding 과정에서 non-nullable 필드에 null이 들어와서 예외가 발생

제가 request body에 설정한 validation가 거치기도 전에 디코딩 과정에서 예외를 뱉어낸 것입니다.

name 필드를 nullable하게 변경하면 되겠다!

name 필드를 nullable하게 변경하면 간단하게 해결할 수 있었습니다.

하지만 저는 필수로 받아야 하는 non-nullable 필드를 validation을 하기 위해 nullable하게 변경하는 건 좋지 않은 변경이라고 생각했습니다.

ServerWebInputException 예외 핸들링

저는 name 필드를 nullable하게 변경하는 대신 ServerWebInputException 예외를 GlobalExceptionHandler에서 잡아서 처리하기로 결정했습니다.

@ExceptionHandler(ServerWebInputException::class)
protected fun handleServerWebInputException(ex: ServerWebInputException): ResponseEntity<ErrorResponse> {
    val message = "${ex.methodParameter.parameter.name}의 validation 규칙을 다시 검토하세요."
    return ResponseEntity(
        ErrorResponse(errorCode = "10001", message = message),
        HttpStatus.BAD_REQUEST
    )
}

ServerWebInputException 예외가 발생한 클래스에 위 코드처럼 validation 규칙을 다시 검토하라는 메시지를 에러 응답값에 포함했습니다.

2. NotEmpty not working

NotBlank 대신 null로 들어온 필드에 대한 에러 메시지를 처리해봤습니다.

그럼 빈 문자열 값("", " ")으로 들어올 때 처리하기 위해 NotEmpty 어노테이션을 사용해보겠습니다.

@Schema(name = "카페 생성 Request")
data class CreateCafeRequest(
    @NotEmpty(message = "카페 이름은 공백일 수 없습니다.")
    @Schema(description = "카페 이름", defaultValue = "댕겸의 커피집")
    val name: String,
    @Schema(description = "카페 위도", defaultValue = "37.56667")
    val latitude: String?,
    @Schema(description = "카페 경도", defaultValue = "126.97806")
    val longitude: String?,
    @Schema(description = "카페 주소", defaultValue = "서울특별시 중구 세종대로 110 서울특별시청")
    val mainAddress: String?,
    @Schema(description = "카페 상세 주소", defaultValue = "1층")
    val detailAddress: String?,
    @Schema(description = "카페 오픈 시간", defaultValue = "09:00:00")
    val openedAt: LocalTime?,
    @Schema(description = "카페 클로즈 시간", defaultValue = "18:00:00")
    val closedAt: LocalTime?,
)

제가 기대했던 바가 아닌 빈 문자열 필드를 담은 request body가 서비스 레이어까지 침투한 걸 보실 수 있습니다.

field: 키워드

다른 포스팅을 참고해보니, 위 data class에서 NotEmpty 같은 어노테이션을 사용하면 java code로 변환하는 과정에서 constructor의 파라미터에 붙는다고 합니다.

그렇기 때문에 field: 라는 키워드를 붙여줘야 합니다.

@Schema(name = "카페 생성 Request")
data class CreateCafeRequest(
    @field:NotEmpty(message = "카페 이름은 공백일 수 없습니다.")
    @Schema(description = "카페 이름", defaultValue = "댕겸의 커피집")
    val name: String,
    @Schema(description = "카페 위도", defaultValue = "37.56667")
    val latitude: String?,
    @Schema(description = "카페 경도", defaultValue = "126.97806")
    val longitude: String?,
    @Schema(description = "카페 주소", defaultValue = "서울특별시 중구 세종대로 110 서울특별시청")
    val mainAddress: String?,
    @Schema(description = "카페 상세 주소", defaultValue = "1층")
    val detailAddress: String?,
    @Schema(description = "카페 오픈 시간", defaultValue = "09:00:00")
    val openedAt: LocalTime?,
    @Schema(description = "카페 클로즈 시간", defaultValue = "18:00:00")
    val closedAt: LocalTime?,
)

 

하지만, 저의 바램과는 다르게 제가 설정했던 "카페 이름은 공백일 수 없습니다" 에러 메시지가 아닌 NotBlank를 위해 설정했던 에러 메시지가 응답값으로 넘어오고 있습니다.

그래서 ServerWebInputException 예외 핸들링에서 디버그를 해보기로 했습니다.

예외 body를 살펴보니, WebExchangeBindException 예외가 발생하고 있었고, WebExchangeBindException 필드인 bindingResult에서 제가 request dto에 설정했던 에러 메시지가 보관하고 있었습니다.

WebExchangeBindException 예외 핸들링

그렇다면 WebExchangeBindException 예외 핸들링하는 코드도 추가해봅시다.

@ExceptionHandler(WebExchangeBindException::class)
protected fun handleWebExchangeBindException(ex: WebExchangeBindException): ResponseEntity<ErrorResponse> {
    val message = ex.bindingResult
        .allErrors[0]
        .defaultMessage
    return ResponseEntity(
        ErrorResponse(errorCode = "10001", message = message),
        HttpStatus.BAD_REQUEST
    )
}

추가한 뒤, 다시 api를 호출해보니, 제가 설정한 에러 메시지가 응답값으로 넘어오는 걸 확인할 수 있었습니다.

결론

이번 포스팅에서는 코프링에서 validation 설정하는 방법에 대해 다뤄봤습니다.

저와 비슷한 문제를 겪으신 분들은 제 포스팅이 도움되셨으면 좋겠습니다.

감사합니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함