예외 처리 표준과 예외 종류별 처리 표준은 Error 계열을 잡지 않기로 정했고, 그 근거로 이렇게 적었다.
AssertionError는 Error 계열이라 @ExceptionHandler(Exception.class)에 잡히지 않고,
서블릿 컨테이너 기본 오류 응답이 나가 응답 형식이 깨진다.
「형식이 깨진다」가 규칙의 이유였다. 그래서 정말 안 잡히는지를 재야 했다.
주장 — 핸들러가 던진 AssertionError는 catch-all Advice에 닿지 않는다
claim.md에 한 문장으로 적었다.
Controller 핸들러가 던진 AssertionError는 @ExceptionHandler(Exception.class)에 잡히지 않는다.
근거 종류는 1(기술 사실)이다.
반증 조건 — Advice가 만든 응답 표식이 하나라도 보이면 반증이다
- refuted — 두 환경 중 하나라도 응답 본문에
handledBy=exception-handler표식이 있다 - supported — 두 환경 모두 표식이 없고, 대조군
RuntimeException에는 표식이 있다 - inconclusive — 서버가 뜨지 않거나 대조군에도 표식이 없다
대조군을 둔 이유가 여기 있다. Advice 설정 자체가 틀려서 아무것도 안 걸린 경우를 「Error라서 안 잡혔다」로 읽으면 안 된다.
설계 — MockMvc 대신 임베디드 톰캣을 실제로 띄운다
주장이 「서블릿 컨테이너까지 올라간다」는 경로를 말하므로 그 경로를 그대로 지나야 한다. server.port=0으로 톰캣을 띄우고 java.net.http.HttpClient로 호출했다.
- Advice에는
@ExceptionHandler(Exception.class)하나만 둔다. 응답 본문에handledBy=exception-handler와 받은 예외의 타입·cause를 함께 찍는다 GET /throw-assertion-error는AssertionError를, 대조군GET /throw-runtime은RuntimeException을 던진다- 환경 둘 — Spring Boot 3.5.16, 4.1.1
이 실험이 보는 것은 컨트롤러 핸들러 메서드 안에서 던진 Error뿐이다. 필터·인터셉터·비동기에서 난 Error는 재지 않았다.
결과 — 두 버전 모두 Advice가 응답을 만들었다
PROBE boot=3.5.16 ae_status=500 ae_marked=true rt_status=500 rt_marked=true
PROBE boot=4.1.1 ae_status=500 ae_marked=true rt_status=500 rt_marked=true
BODY /throw-assertion-error :: handledBy=exception-handler
type=jakarta.servlet.ServletException cause=java.lang.AssertionError
BODY /throw-runtime :: handledBy=exception-handler
type=java.lang.RuntimeException cause=none
판정은 refuted다. 대조군에 표식이 있으므로 Advice가 죽어 있던 경우도 아니다.
예상과 달랐던 점 — Advice가 받은 것은 AssertionError가 아니라 ServletException이었다
type= 칸이 답을 준다. 핸들러가 받은 예외는 jakarta.servlet.ServletException이고, 그 cause가 AssertionError였다. 감싼 쪽이 Exception 계열이므로 catch-all에 걸린다. 대조군 RuntimeException은 감싸이지 않고 그대로 왔다(cause=none).
누가 감쌌는지는 이 실험이 재지 않았다. 측정된 것은 「Advice가 받은 타입이 ServletException이고 cause가 AssertionError」까지다.
표준 글의 문장은 두 갈래였는데 결과가 갈렸다.
| 표준 글의 문장 | 결과 |
|---|---|
| 상태 코드 500이 나간다 | 맞다 (두 버전 모두 500) |
@ExceptionHandler(Exception.class)에 잡히지 않는다 |
틀렸다 |
| 서블릿 컨테이너 기본 오류 응답이 나가 형식이 깨진다 | 틀렸다 — Advice가 만든 응답이 나갔다 |
이 결과가 표준에 남기는 것 — 「Error는 잡지 않는다」의 근거가 바뀐다
규칙 자체(Error를 잡지 않는다)는 이 실험이 판정하지 않는다. 무너진 것은 그 규칙이 기대던 설명이다. 「어차피 안 잡히니 형식이 깨진다」가 아니라, catch-all을 두면 Error까지 같이 잡힌다가 실제 모양이다. 뒤집으면 「Error를 안 잡으려면 따로 손을 써야 한다」가 된다.
표준을 증명하는 방식 원칙 8대로 규칙은 바로 고치지 않고, 표준 글에 > 반증: 줄을 달고 kind: 모순 이슈에서 따진다.
정리
- 두 Boot 버전 모두
AssertionError가 catch-all Advice에 걸렸다. 상태는 500, 응답은 Advice가 만들었다 - Advice가 받은 타입은
ServletException이고 cause가AssertionError였다. 감싼 주체는 재지 않았다 - 맞은 것은 상태 코드뿐이다. 「안 잡힌다」와 「형식이 깨진다」는 틀렸다
- 재지 않은 범위 — 핸들러 밖(필터·인터셉터·비동기)에서 난
Error
자신만의 철학을 만들어가는 중입니다.
댓글남기기