엔터티, 인코딩, 국제화
- 4.1 엔터티와 인코딩
- 4.2 국제화
- 4.3 내용 협상과 트랜스코딩
HTTP가 실어나르는 "화물"의 구조를 모르면, 결국 헤더를 읽을 때 감으로 때리게 돼.
HTTP 메시지를 택배 상자에 비유하면, 메시지가 상자(컨테이너)이고 엔터티가 상자 안의 화물이야. 엔터티는 엔터티 헤더와 엔터티 본문으로 구성되거든. Content-Type, Content-Length, Content-Encoding 같은 헤더가 화물의 메타데이터를 설명하고, 본문이 실제 바이트 데이터야.
Content-Length는 단순해 보이지만 꽤 중요해. 지속 커넥션에서 응답이 어디서 끝나는지 알려주는 역할을 하거든. 이게 없으면 수신자가 메시지를 다 받았는지조차 확인할 수 없어. 커넥션이 갑자기 끊기면 잘림을 감지할 수도 있고. 콘텐츠가 gzip으로 압축되었다면, Content-Length는 압축된 후의 크기야 — 원본 크기가 아니라. Content-MD5로 무결성 검증도 가능하긴 한데, TCP랑 TLS가 이미 해주니까 실무에서는 잘 안 써.
Content-Type은 MIME 타입을 명시하는 거고, 텍스트의 경우 charset 파라미터가 핵심이야. Content-Type: text/html; charset=utf-8 — 이게 없으면 브라우저가 바이트를 어떤 문자로 해석할지 모르잖아. 파일 업로드할 때 쓰는 multipart/form-data도 여기서 나오는 개념이지. 각 파트가 boundary 문자열로 구분되고, 파트마다 자체 Content-Type을 가져.
콘텐츠 인코딩은 엔터티 본문을 압축해서 전송 크기를 줄이는 거야. 서버가 본문을 gzip 같은 걸로 압축하고, Content-Encoding 헤더를 추가하면, 클라이언트가 그걸 보고 디코딩하는 흐름이지. gzip이 가장 널리 쓰이고, br(Brotli)이 더 높은 압축률을 자랑해. 클라이언트는 Accept-Encoding: gzip, deflate, br로 지원하는 인코딩을 서버에 알려줘.
전송 인코딩은 콘텐츠 인코딩과 다른 개념이야. 콘텐츠 인코딩이 엔터티 자체를 변환하는 거라면, 전송 인코딩은 네트워크 전송을 위한 변환이야. 그중 청크 인코딩이 가장 중요한데, 서버가 동적으로 콘텐츠를 생성할 때 전체 크기를 미리 알 수 없거든. 청크 인코딩을 쓰면 Content-Length 없이도 데이터를 보낼 수 있어 — 각 청크의 크기만 16진수로 알려주고, 크기 0인 청크가 오면 끝이야.
HTTP/1.1 200 OK
Transfer-Encoding: chunked
1a
이것은 첫 번째 청크입니다
10
두 번째 청크다
0
웹 리소스는 시간이 지나면 변하니까, 같은 URL이라도 시점에 따라 다른 인스턴스를 가질 수 있어. **검사기(validator)**는 특정 인스턴스를 식별하는 값인데, 강한 검사기(ETag)는 바이트 하나만 바뀌어도 변하고, 약한 검사기(Last-Modified)는 의미 있는 변경만 감지해. 범위 요청은 Range: bytes=4000-처럼 문서의 일부분만 요청하는 기능이야. 다운로드 중단 후 이어받기가 이걸로 구현되지. 서버가 지원하면 206 Partial Content로 응답하고.
여기서 국제화 얘기로 넘어갈게. HTTP가 전 세계에서 쓰이려면 다양한 언어와 문자를 처리할 수 있어야 하잖아. 핵심은 두 가지야 — 문자 인코딩이랑 언어. 문자 인코딩은 charset 파라미터로 지정하고, 언어는 Content-Language 헤더로 알려줘.
문자집합과 문자 인코딩은 엄밀히 다른 개념이야. 문자집합은 어떤 문자들이 포함되어 있는지고, 인코딩은 그 문자를 바이트로 변환하는 규칙이야. 근데 HTTP에서 charset=utf-8이라고 하면 사실상 인코딩을 지정하는 거지. 유니코드의 인코딩 방식으로는 UTF-8(가변 1~4바이트, ASCII 호환, 웹 표준), UTF-16(2 또는 4바이트, Windows/Java 내부), UTF-32(고정 4바이트, 공간 낭비)가 있어. 현대 웹에서는 UTF-8이 사실상 표준이야 — 모든 유니코드 문자를 표현하면서 ASCII와 호환되고 공간 효율도 좋거든. 한국어 인코딩 역사를 보면 EUC-KR(완성형 2,350자), CP949(확장판), 그리고 결국 UTF-8로 수렴했지.
언어 태그는 ko, en, en-US 같은 표준화된 문자열이야. 서버는 Content-Language: ko로 응답 본문의 언어를 알려주고, 클라이언트는 Accept-Language: ko, en-US;q=0.8로 선호 언어를 보내. URI에 한글 같은 비ASCII 문자를 넣으려면 퍼센트 인코딩을 써야 해 — 검색이 %EA%B2%80%EC%83%89이 되는 식이야. IRI라는 국제화 버전이 제안되어서 브라우저 주소창에서는 한글을 직접 보여주지만, 실제 HTTP 요청에서는 여전히 퍼센트 인코딩으로 변환돼. 참고로 HTTP의 날짜는 항상 GMT 기준이고, **국제화 도메인 이름(IDN)**은 내부적으로 Punycode로 변환돼.
마지막으로 내용 협상. 같은 URL에 한국어, 영어, 일본어 버전이 있을 때, 클라이언트에게 가장 적합한 버전을 골라주는 메커니즘이야. 세 가지 방식이 있어 — 클라이언트 주도(서버가 목록을 주고 사용자가 고름, 왕복 두 번이라 느림), 서버 주도(Accept 헤더를 보고 서버가 자동 선택, 가장 흔함), 투명 협상(프락시 캐시가 대신 골라줌, 표준화 미완). 서버 주도 협상에서는 Accept, Accept-Language, Accept-Charset, Accept-Encoding 헤더와 q값(0.0~1.0)으로 선호도를 표현해. q=1.0이 최선호, 생략하면 기본값 1.0이야.
근데 같은 URL인데 클라이언트에 따라 다른 응답을 보내면 캐시가 헷갈리잖아. 이걸 해결하는 게 Vary 헤더야. Vary: Accept-Language, Accept-Encoding이라고 하면, 캐시에게 "이 헤더 값이 다른 요청에는 캐시된 응답을 쓰지 마"라고 알려주는 거지.
서버에 클라이언트가 원하는 버전이 아예 없으면? 트랜스코딩이라는 방법이 있어. 기존 콘텐츠를 변환하는 건데 — 포맷 변환(이미지 해상도 변경 등), 정보 축소(광고 제거, 요약본), 콘텐츠 주입(광고 삽입) 같은 종류가 있지. 미리 모든 변형을 만들어둘 수도 있고, 요청 시점에 동적으로 변환할 수도 있어.
정리
4장 읽고 기억할 거 세 가지:
- 엔터티 = 헤더 + 본문이고, gzip으로 압축하고 청크 인코딩으로 스트리밍해. Content-Length 없이도 동적 콘텐츠를 전송할 수 있는 게 청크 인코딩 덕분이야
- UTF-8이 현대 웹의 표준 인코딩이야. charset 파라미터가 빠지면 문자 깨짐이 발생하고, Accept-Language로 언어 협상을 해
- 내용 협상은 Accept 헤더 + q값으로 선호를 표현하고, Vary 헤더로 캐시와 연결해. 같은 URL에서 클라이언트마다 다른 버전을 제공하는 게 핵심이야