예외 처리 표준예외 종류별 처리 표준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-errorAssertionError를, 대조군 GET /throw-runtimeRuntimeException을 던진다
  • 환경 둘 — 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이고, 그 causeAssertionError였다. 감싼 쪽이 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

자신만의 철학을 만들어가는 중입니다.
최상단으로 이동했습니다!
확대 이미지

댓글남기기