Skip to Content

6장. 2바이트의 세계 — 한자와 한글을 담다

만화로 보는 요약 — 먼저 읽어보세요

만화로 보는 요약 — 먼저 읽어보세요

동아시아 문자는 두 바이트를 이어 담아 방을 넓혔고, 선행 바이트로 경계를 찾게 됐지만 바이트 수와 글자 수가 달라 반쪽을 자르면 문자가 깨진다.

면접 실전 질문: ① 멀티바이트 인코딩이 필요한 이유는? ② 선행 바이트는 어떤 역할을 하는가? ③ 바이트 단위로 자르면 왜 문자가 깨지는가?


배경 — 256칸으로는 시작도 못 한다

5장은 유럽의 이야기였어요. éñ 몇십 개는 빈 128칸에 욱여넣을 수 있었죠. 그런데 동아시아로 오면 스케일이 달라집니다. 한자는 상용만 수천, 다 합치면 수만 자예요. 한글도 음절을 조합하면 만 자가 넘고요. 256칸? 명함도 못 내밉니다.

방법은 하나뿐이었어요. 한 바이트로 안 되면, 두 개를 붙이면 됩니다. 바이트 하나가 256가지니, 둘을 이으면 256 × 256 = 65,536가지. 이제야 한자와 한글을 담을 방이 생겼어요. 이 “글자 하나에 바이트 둘”이 바로 멀티바이트(multibyte) 인코딩입니다.

그런데 여기서 수십 년 동안 굳어온 상식 하나가 깨집니다. “바이트 하나 = 글자 하나” 가 더 이상 참이 아니게 된 거예요.

스토리 — 첫 바이트가 신호를 준다

문제가 하나 있었습니다. 무작정 모든 글자를 2바이트로 만들 순 없었어요. 영문과 숫자, 그리고 기존 ASCII 기반 시스템을 다 버릴 순 없으니까요. ASCII는 1바이트 그대로 두고, 한자·한글만 2바이트로 담아야 했습니다.

그럼 컴퓨터가 바이트를 읽다가, 이게 1바이트짜리 영문인지 2바이트짜리 한글의 시작인지 어떻게 알까요? 여기에 영리한 규칙이 있어요. 첫 바이트의 값을 보는 겁니다.

  • 바이트 값이 0~127(ASCII 범위)이면 → 그냥 1바이트짜리 영문·숫자.
  • 바이트 값이 128 이상(최상위 비트가 켜진 값)이면 → “아, 2바이트 글자의 첫 바이트 구나” 하고 다음 바이트까지 묶어서 한 글자로 읽는다.

이 “2바이트 글자를 여는” 첫 바이트를 선행 바이트(lead byte) 라고 불러요. (엄밀히 EUC-KR의 선행 바이트는 128이 아니라 0xA1(161)부터 시작하지만, “최상위 비트가 켜지면 멀티바이트”라는 설계 원리는 그대로예요.) 한국의 EUC-KR로 실제 바이트를 찍어보면 이 규칙이 눈에 보입니다.

s = "A가B" print(s.encode("euc-kr")) # b'A\xb0\xa1B' ← A=1바이트, 가=2바이트(0xB0 0xA1), B=1바이트 print(len(s), "글자 /", len(s.encode("euc-kr")), "바이트") # 3 글자 / 4 바이트

A는 그대로 0x41 한 바이트, B도 한 바이트. 그런데 0xB0 0xA1 두 바이트죠. 첫 바이트 0xB0(176)은 128보다 크니까, 컴퓨터는 “여긴 2바이트 글자 시작”이라 판단하고 뒤 0xA1까지 한 글자로 묶습니다. 이렇게 각 나라가 자기 표준을 만들었어요. 중국은 GB2312, 일본은 Shift-JIS, 한국은 EUC-KR. 이런 방식을 통틀어 더블바이트 문자 집합(DBCS, Double-Byte Character Set) 이라고 부릅니다. (이름은 “더블바이트”지만 ASCII는 1바이트로 섞여 있으니 실제로는 가변 폭이에요. 관례로 굳은 이름일 뿐입니다.)

핵심 — 가변 길이가 돌아왔다

