한글 JSON 요청이 잘릴 때: HTTP Content-Length는 문자 수가 아니라 바이트 수다
한글 JSON의 문자 수와 UTF-8 바이트 길이를 혼동해 본문이 잘리는 문제를 재현한다. Python 로컬 HTTP 검증 6개로 자동 길이 계산과 직렬화 표현의 차이를 확인한다.
한글이 들어간 JSON 요청만 잘리거나 파싱에 실패한다면, HTTP Content-Length를 문자열의 문자 수로 계산했는지 확인한다. UTF-8로 인코딩한 실제 본문 바이트를 만든 뒤 그 길이를 사용해야 한다. Python http.client에서는 바이트 본문을 넘기고 길이 헤더를 직접 지정하지 않는 방식으로 이 계산을 라이브러리에 맡길 수 있다. 아래 로컬 HTTP 예제로 실패와 수정 결과를 확인한다.
문자 묶음이 더 긴 바이트 묶음으로 표현되는 모습을 길이 눈금과 함께 그린 개념 일러스트다. 정확한 문자·바이트 수를 나타내는 도식은 아니다.
작성·문서 확인: 2026-11-05. 검증 환경: Linux, CPython 3.12.14, 표준 라이브러리 HTTP 클라이언트와 루프백 서버. 압축하지 않은 JSON 요청을 대상으로 했다. 실제 TTS 서비스나 외부 API의 장애 사례를 재현했다고 주장하지 않는다.
문자 수가 맞아도 HTTP 길이는 틀릴 수 있다
RFC 9110 §8.6은 Content-Length의 단위를 옥텟, 즉 바이트로 정의한다. 이 예제에서 세어야 할 대상은 JSON 값 안의 글자만이 아니라 중괄호·키·따옴표 등을 포함한 전체 직렬화 본문이다.
예제의 {"text":"안녕"}은 Python 문자열 길이로 13이지만 UTF-8 바이트로는 17이다. 영문 ASCII만 넣으면 두 길이가 같아 잘못된 계산이 드러나지 않을 수 있다. Python의 len(str)는 화면에 보이는 글자 수를 항상 뜻하는 것도 아니므로, 이를 범용적인 전송 길이 계산으로 쓰지 않는다.
JSON을 네트워크의 서로 독립된 시스템 사이에서 교환할 때 UTF-8을 사용해야 한다는 요구는 RFC 8259 §8.1에 있다. 인코딩을 정하는 일과 길이를 계산하는 일은 연결돼 있다. 먼저 인코딩을 끝내야 실제 전송할 바이트를 셀 수 있다.
해결 순서: 직렬화 → UTF-8 바이트 → 전송
예제의 정상 경로는 json.dumps(...).encode("utf-8")로 본문을 한 번 만들고 그 바이트 객체를 전송한다. Content-Type: application/json을 지정하고, Content-Length는 생략한다.
Python 3.12의 HTTPConnection.request 문서는 Content-Length와 Transfer-Encoding이 없고 본문이 바이트 객체인 경우 길이 헤더를 자동으로 추가하는 동작을 설명한다. 이는 이 API와 본문 타입에 대한 설명이며, 모든 HTTP 라이브러리나 스트리밍 입력이 같은 방식으로 동작한다는 뜻은 아니다.
직접 헤더를 만들어야 하는 저수준 경로라면 len(body)처럼 실제로 보낼 바이트 객체에서 길이를 얻는다. 직렬화를 다시 하면서 공백·이스케이프 설정을 바꾸거나, 길이 계산 뒤 본문을 바꾸지 않는다.
실행 가능한 재현: 잘못된 13바이트와 올바른 17바이트
아래를 content_length_demo.py로 저장하고 python3 content_length_demo.py로 실행한다. 외부 패키지는 필요 없다. 서버는 127.0.0.1의 임시 포트에만 바인딩하며 종료 시 닫는다.
이 서버는 선언된 길이만 읽은 뒤 JSON 해석 결과를 응답하는 검증용 장치다. 운영용 HTTP 서버 구현이나 잘못된 요청 처리 지침이 아니다. 실패 실험은 요청마다 연결을 닫아 남은 바이트를 다음 요청으로 재사용하지 않는다.
실제 실행 결과는 다음과 같다.
결과를 읽을 때 구분할 것
| 검증 사례 | 확인한 결과 | 의미 |
| ASCII JSON | 문자 16, 바이트 16, 자동 길이 정상 | 영문 테스트만으로는 오류가 숨을 수 있다 |
| 한글 JSON | 문자 13, 바이트 17, 자동 길이 정상 | 전체 본문의 UTF-8 길이를 사용한다 |
| 이모지 JSON | 문자 12, 바이트 15, 자동 길이 정상 | 언어 이름으로 배수를 정할 수 없다 |
| 문자 수 13을 헤더로 지정 | 17바이트 중 13바이트를 읽고 JSON 실패 | 디코딩·파싱 전에 본문 길이부터 점검한다 |
| 바이트 수 17을 헤더로 지정 | 원래 한글 값을 복원 | 같은 바이트 본문을 기준으로 길이를 지정한다 |
| ASCII 이스케이프 JSON | 23바이트지만 복원한 값은 동일 | 같은 JSON 값도 직렬화 바이트 수는 달라진다 |
마지막 사례는 ensure_ascii=True로 만든 별도의 표현이다. 이 설정을 바꾸면 헤더 오류가 가려질 수 있지만 길이 계산 원칙을 대신하지 못한다. 원래 값은 같아도 전송 표현은 달라진다.
진단 응답의 HTTP 200은 검증 서버가 결과 보고서를 돌려줬다는 뜻이다. valid=false인 요청까지 정상 JSON이었다는 뜻은 아니다. 운영 API가 같은 오류 코드나 메시지를 반환한다고 추정해서는 안 된다.
적용 범위와 확인 절차
이 재현은 HTTP/1.1 요청과 HTTP/1.0 진단 응답을 사용하는 로컬 실험이다. 프록시·TLS·HTTP/2·HTTP/3·청크 전송·압축 본문은 시험하지 않았다. Content-Length가 없는 정상적인 스트리밍 요청에 임의의 추정 길이를 추가하지 않는다. 전송 방식은 사용하는 클라이언트의 문서를 따른다.
운영 점검에서는 먼저 실제 전송 직전 본문의 바이트 길이와 길이 헤더를 비교하고, ASCII·한글·이모지 입력을 각각 시험한다. 내용 전체를 로그에 남기기보다 길이와 요청 ID 등 필요한 정보부터 기록한다. 헤더를 라이브러리에 맡겼는데도 실패한다면 직렬화 이후 본문 변경, 중간 계층의 처리, 서버의 실제 읽기·파싱 단계를 나눠 확인한다.
잘못된 Content-Length를 공개 서비스에 보내며 시험하지 않는다. 여기서 확인한 것은 통제된 단일 연결에서의 본문 잘림이다. 실제 중간 계층을 포함한 요청 경계나 보안 동작은 별도 검증 대상이다. RFC 9110 §8.6의 잘못된 길이 경고
English version: HTTP Content-Length for UTF-8 JSON: Count Bytes, Not Characters