CS · Encoding · Backend

인코딩 - 컴퓨터는 글자를 모른다

화면 가득한 글자를 컴퓨터는 하나도 모른다. 저장된 것은 숫자뿐이고, 그 숫자를 글자로 되돌리는 건 미리 나눠 가진 약속이다. 그 약속이 어긋날 때 무슨 일이 벌어지는지부터 본다.

·0과 1로 어떻게 담나 1편
목차
  1. 저장된 것은 숫자뿐이다
  2. 숫자만 적을 수 있는 엽서
  3. 같은 수가 세 가지로 읽힌다
  4. 표가 어긋나면 글자가 깨진다
  5. 파일에는 표 이름이 안 적혀 있다
  6. 번호를 매기는 일과 담는 일은 다르다
  7. 실무에서: 경계마다 표를 못 박는다
  8. 정리

글자가 깨져 본 적 있다면 이미 인코딩을 만난 것이다. 그날 무슨 일이 있었는지를 이 시리즈에서 푼다.

저장된 것은 숫자뿐이다

메모리든 파일이든 디스크든, 컴퓨터가 실제로 들고 있는 건 번호가 붙은 칸과 그 칸에 든 수다. 칸이 줄지어 있고 번호로 찾는다는 그 구조가 밑바닥까지 그대로다.

그래서 “파일에 이라는 글자를 저장했다”는 말은 사실 이렇게 바꿔 적어야 정확하다.

에 해당하기로 약속한 수를 저장했다.

글자는 어디에도 없다. 수만 있다. 화면에 이 보이는 건 그 수를 받아 든 프로그램이 같은 약속표를 들고 있어서 글자 모양으로 그려 준 것뿐이다.

숫자만 적을 수 있는 엽서

이 시리즈는 처음부터 끝까지 숫자만 적을 수 있는 엽서로 편지를 주고받는 두 사람을 따라간다.

엽서에는 글자를 못 쓰고 수만 적을 수 있다. 그래서 둘은 편지를 주고받기 전에 번호표를 하나씩 나눠 갖는다. 1, 2, 이런 식이다. 보내는 사람은 표를 보고 글자를 수로 바꿔 적고, 받는 사람은 같은 표를 보고 수를 글자로 되돌린다.

이 번호표가 인코딩이다. 그리고 이 비유 하나로 시리즈 전체가 굴러간다.

  • 번호표에 글자가 몇 개나 들어가나 → 아스키와 유니코드
  • 번호가 커지면 엽서에 어떻게 나눠 적나 → UTF-8
  • 표를 다르게 들면 무슨 일이 나나 → 지금부터 볼 것

같은 수가 세 가지로 읽힌다

핵심은 수 자체에는 아무 뜻도 없다는 것이다. 뜻은 어느 표로 읽느냐가 정한다.

C7D1
파일에 실제로 들어 있는 것 - 수 두 개가 전부다
한국어 표로 읽으면
두 수를 묶어 글자 하나
서유럽 표로 읽으면
ÇÑ수 하나에 글자 하나
그냥 수로 읽으면
51153글자가 아니라 값

바이트는 그대로인데 바꿔 들면 결과가 갈라진다. 셋 다 자기 표대로는 정확히 읽은 것이다.

똑같은 두 수를 놓고 표를 바꿔 들자 결과가 셋으로 갈라졌다. 어느 하나가 맞고 나머지가 틀린 게 아니다. 셋 다 자기 표대로는 정확히 읽은 것이다.

여기서 중요한 사실 하나가 따라 나온다. 수만 보고는 어느 표로 읽어야 하는지 알 수 없다. 파일을 열어 바이트를 들여다봐도 거기에 “이건 한국어 표입니다”라고 적혀 있지 않다.

표가 어긋나면 글자가 깨진다

그래서 보내는 쪽과 받는 쪽이 다른 표를 들면 사고가 난다.

