목차
글자가 깨져 본 적 있다면 이미 인코딩을 만난 것이다. 그날 무슨 일이 있었는지를 이 시리즈에서 푼다.
저장된 것은 숫자뿐이다
메모리든 파일이든 디스크든, 컴퓨터가 실제로 들고 있는 건 번호가 붙은 칸과 그 칸에 든 수다. 칸이 줄지어 있고 번호로 찾는다는 그 구조가 밑바닥까지 그대로다.
그래서 “파일에 한이라는 글자를 저장했다”는 말은 사실 이렇게 바꿔 적어야 정확하다.
한에 해당하기로 약속한 수를 저장했다.
글자는 어디에도 없다. 수만 있다. 화면에 한이 보이는 건 그 수를 받아 든 프로그램이 같은 약속표를 들고 있어서 글자 모양으로 그려 준 것뿐이다.
숫자만 적을 수 있는 엽서
이 시리즈는 처음부터 끝까지 숫자만 적을 수 있는 엽서로 편지를 주고받는 두 사람을 따라간다.
엽서에는 글자를 못 쓰고 수만 적을 수 있다. 그래서 둘은 편지를 주고받기 전에 번호표를 하나씩 나눠 갖는다. 1은 ㄱ, 2는 ㄴ, 이런 식이다. 보내는 사람은 표를 보고 글자를 수로 바꿔 적고, 받는 사람은 같은 표를 보고 수를 글자로 되돌린다.
이 번호표가 인코딩이다. 그리고 이 비유 하나로 시리즈 전체가 굴러간다.
- 번호표에 글자가 몇 개나 들어가나 → 아스키와 유니코드
- 번호가 커지면 엽서에 어떻게 나눠 적나 → UTF-8
- 표를 다르게 들면 무슨 일이 나나 → 지금부터 볼 것
같은 수가 세 가지로 읽힌다
핵심은 수 자체에는 아무 뜻도 없다는 것이다. 뜻은 어느 표로 읽느냐가 정한다.
바이트는 그대로인데 표를 바꿔 들면 결과가 갈라진다. 셋 다 자기 표대로는 정확히 읽은 것이다.
똑같은 두 수를 놓고 표를 바꿔 들자 결과가 셋으로 갈라졌다. 어느 하나가 맞고 나머지가 틀린 게 아니다. 셋 다 자기 표대로는 정확히 읽은 것이다.
여기서 중요한 사실 하나가 따라 나온다. 수만 보고는 어느 표로 읽어야 하는지 알 수 없다. 파일을 열어 바이트를 들여다봐도 거기에 “이건 한국어 표입니다”라고 적혀 있지 않다.
표가 어긋나면 글자가 깨진다
그래서 보내는 쪽과 받는 쪽이 다른 표를 들면 사고가 난다.
바이트는 두 판에서 한 개도 안 바뀌었다. 달라진 것은 읽을 때 든 표 하나뿐이고, 같은 표를 들었을 때만 글자가 돌아온다.
깨진 글자의 생김새는 대체로 셋 중 하나다.
한글같은 라틴 문자 무더기 - 여러 바이트를 묶어 한 글자로 만드는 약속인데, 받는 쪽이 한 바이트씩 끊어 읽었다茄臂같은 엉뚱한 한자 - 표는 있는데 다른 나라 표다. 수를 자기 표대로 읽어 엉뚱한 글자가 나왔다�(검은 마름모 물음표) - 받는 쪽이 “내 표로는 이 바이트를 읽을 수 없다”고 포기한 자리다
셋이 서로 다른 증상이라는 게 중요하다. 무엇이 보이느냐가 어디서 어긋났는지를 말해 주기 때문이고, 이 시리즈의 마지막 편은 그 되짚기를 다룬다.
파일에는 표 이름이 안 적혀 있다
여기서 비유가 한 번 깨진다. 사람이라면 “이거 무슨 표로 읽어?”라고 되물을 수 있다. 파일은 못 묻는다. 프로그램은 받은 수를 무조건 자기가 아는 표로 읽어 버리고, 그 결과가 이상해도 대개 멈추지 않는다.
그래서 현실에서는 표 이름을 따로 실어 보낸다.
- 웹은 응답 헤더에
Content-Type: text/html; charset=utf-8처럼 적어 보낸다 - HTML은 문서 맨 앞에
<meta charset="utf-8">을 둔다 - 어떤 파일은 맨 앞 몇 바이트에 표식(BOM)을 박아 둔다
셋 다 본문이 아니라 본문 바깥에 붙는 쪽지다. 쪽지가 없으면 받는 쪽은 추측한다. 그리고 추측이 틀리는 날이 사고 나는 날이다.
번호를 매기는 일과 담는 일은 다르다
인코딩 이야기가 헷갈리는 가장 큰 이유는 두 가지 다른 일이 한 이름으로 불리기 때문이다. 여기서 갈라 두면 나머지가 쉬워진다.
| 하는 일 | 이 시리즈에서 | |
|---|---|---|
| 번호 매기기 | 세상의 글자마다 고유 번호를 붙인다 | 아스키, 유니코드 |
| 번호 담기 | 그 번호를 바이트에 어떻게 나눠 넣나 | UTF-8, UTF-16 |
유니코드는 번호표일 뿐 담는 법이 아니다. 한의 번호가 54620이라는 것과, 그 번호를 3바이트로 적을지 2바이트로 적을지는 별개의 결정이다. “유니코드로 저장했다”는 말이 애매하게 들리는 이유가 이거다.
실무에서: 경계마다 표를 못 박는다
인코딩 사고는 코드 한가운데서 나지 않는다. 무언가를 넘겨주는 자리에서 난다. 파일을 읽을 때, HTTP로 받을 때, DB에 넣을 때, 다른 시스템에 넘길 때다.
그래서 실무의 원칙은 하나다. 경계마다 표 이름을 명시하고, 기본값에 기대지 않는다.
// 위험: 이 코드가 어떤 표를 쓸지는 실행되는 컴퓨터가 정한다
new String(bytes);
new FileReader("data.csv");
// 안전: 표를 코드가 정한다
new String(bytes, StandardCharsets.UTF_8);
Files.newBufferedReader(Path.of("data.csv"), StandardCharsets.UTF_8);앞의 코드가 무서운 건 틀렸을 때도 잘 돌아간다는 점이다. 개발 장비에서는 멀쩡하고 서버에 올리면 깨진다. 운영체제 기본값이 달라서다. 내 컴퓨터에선 되는데의 전형적인 얼굴 중 하나가 바로 인코딩이다.
DB도 같다. 연결의 인코딩, 테이블·컬럼의 문자셋, 클라이언트가 보내는 인코딩이 세 군데 다 맞아야 한다. 하나만 어긋나도 저장은 성공하고 조회할 때 깨진다.
정리
- 컴퓨터에 글자는 없다. 수만 있고, 그 수를 글자로 되돌리는 약속표가 인코딩이다.
- 같은 수도 어느 표로 읽느냐에 따라 다른 글자가 된다. 셋 다 자기 표대로는 정확히 읽은 것이다.
- 수 자체에는 표 이름이 안 들어 있다. 그래서
charset·meta·BOM처럼 바깥에 쪽지로 붙여 보낸다. - 번호 매기기(유니코드)와 번호 담기(UTF-8)는 다른 일이다. 이 둘을 갈라야 나머지가 풀린다.
- 사고는 경계에서 난다. 읽고 쓸 때 표를 코드에 명시하고 기본값에 기대지 않는다.
다음 글은 그 아래로 한 층 더 내려간다. 수를 담는 칸 자체가 몇 개의 자리로 되어 있는지부터 봐야 나머지가 보인다.