Files
llm-wiki/raw/errors/mockmvc-406-produces-accept-double-fault-2026-06-02.md

57 lines
3.8 KiB
Markdown

---
title: error / mockmvc-406-produces-accept-double-fault-2026-06-02
source_type: error-note
status: raw
related_branches: [feature-api-contract-baseline]
related_projects: [ca-skeleton]
tags: [error, ca-skeleton, spring-mvc, content-negotiation, 406, mockmvc, testing]
created: 2026-06-02
status_label: resolved
---
# error: mockmvc-406-produces-accept-double-fault-2026-06-02
> Layer: `raw/errors/` — 실제 발생한 오류 / 막힘 / 트러블슈팅의 원석.
## Parent / 부모
- [[raw/branch-notes/feature-api-contract-baseline]] — D9 (406 vs 415 distinct) 계약 테스트 작성 중 발생.
## 증상
406 Not Acceptable 핸들러(D9)를 검증하려고, `produces = APPLICATION_JSON` 인 핸들러에 `Accept: application/xml` 로 요청해 `HttpMediaTypeNotAcceptableException` 을 유발하는 테스트를 작성. `status().isNotAcceptable()` 단언이 `AssertionError`, 그 원인은 `IllegalArgumentException` 이었고 응답이 406 envelope 으로 떨어지지 않았다.
## 근본 원인 / Root cause
이중 실패(double-fault)였다. 컨텐츠 협상 실패로 406 이 발생하면 `GlobalExceptionHandler.handleHttpMediaTypeNotAcceptable` 가 envelope `ResponseEntity<Envelope>` 를 반환한다. 그런데 이 **에러 응답 본문 자체** 도 클라이언트의 `Accept: application/xml` 에 맞춰 직렬화돼야 하는데, 그 미디어 타입을 만족하는 메시지 컨버터가 없어(JSON 만 등록) 응답 작성 단계에서 다시 협상 실패가 난다. 실 서버(full Spring)에서는 에러 경로가 JSON 으로 강제 작성되지만, standalone MockMvc 의 최소 컨버터 구성에서는 이 2차 실패가 그대로 표면화된다.
즉 "produces/Accept 불일치" 시나리오는 406 *핸들러 로직* 이 아니라 *테스트 하네스의 컨버터 협상* 을 시험하게 되어, 정작 검증하려는 핸들러 매핑을 못 본다.
## 해결 / Resolution
테스트를 협상 경로 대신 **예외를 직접 던지는 probe** 로 전환:
```java
@GetMapping("/t/not-acceptable")
Map<String,String> notAcceptable() throws HttpMediaTypeNotAcceptableException {
throw new HttpMediaTypeNotAcceptableException(List.of(MediaType.APPLICATION_JSON));
}
```
요청은 기본 `Accept`(*/*) 라 406 envelope 이 JSON 으로 정상 직렬화되고, `ResponseEntityExceptionHandler` 우산 → `handleHttpMediaTypeNotAcceptable` override 가 실제로 타는지 결정적으로 검증된다. 415(`handleHttpMediaTypeNotSupported`)는 요청 본문 Content-Type 으로 자연스럽게 유발 가능하므로 그대로 두고, 406 만 직접 throw 로 분리.
## 회고 / Lessons
- **406 의 본질: 응답 표현 협상 실패.** 그 에러 응답을 거부된 미디어 타입으로 다시 쓰려 하면 무한히 협상 실패한다 — 실서버는 fallback 으로 해결하지만 테스트 하네스는 다를 수 있다.
- 핸들러 *매핑/분류* 를 검증할 때는 협상 경로를 통하기보다 해당 예외를 직접 던지는 게 결정적이고 하네스-독립적. (협상 자체의 동작은 별도 통합 테스트에서.)
- 415(요청 본문) 와 406(응답 표현) 는 RFC 9110 상 의미가 다르고 유발 경로도 다르다 — 테스트도 분리해야 한다. 이 분리 자체가 D9 가 "둘을 같은 코드로 뭉개지 말라" 고 한 이유의 실증.
## 재발 가능성
- 향후 XML/기타 표현 협상을 지원하면 produces/Accept 경로의 통합 테스트가 필요해지고, 그때는 컨버터를 갖춘 full-context 테스트로 가야 한다.
## Sources / 근거
- `src/adapter-web/src/main/java/dev/caskeleton/adapter/web/error/GlobalExceptionHandler.java``handleHttpMediaTypeNotAcceptable` / `handleHttpMediaTypeNotSupported`
- `src/adapter-web/src/test/java/dev/caskeleton/adapter/web/error/TransportErrorHandlingTest.java`