계층별 검증 표준은 검증 실패 응답을 한 자리에서 만들기로 했고, 근거는 이랬다.
@RequestBody @Valid 실패는 MethodArgumentNotValidException 으로 올라와
@RestControllerAdvice 의 한 핸들러에서 필드별 오류를 만든다.
「한 핸들러」가 errors 응답 형식을 보장하는 자리다. 검증 실패가 정말 한 타입으로 모이는지 쟀다.
주장 — 요청 값 검증 실패는 한 예외로 올라온다
요청 값 검증 실패는 MethodArgumentNotValidException 하나로 올라와
Advice 의 한 핸들러에서 받는다.
근거 종류는 1(기술 사실)이다.
반증 조건 — 잡힌 핸들러가 갈리면 반증이다
- refuted — 두 환경 중 하나라도 세 경우가 한 핸들러로 모이지 않는다
- supported — 두 환경 모두 세 경우가
MethodArgumentNotValidException핸들러 하나로 모인다 - inconclusive — 서버가 뜨지 않거나 어느 경우가 검증 실패 없이 200으로 끝난다
설계 — 예외 타입마다 핸들러를 두고 어느 쪽이 잡는지 표식을 찍는다
Advice에 MethodArgumentNotValidException · HandlerMethodValidationException · BindException · ConstraintViolationException · Exception 핸들러를 각각 두고, 응답 본문에 어느 핸들러가 잡았는지 표식을 넣었다.
(가) 본문 객체 POST /probe @RequestBody @Valid, @NotBlank 위반
(나) 파라미터 제약 GET /probe?size=0 컨트롤러 @Validated + 파라미터 @Min(1)
(다) 폼·쿼리 객체 GET /probe/search @ModelAttribute @Valid, @NotBlank 위반
임베디드 톰캣을 실제로 띄웠고(server.port=0) 환경은 Spring Boot 3.5.16과 4.1.1이다. 네 예외 타입이 두 버전에 다 존재하는지도 함께 기록했다 — 전부 있었다.
결과 — 파라미터 제약만 다른 핸들러로 갔다
PROBE boot=3.5.16 (가) 400 manv MethodArgumentNotValidException
(나) 400 cv ConstraintViolationException
(다) 400 manv MethodArgumentNotValidException
PROBE boot=4.1.1 (같음)
판정은 refuted다. 상태 코드는 셋 다 400으로 같지만 잡은 핸들러가 갈린다.
예상과 달랐던 점 — 갈림은 Boot 버전이 아니라 검증이 걸리는 자리였다
두 Boot 버전이 완전히 같았다. 갈린 기준은 값이 어디에 실려 오는가다.
| 경우 | 예외 | 왜 갈리나 |
|---|---|---|
| 본문 객체 | MethodArgumentNotValidException |
객체를 바인딩한 뒤 검증한다 |
| 폼·쿼리 객체 | MethodArgumentNotValidException |
같은 경로다 |
| 파라미터 제약 | ConstraintViolationException |
메서드 파라미터 검증이라 경로가 다르다 |
폼 객체가 BindException이 아니라 MethodArgumentNotValidException이었다는 것도 예상 밖이다. 두 타입을 다 두고 쟀기 때문에 구분됐다.
범위를 밝힌다. claim.md가 컨트롤러에 @Validated를 달도록 지정했고 그 설정이 메서드 검증 경로를 태운다. @Validated 없이 파라미터에 제약만 단 변형은 돌리지 않았으므로 그쪽 결과는 모른다.
이 결과가 표준에 남기는 것 — 핸들러가 하나로 부족하다
규칙(검증 실패는 errors로 필드별로 내려준다)은 흔들리지 않는다. 깨진 것은 한 핸들러로 다 받는다는 전제다. 지금 글대로 MethodArgumentNotValidException 핸들러만 두면 파라미터 제약 위반은 그 핸들러를 안 타고, errors 형식이 아닌 응답이 나간다.
규칙은 바로 고치지 않는다. 표준 글에 > 반증: 줄을 달고 kind: 모순 이슈에서 핸들러를 몇 개 둘지, 세 경로의 응답을 같은 형식으로 맞출지를 정한다.
정리
- 본문 객체와 폼 객체는
MethodArgumentNotValidException으로 모인다 - 파라미터 제약은
ConstraintViolationException으로 갈린다. 두 Boot 버전이 같았다 - 상태 코드는 셋 다 400이다. 갈리는 것은 예외 타입과 응답 형식이다
- 재지 않은 것 —
@Validated없이 제약만 달았을 때
자신만의 철학을 만들어가는 중입니다.
댓글남기기