들어가며

안녕하세요 ㅎㅎ 다들 spring 기반의 application 을 운영하게 되는 일들이 많은데요.
오늘은 client 에서 요청하는 값들에 대해 검증하고 싶을 때 어떻게 검증하는지와 내부 동작과정에 대해 한번 알아보겠습니다
client 에서 요청하는 데이터들은 정말 다양하고 위변조 되는 데이터들이 많을 수 밖에 없어
validation 을 하는 것은 선택이 아닌 필수 라는 것을 항상 인지하셨으면 합니다.
글에서 설명드릴 spring 버젼은 spring boot 3.x 이후, spring 6.x 버젼 이후의 방식임을 인지하셨으면 합니다.
버젼간의 디테일한 동작 방식은 차이가 있을 수 있습니다.
Spring boot validation
일반적으로는 spring boot 에서 관련된 의존성들을 묶어서 관리하기 때문에 spring boot validation 모듈을 가져와봅시다
implementation("org.springframework.boot:spring-boot-starter-validation")
그렇게 하면 아래와 같이 jakarta.validation 라이브러리를 가져오게 되는 것이죠
다양한 검증 어노테이션을 제공하니 한번 바로 적용해보도록 해봅시다.
검증하는 로직 작성해보기
request body 를 받는 API 와 path variable 을 받는 API 2개를 한번 설계해서 테스트로 한번 API 를 호출해봅시다
@RestController
@RequestMapping("/api/v1/valid")
class ValidController {
@PostMapping("/request")
fun requestBody(
@RequestBody @Valid requestDto: RequestDto
): ResponseDto {
return ResponseDto(
value = requestDto.value!!
)
}
@GetMapping("/path/{id}")
fun path(
@PathVariable @Positive id: Long,
): ResponseDto {
return ResponseDto(
value = id.toString()
)
}
}
data class RequestDto(
@get:NotBlank
val value: String?
)
data class ResponseDto(
val value: String,
)
request body 의 value 필드는 항상 NotBlank
path variable 의 값은 항상 Positive 라는 제약으로 두었습니다.
위와 같이 API 를 2개 설계하고 아래와 같이 한번 호출해보도록 해볼게요.
둘다 올바르지 않은 값을 호출하였을 때 어떦식으로 response 가 오게 되는걸까요?
위와 같이 Http status 400 과 함께 응답이 내려오는 것을 볼 수 있습니다.

