실무의 함정 — 선언과 실체를 일치시킨다
문자 깨짐의 정체는 「선언(라벨)」과 「실체(바이트 열)」의 불일치. charset 선언·Excel 과 BOM 있는 UTF-8·줄바꿈 코드·데이터베이스의 연결 설정이라는 현장의 단골을 정리한다.
「어제까지 정상이었는데」— 깨짐은 경계에서 일어난다
개발 환경에서는 정상인데 프로덕션에 올렸더니 페이지가 깨졌다. 배치가 출력한 CSV 를, 이용자가 Excel 에서 열면 깨진다. 앱에서는 올바르게 등록된 것처럼 보이는데, DB 를 직접 보면 다른 문자가 들어 있다. — 실무의 문자 깨짐은, 대개 시스템과 시스템의 경계, 즉 데이터가 손에서 손으로 넘겨지는 곳에서 일어납니다.
이 장의 도구는 1 가지뿐입니다. 경계를 넘는 데이터에는, 실체(바이트 열)와, 그것을 어떻게 읽어야 하는지 전하는 선언(라벨)이 있다. 그리고 선언은 바이트 열을 변환해 주지 않는다. 그저 표찰입니다. 표찰과 내용물이 어긋난 순간, 받은 쪽은 잘못된 대응표를 찾고, 제 1 장에서 본 문자 깨짐이 재생됩니다.
실체는 UTF-8, 라벨은 Shift_JIS. 브라우저는 라벨을 믿으므로 깨집니다. 고쳐야 할 것은 라벨인가, 실체인가 — 양쪽의 가능성을 의심하는 것이 조사의 틀입니다.
웹의 선언 — Content-Type 과 meta charset
웹 페이지의 인코딩 라벨은 2 곳에 있습니다. HTTP 응답 헤더의 Content-Type: text/html; charset=UTF-8 과, HTML 안의 <meta charset="UTF-8"> 입니다. 양쪽이 있을 때는 HTTP 헤더가 우선됩니다 — 서버의 설정이, 파일에 쓰인 meta 를 덮어쓸 수 있다는 것입니다.
「파일은 UTF-8 로 변환했고 meta 도 고쳤는데, 프로덕션에서만 깨진다」는 괴현상은, 대개 서버나 프레임워크가 낡은 charset 을 헤더에 붙이고 있습니다. 반대로, 선언이 어디에도 없으면 브라우저는 추측으로 읽습니다. 실체를 UTF-8 로 하고, 헤더와 meta 양쪽을 UTF-8 로 맞춘다 — 현대 웹의 기본형입니다.
Excel 과 CSV — 「BOM 있는 UTF-8」이 효력을 발휘하는 장면
CSV 에는 charset 헤더 같은 선언의 구조가 없습니다. 더블 클릭으로 열렸을 때, 종래의 Excel 은 지역 설정의 기본값(일본어 환경이라면 CP932)으로 읽으러 갑니다. UTF-8 로 출력한 CSV 가 깨지는 단골은 이것입니다.
여기서 제 3 장의 BOM 이 활약합니다. 파일 선두에 EF BB BF 가 붙어 있으면, Excel 은 그것을 표시로 삼아 UTF-8 로 읽어 줍니다. 「시스템에서 출력하는 CSV 는 BOM 있는 UTF-8 로 한다」는, Excel 이용자에게 열릴 파일의 정석입니다. 다만 만능은 아닙니다 — BOM 을 상정하지 않은 다른 시스템에 같은 파일을 건네면, 선두 3 바이트가 쓸데없는 데이터로서 오동작을 부르는 일이 있습니다. 누가 읽는 파일인지에 따라, BOM 의 필요 여부를 정하는 것이 실무의 판단입니다.
소문제 6-1 — 선언과 실체
「라벨은 바이트 열을 변환하지 않는다」는 원칙을, 웹과 Excel 의 단골 장면에서 확인합니다.
Q1. HTTP 의 Content-Type 헤더의 charset 이나 HTML 의 meta charset 의 역할로 올바른 것은 어느 것입니까.
charset 선언은 어디까지나 「읽는 방식의 지시서」로, 바이트 열 그 자체에는 손을 대지 않습니다. 그래서 UTF-8 의 실체에 charset=Shift_JIS 라고 쓰면, 브라우저는 고지식하게 Shift_JIS 의 표로 찾아서 깨집니다. 「선언을 바꿨는데 고쳐지지 않는다」면 실체 쪽이, 「실체를 변환했는데 깨진다」면 선언 쪽이 낡은 채라는 구분법이 그대로 쓸 수 있습니다.
Q2. UTF-8 로 저장된 HTML 이 charset=Shift_JIS 로 선언되어 배포되어, 문자가 깨졌습니다. 문자 깨짐의 「정체」의 설명으로 가장 정확한 것은 어느 것입니까.
제 1 장의 틀 그 자체입니다 — 깨짐의 정체는 항상 「쓴 인코딩」과 「읽은 인코딩」의 어긋남으로, 이번에는 선언이 읽는 방식을 그르치게 했습니다. 바이트 열은 파손되지 않았고, 라벨을 실체에 맞추는 것만으로 해결됩니다. 「궁합」이나 「버그」라는 말로 처리하지 말고, 어느 쪽이 어긋났는지를 말할 수 있게 되는 것이 이 강좌의 목표입니다.
Q3. UTF-8(BOM 없음)로 출력한 CSV 를 이용자가 더블 클릭으로 Excel 에서 열면 일본어가 깨진다, 는 단골 트러블이 있습니다. 파일 쪽에서의 대표적인 대책은 어느 것입니까.
종래의 Excel 은 더블 클릭으로 연 CSV 에 인코딩의 선언이 없으면, 지역 설정의 기본값(일본어 환경에서는 CP932)으로 읽어 버립니다. 선두에 BOM 을 붙이면 「UTF-8 이다」라는 표시로 기능하여, 올바르게 읽힙니다. 제 3 장에서 「BOM 은 필수가 아닌 표시」라고 배웠지만, Excel 연계는 그 표시가 실무에서 위력을 발휘하는 대표 예입니다. 반대로 BOM 을 상정하지 않는 수신 프로그램에서는 선두 3 바이트가 방해가 되는 일도 있어, 상대에 맞춰 붙일지 뗄지를 판단합니다.
줄바꿈 코드 — 또 하나의 「보이지 않는 바이트」
인코딩 상담을 받으면, 종종 범인은 줄바꿈 코드였다, 는 일이 있습니다. 줄바꿈도 바이트입니다. Windows 는 CRLF(0D 0A)의 2 바이트, Unix/Linux·macOS 는 LF(0A)의 1 바이트가 관습으로, 같은 「겉모습」의 텍스트라도 바이트 열이 다릅니다.
CRLF 의 파일을 LF 전제의 프로그램에 건네면, 각 행의 끝에 CR(0x0D)이 남아, 「값의 끝에 보이지 않는 1 문자가 붙어 비교가 실패한다」 「행 수를 세는 방식이 어긋난다」 같은, 문자 깨짐보다도 알아차리기 어려운 버그가 됩니다. CSV 연계의 약속에서는, 인코딩과 줄바꿈 코드를 세트로 지정하는(예: UTF-8·BOM 없음·LF) 것이 정석입니다.
데이터베이스 — 선언은 경로의 전 구간에 필요
데이터베이스에도 인코딩 설정은 여럿 있습니다. 대표적인 것은 테이블이나 열의 인코딩(바이트 열을 어떻게 저장·해석하는가)과, 연결의 인코딩(앱과 DB 의 대화가 무엇으로 이루어지는가의 선언)입니다. 앱 → 연결 → 테이블이라는 경로의 어느 1 구간이라도 선언과 실체가 어긋나면, 저장 시나 조회 시에 오변환·오해석이 일어나, 깨진 데이터가 저장되어 버립니다.
성가신 것은, 웹 페이지와 달리 깨진 결과가 영속화된다는 점입니다. 제 1 장의 철칙 「깨진 채로 저장하지 않는다」의 최악형이, DB 에서 조용히 진행되는 깨진 데이터의 축적입니다. 신규 구축에서는 처음에 전 구간을 UTF-8 계로 맞추고, 기존 시스템에서는 「어느 구간의 설정이 실체와 어긋나 있는가」를 1 구간씩 확인한다 — 조사의 틀은 여기서도 같습니다.
이 강좌의 구호, 실무판: 깨지면 「어느 경계에서, 선언과 실체가 어긋났는가」. 쓴 인코딩과 읽은 인코딩의 어긋남은, 경계의 수만큼 일어날 수 있다.
소문제 6-2 — 보이지 않는 바이트와 연결 설정
줄바꿈 코드와 데이터베이스 — 「보이지 않는 부분」의 불일치를 마무리합니다.
Q4. 줄바꿈 코드의 설명으로 올바른 것은 어느 것입니까.
줄바꿈도 CR(0x0D)·LF(0x0A)라는 어엿한 바이트입니다. Windows 계는 CRLF 의 2 바이트, Unix 계는 LF 의 1 바이트가 관습으로, 행 단위로 파일을 처리하는 프로그램이나 셸 스크립트가 「행 끝에 쓸데없는 1 바이트가 붙는다/안 붙는다」로 오동작합니다. 문자 집합의 문제는 아니지만, 「보이지 않는 부분의 바이트가 환경에 따라 다르다」는 점에서 인코딩 트러블의 친척이며, CSV 연계에서는 인코딩과 세트로 확인해야 할 항목입니다.
Q5. 데이터베이스에서 문자 깨짐이 일어나는 전형적인 원인으로 올바른 것은 어느 것입니까.
데이터베이스에는 「테이블이나 열의 인코딩」과 「연결의 인코딩(클라이언트가 무엇으로 말하는가의 선언)」이라는 복수의 설정이 있고, 이것들의 불일치가 깨짐의 전형적 원인입니다. 앱은 UTF-8 의 바이트 열을 보내고 있는데 연결 설정이 latin1 인 채, 같은 구성은 「넣을 때와 꺼낼 때의 읽는 방식이 어긋난다」를 DB 내부에서 일으킵니다. 웹 서버의 charset 과 마찬가지로, 선언과 실체의 일치는 경로의 전 구간에서 필요합니다.
Q6. 받은 텍스트 파일의 인코딩을, 읽기 전에 100% 확실하게 아는 방법이 있을까요.
인코딩의 자동 판정은 「이 바이트 열은 어느 표로 읽으면 자연스러운가」라는 추측이며, 짧은 파일이나 복수의 인코딩으로 해석 가능한 바이트 열에서는 빗나갑니다(유명한 예로, 내용에 따라 오판정이 일어나는 케이스가 옛날부터 알려져 있습니다). 그래서 실무의 본진은 판정 기술이 아니라 약속입니다 — 인터페이스 사양서에 「UTF-8(BOM 없음), 줄바꿈 LF」라고 명기하고, 선언할 수 있는 곳(HTTP 헤더·DB 연결 설정)에서는 반드시 선언한다. 깨짐의 추리(제 7 장)는, 그 약속이 깨졌을 때의 조사술입니다.
이 장에서 가져갈 것
- charset 은 라벨. 바이트 열은 변환되지 않는다. HTTP 헤더는 meta 보다 우선된다
- Excel 에서 열릴 CSV 는 BOM 있는 UTF-8 이 정석. 다만 BOM 의 필요 여부는 「누가 읽는가」로 정한다
- 줄바꿈 코드(CRLF 0D 0A / LF 0A)는 인코딩과 다른 축의 「보이지 않는 바이트」 문제. 약속은 세트로
- DB 는 테이블의 인코딩과 연결의 인코딩의 전 구간 일치가 필요. 깨짐의 영속화가 최악의 패턴
- 인코딩을 확실하게 판정하는 방법은 없다. 사양으로 정하고, 선언과 실체를 일치시킨다가 본진
도구는 모두 갖추어졌습니다. 마지막 장에서는, 깨진 화면 그 자체를 증거로 삼아, 오독의 조합을 추리하는 종합 연습입니다.