예외 종류별 처리 표준은 외부 연동 실패를 5xx로 가르면서 이렇게 적었다.
상대가 4xx/5xx나 이상한 응답을 주면 RestClientResponseException으로 잡혀 502가 된다.
「이상한 응답」까지 한 예외 타입으로 잡힌다는 것이 규칙의 전제였다. 그 전제를 재봤다.
주장 — 상대의 오류 응답과 깨진 본문 둘 다 RestClientResponseException으로 잡힌다
상대 서버가 4xx·5xx를 주거나 본문이 깨진 응답을 주면,
호출한 쪽에서 RestClientResponseException으로 잡힌다.
근거 종류는 1(기술 사실)이다.
반증 조건 — 둘 중 하나라도 다른 타입이면 반증이다
- refuted — 두 환경 중 하나라도 두 경로 중 하나에서
RestClientResponseException(하위 포함)이 아닌 예외가 나오거나, 예외가 아예 나지 않는다 - supported — 두 환경 모두 두 경로에서
RestClientResponseException계열이 나온다 - inconclusive — 서버가 뜨지 않거나 호출 자체가 실패한다
설계 — 상대 역할도 같은 앱이 맡아 응답을 정확히 통제한다
외부 서비스를 띄우면 그 서비스의 동작이 변수로 들어온다. 그래서 같은 앱에 두 경로를 두고 자기 자신을 RestClient로 불렀다.
GET /upstream/500 상태 500 + 정상 JSON 본문
GET /upstream/broken 상태 200 + Content-Type: application/json + 깨진 본문 {"a":
깨진 경로는 먼저 String으로 한 번 받아 서버가 의도한 것을 실제로 주는지 확인한다(raw=ok). 이게 없으면 「서버가 내가 생각한 걸 안 줬다」와 진짜 결과를 구분할 수 없다. 환경은 Spring Boot 3.5.16과 4.1.1이고, Boot 4는 Jackson 3이라 파싱 실패 경로가 다를 수 있어 둘 다 봤다.
결과 — 오류 응답은 잡혔고 깨진 본문은 다른 타입이었다
PROBE boot=3.5.16 raw=ok s500=InternalServerError s500sub=true broken=RestClientException brokensub=false
PROBE boot=4.1.1 raw=ok s500=InternalServerError s500sub=true broken=RestClientException brokensub=false
DIAG 500 | org.springframework.web.client.HttpServerErrorException$InternalServerError
:: 500 Internal Server Error: "{"message":"upstream-boom"}"
DIAG broken | org.springframework.web.client.RestClientException
:: Error while extracting response for type [proof.Probe$Payload] and content type [application/json]
판정은 refuted다. 상대가 500을 줄 때는 HttpServerErrorException$InternalServerError로, 즉 RestClientResponseException 하위로 잡혔다. 본문이 깨졌을 때는 RestClientException이고 이건 RestClientResponseException의 하위가 아니다. 두 Boot 버전이 같았다.
예상과 달랐던 점 — 갈림은 Jackson 버전이 아니라 예외 계층에 있었다
Boot 4의 Jackson 3에서 파싱 실패 경로가 달라질까 봐 두 환경을 뒀는데, 두 버전이 같은 타입을 냈다. 갈린 것은 버전이 아니라 「응답을 받았는가」와 「응답을 값으로 바꿨는가」의 차이다. 상태 코드가 오류면 응답 자체가 실패라 RestClientResponseException이고, 본문 변환이 실패하면 그건 응답이 아니라 변환의 실패라 다른 가지로 간다.
표준대로 짠 코드에는 그래서 구멍이 생긴다. catch (RestClientResponseException e) { 502 }만 두면 깨진 본문은 그 catch를 지나쳐 바깥의 미분류 경로로 간다. 표준 글이 그 경로에 500을 배정했으므로, 상대가 준 이상한 응답이 우리 서버의 장애로 기록된다. 원래 502로 가야 할 실패다.
이 결과가 표준에 남기는 것 — 502 catch에 잡을 타입을 하나 더 정해야 한다
이 실험은 「502가 맞는 상태 코드인가」를 판정하지 않는다. 판정한 것은 한 타입으로 다 잡히지 않는다는 사실뿐이다.
규칙은 바로 고치지 않는다. 표준 글에 > 반증: 줄을 달고, kind: 모순 이슈에서 깨진 본문을 502로 볼지 다른 코드로 볼지, catch를 RestClientException까지 넓힐지를 정한다.
정리
- 상대의 500은
HttpServerErrorException$InternalServerError로 잡혔다 —RestClientResponseException하위가 맞다 - 깨진 본문은
RestClientException이라 그 catch에 안 걸린다. 두 Boot 버전이 같았다 - 표준대로 짜면 502로 갈 실패가 500으로 기록된다
- 재지 않은 것 — 연결 실패·타임아웃의 타입, 502·503·504 배정이 명세에 맞는지
자신만의 철학을 만들어가는 중입니다.
댓글남기기