어떻게 동작하고 있는 걸까요? 한번 근본적으로 탐구하기 위해 예외 핸들러를 하나 만들어놓고 체크해봅시다.
어떤 예외가 발생하는 걸까?
request body 에 대한 검증 실패와 path variable 에 대한 검증 실패는 같은 예외를 가지게 될까요?
한번 범용적인 예외 핸들러를 만들어 놓고 예외 클래스를 한번 살펴봅시다
@RestControllerAdvice
class GlobalExceptionController {
private val logger = LoggerFactory.getLogger(javaClass)
@ExceptionHandler(Throwable::class)
fun handleException(e: Throwable): ErrorResponse {
logger.error("Exception occurred", e)
return ErrorCommonResponse(HttpStatus.INTERNAL_SERVER_ERROR, e.message)
}
}
위와 같이 RestControllerAdvice 를 만들어놓고 테스트를 다시 한번 해봅시다
Exception 에 대해 debug 를 해보면 크게 2가지 예외가 구분되는 걸로 볼 수 있겠네요
- MethodArgumentNotValidException: request body 검증시 발생하는 예외
- HandlerMethodValidationException: path variable 검증시 발생하는 예외
위 2가지 예외클래스가 구분되어 발생하고 있었네요.!
같은 response 라서 같은 예외클래스인줄 알았지만 사실은 서로 다른 예외 클래스 였군요.
각 예외클래스가 무엇이고, 어떤 상황에서 발생하는지 한번 알아볼까요?
MethodArgumentNotValidException
먼저 우리는 클래스 주석과 사용처에 대해 알아볼 필요가 있습니다.
/**
* Exception to be thrown when validation on an argument annotated with {@code @Valid} fails.
* Extends {@link BindException} as of 5.3.
*
* @since 3.1
*/
public class MethodArgumentNotValidException extends BindException implements ErrorResponse
주석 설명에서도 볼 수 있듯이 @Valid 어노테이션 기반으로 선언된 곳에서 예외를 발생시킨다고 되어 있습니다
어디에서 발생시키는지 한번 알아봅시다
binder 를 생성하는 로직에 의해 result 를 기반으로 Error 가 발생된게 있다면 예외를 발생시키는 것을 알 수 있네요
로직을 파헤쳐가다가보면 BinderFactory 에 invoke 하는 로직을 찾게됩니다.
이 로직을 조금 더 들어가면.! 드디어 검증하는 로직을 찾게됩니다.
methodValidator 에 의해 검증을 진행하게되는 것을 볼 수 있네요. 내부로직은 BeanValidation 에 의해 진행되어 결과값을 가지게 된다고 이해하면 되겠습니다.
따라서 Request body 에 대한 검증은 @Valid 어노테이션 기반으로 동작하게 되고, @Valid 기반의 객체에서 검증을 하고, 예외를
MethodArgumentNotValidException 을 던지는 것으로 이해해볼 수 있겠네요 ㅎㅎ
HandlerMethodValidationException
그러면 반대로 path variable 검증시에는 어떤 예외 클래스이고, 어떤 상황에서 발생하는지 알아봅시다
/**
* {@link ResponseStatusException} that is also {@link MethodValidationResult}.
* Raised by {@link HandlerMethodValidator} in case of method validation errors
* on a web controller method.
*
* <p>The {@link #getStatusCode()} is 400 for input validation errors, and 500
* for validation errors on a return value.
*
* @since 6.1
*/
public class HandlerMethodValidationException extends ResponseStatusException implements MethodValidationResult
클래스 설명에서는 Web Controller Method 에서 method validation 이 발생할 때 예외를 던지는 것으로 볼 수 있네요
한번 어떤 상황에서 예외를 발생시키는지 찾아봅시다
HandlerMethodValidator 에 의해서 argument 를 1개씩 순회하면서 검증하는 것을 볼 수 있네요.
위 HandlerMethodValidator 는 MethodValidationInterceptor 에 의해 호출되는데요.
이 MethodValidationInterceptor 는 어떻게 ControllerMethod 쪽에 지정된 것일까요?
정답은 바로 RequestMappingHandlerAdapter 쪽에 답이 있습니다.
BEAN_VALIDATION_PRESENT 의 판단기준만 알면 되는데요!
바로 HandlerMethod 의 Class loader 에서 jakarta.validation.Validator 가 있으면 유효성 검사를 실시하도록 되어 있습니다.
즉 우리는 처음에 validator 의존성을 가져왔기 때문에 동작한다는 것을 알 수 있죠.

이렇게 모든 의문이 다 해결되었습니다.
라이브러리에서 어떻게 validation 을 지원하고 있는지, 어떤 식으로 자동으로 validation 을 하는 것인지 알수 있게 되었네요
요약하며
spring 에서는 사용자 요청 검증을 위한 validation 을 제공하고 있고, 그 유형에 따라서 각기 다른 예외를 던지고 있었네요.
우리는 사용하는 개발자들이니 어떻게 동작하는지 탐구하고, 사용하는 법에 대해 배워보았습니다 ㅎㅎ
'Developer > Spring' 카테고리의 다른 글
Spring Validation 을 활용하여 Enum entry 에 대한 validation 해보자 (0) | 2025.02.01 |
---|---|
[Gradle] Gradle multi module(project) with spring boot(feat. kotlin) (0) | 2024.09.30 |
[Test] Junit5 에서 제공하는 Tag 에 대해 알아보자 (0) | 2024.06.20 |
Spring Boot API server 성능 테스트(performance test)를 해보자(with python locust) (0) | 2024.02.12 |
[JPA] Hibernate 에서 지원하는 Id generator 에 대해 알아보자 (1) | 2023.12.17 |