눈치채셨나요? 어떤 글자는 1바이트, 어떤 글자는 2바이트 — 이거 가변 길이예요. 2장 모스 부호에서 봤던 그 방식이 돌아온 겁니다. 그때의 저주도 같이 돌아왔어요. 글자 경계가 어디냐는 문제요.

모스는 간격(구분자)으로 경계를 표시했지만, 멀티바이트는 첫 바이트의 최상위 비트가 그 역할을 합니다. “128 넘으면 두 칸”이라는 규칙 자체가 경계 신호인 거죠. 덕분에 붙여 써도 풀어 읽을 수 있어요. (단, 이 깔끔한 규칙은 EUC 계열(한국 EUC-KR·중국 GB2312) 얘기예요. 일본 Shift-JIS는 여기서 한 번 더 꼬이는데 — 두 번째 바이트가 하필 ASCII 영역과 겹쳐서 난리가 납니다. 그 사고는 9장에서 따로 다뤄요.) 여기까진 좋습니다. 그런데 이 구조가 주니어 개발자를 지금도 울리는 버그를 하나 품고 있어요.

바이트를 반쪽만 자르면 글자가 깨집니다.

b = "한글".encode("euc-kr") # b'\xc7\xd1\xb1\xdb' — 4바이트(한=2, 글=2) print(b[:3]) # b'\xc7\xd1\xb1' — 3바이트에서 뎅강 잘랐다 print(b[:3].decode("euc-kr")) # UnicodeDecodeError! '글'의 반쪽만 남았다

한글은 4바이트인데, 앞 3바이트만 자르면 (2바이트)은 온전하지만 선행 바이트만 남고 뒷 바이트가 잘려요. 반쪽짜리 글자, 기계는 이걸 해독하지 못합니다. “문자열을 100 바이트로 자르라”는 순진한 코드가, 하필 한글 한가운데를 자르면 마지막 글자가 깨지는 거예요. 바이트 길이와 글자 수가 다르다는 걸 잊는 순간 터지는 사고죠. (이 “길이” 문제는 뒤에서 UTF-16·이모지까지 가며 훨씬 더 깊어집니다.)

정리하면, 멀티바이트는 동아시아 문자를 담는 데 성공했지만 대가가 있었어요.

  • 글자 하나 = 바이트 하나라는 상식이 깨졌다. 이제 바이트 수를 세도 글자 수를 알 수 없다. 문자열 자르기·길이 계산이 전부 조심스러워졌다.
  • 여전히 나라마다 다른 표다. GB2312·EUC-KR·Shift-JIS… 2바이트로 방은 넓혔지만, 5장의 근본 문제 — “어느 나라 약속표냐” — 는 하나도 안 풀렸다. 오히려 표가 더 커지고 복잡해졌을 뿐.

정리

  • 한자·한글은 수만 자라 256칸으로는 불가능 → 바이트 둘을 이어 65,536칸을 여는 멀티바이트(2바이트) 인코딩이 등장했다(중국 GB2312, 일본 JIS, 한국 EUC-KR = DBCS).
  • ASCII(0~127)는 1바이트로 두고, 첫 바이트가 128 이상이면 2바이트 글자의 선행 바이트로 보는 규칙으로 1바이트·2바이트를 섞었다 → 가변 길이의 귀환.
  • 대가 ①: “바이트 하나 = 글자 하나”가 깨졌다. 바이트 반쪽을 자르면 글자가 깨지고 (UnicodeDecodeError), 바이트 길이 ≠ 글자 수가 됐다.
  • 대가 ②: 여전히 나라별 표라, “어느 약속표냐”는 5장의 문제는 그대로 남았다.

생각해볼 질문: 이제 한국 차례예요. 65,536칸이 생겼으니 한글을 넣으면 되는데 — 여기서 또 갈림길이 나옵니다. 한글은 자음·모음을 조합하는 글자잖아요? 완성된 음절(, )을 통째로 하나씩 표에 넣을까요, 아니면 자음·모음 조각을 조합하도록 설계할까요? 한국이 이 두 길에서 어떤 선택을 했는지, 그리고 그 선택이 오늘까지 어떤 흔적을 남겼는지 봅시다.

7장 · EUC-KR, 완성형과 조합형 — 한국의 선택

Last updated on