같은 표를 들었을 때
한글보내는 쪽
ED 95 9C EA B8 80실려 간 바이트
UTF-8 표받는 쪽
한글
보내는 쪽은 그대로, 받는 표만 바꿨을 때
한글보내는 쪽
ED 95 9C EA B8 80실려 간 바이트
서유럽 표받는 쪽
한글

바이트는 두 판에서 한 개도 안 바뀌었다. 달라진 것은 읽을 때 든 표 하나뿐이고, 같은 표를 들었을 때만 글자가 돌아온다.

깨진 글자의 생김새는 대체로 셋 중 하나다.

  • 한글 같은 라틴 문자 무더기 - 여러 바이트를 묶어 한 글자로 만드는 약속인데, 받는 쪽이 한 바이트씩 끊어 읽었다
  • 茄臂 같은 엉뚱한 한자 - 표는 있는데 다른 나라 표다. 수를 자기 표대로 읽어 엉뚱한 글자가 나왔다
  • (검은 마름모 물음표) - 받는 쪽이 “내 표로는 이 바이트를 읽을 수 없다”고 포기한 자리다

셋이 서로 다른 증상이라는 게 중요하다. 무엇이 보이느냐가 어디서 어긋났는지를 말해 주기 때문이고, 이 시리즈의 마지막 편은 그 되짚기를 다룬다.

파일에는 표 이름이 안 적혀 있다

여기서 비유가 한 번 깨진다. 사람이라면 “이거 무슨 표로 읽어?”라고 되물을 수 있다. 파일은 못 묻는다. 프로그램은 받은 수를 무조건 자기가 아는 표로 읽어 버리고, 그 결과가 이상해도 대개 멈추지 않는다.

그래서 현실에서는 표 이름을 따로 실어 보낸다.

  • 웹은 응답 헤더Content-Type: text/html; charset=utf-8처럼 적어 보낸다
  • HTML은 문서 맨 앞에 <meta charset="utf-8">을 둔다
  • 어떤 파일은 맨 앞 몇 바이트에 표식(BOM)을 박아 둔다

셋 다 본문이 아니라 본문 바깥에 붙는 쪽지다. 쪽지가 없으면 받는 쪽은 추측한다. 그리고 추측이 틀리는 날이 사고 나는 날이다.

번호를 매기는 일과 담는 일은 다르다

인코딩 이야기가 헷갈리는 가장 큰 이유는 두 가지 다른 일이 한 이름으로 불리기 때문이다. 여기서 갈라 두면 나머지가 쉬워진다.

하는 일이 시리즈에서
번호 매기기세상의 글자마다 고유 번호를 붙인다아스키, 유니코드
번호 담기그 번호를 바이트에 어떻게 나눠 넣나UTF-8, UTF-16

유니코드는 번호표일 뿐 담는 법이 아니다. 의 번호가 54620이라는 것과, 그 번호를 3바이트로 적을지 2바이트로 적을지는 별개의 결정이다. “유니코드로 저장했다”는 말이 애매하게 들리는 이유가 이거다.

실무에서: 경계마다 표를 못 박는다

인코딩 사고는 코드 한가운데서 나지 않는다. 무언가를 넘겨주는 자리에서 난다. 파일을 읽을 때, HTTP로 받을 때, DB에 넣을 때, 다른 시스템에 넘길 때다.

그래서 실무의 원칙은 하나다. 경계마다 표 이름을 명시하고, 기본값에 기대지 않는다.

java
// 위험: 이 코드가 어떤 표를 쓸지는 실행되는 컴퓨터가 정한다
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)는 다른 일이다. 이 둘을 갈라야 나머지가 풀린다.
  • 사고는 경계에서 난다. 읽고 쓸 때 표를 코드에 명시하고 기본값에 기대지 않는다.

다음 글은 그 아래로 한 층 더 내려간다. 수를 담는 칸 자체가 몇 개의 자리로 되어 있는지부터 봐야 나머지가 보인다.