콘텐츠 협상(Content Negotiation)

Accept·Accept-Language·Accept-Encoding과 q-value로 표현을 고르는 원리, 서버 주도 협상과 에이전트 주도 협상, 캐시를 지키는 Vary 헤더, 그리고 406 응답을 설명합니다.

하나의 URL이 여러 형태로 존재할 수 있습니다. 같은 문서를 HTML로도 JSON으로도, 한국어로도 영어로도, gzip 압축본으로도 원본으로도 제공할 수 있죠. 콘텐츠 협상(content negotiation)은 클라이언트와 서버가 이 여러 '표현(representation)' 중 어느 것을 주고받을지 합의하는 HTTP 메커니즘입니다. 클라이언트는 자신이 선호하는 것을 Accept 계열 요청 헤더로 알리고, 서버는 가진 표현 중 가장 잘 맞는 것을 골라 응답합니다.

Accept 계열 헤더

클라이언트의 선호를 표현하는 요청 헤더는 축별로 나뉩니다. 각 축은 독립적으로 협상됩니다.

q-value로 우선순위 매기기

각 후보 뒤에 붙는 q 파라미터(quality value)는 0부터 1 사이의 선호 가중치입니다. q를 생략하면 기본값은 1.0(최고 선호)이고, q=0은 '이건 받지 않겠다'는 명시적 거부입니다. 서버는 이 가중치를 보고 가장 높은 점수의 표현을 고릅니다. 아래 요청은 '한국어를 가장 원하지만(1.0), 없으면 영어(0.8)도 좋고, 그 밖의 언어(0.5)는 최후의 수단'이라는 뜻입니다.

GET /article/42 HTTP/1.1
Host: example.com
Accept: text/html, application/json;q=0.8
Accept-Language: ko;q=1.0, en;q=0.8, *;q=0.5
Accept-Encoding: br, gzip;q=0.9

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Language: ko
Content-Encoding: br
Vary: Accept, Accept-Language, Accept-Encoding

서버 주도 협상 vs 에이전트 주도 협상

협상에는 두 방식이 있습니다. 서버 주도(server-driven) 협상은 위 예처럼 서버가 요청 헤더를 보고 알아서 최적 표현을 골라 바로 응답하는 방식으로, 왕복이 한 번이라 가장 흔합니다. 단점은 서버가 클라이언트의 진짜 선호를 완벽히 알 수 없다는 점입니다. 에이전트 주도(agent-driven) 협상은 서버가 우선 선택지 목록을 돌려주고(예: 300 Multiple Choices) 클라이언트가 그중 하나를 다시 요청하는 방식입니다. 정확하지만 왕복이 늘어 실무에서는 드물고, 대신 URL 자체에 형식을 넣는 방식(/article/42.json, /ko/article/42)이 명시적 대안으로 널리 쓰입니다.

Vary: 캐시가 표현을 헷갈리지 않게

콘텐츠 협상은 캐시와 위험하게 얽힙니다. 같은 URL이 클라이언트에 따라 다른 응답을 내므로, 캐시가 한국어 사용자에게 저장한 응답을 영어 사용자에게 그대로 주면 사고입니다. 그래서 서버는 Vary 응답 헤더로 '이 응답은 나열된 요청 헤더 값에 따라 달라진다'고 알려야 합니다. Vary: Accept-Language가 있으면 캐시는 언어별로 응답을 따로 보관합니다. 협상에 쓴 헤더를 Vary에 빠뜨리면 잘못된 변형이 다른 사용자에게 전달되는 캐시 오염이 발생합니다.

406 Not Acceptable

서버가 가진 어떤 표현도 클라이언트의 Accept 조건을 만족하지 못할 때 반환하는 코드가 406 Not Acceptable입니다. 예를 들어 클라이언트가 Accept: application/xml만 받겠다고 했는데 서버가 JSON밖에 없으면 406을 줄 수 있습니다. 다만 실무에서는 엄격히 406을 내는 대신, 서버가 가진 기본 표현(대개 JSON)을 그냥 돌려주는 API가 많습니다. 사용자 경험상 빈 오류보다 최선의 표현을 주는 편이 낫다는 판단이며, 어느 쪽을 택하든 API 문서에 동작을 명시하는 것이 중요합니다.