CP949
고친 사람 github-actions[bot]
CP949 는 현대 한글 음절 11,172 자를 모두 바이트로 적게 해 주는 인코딩입니다. 먼저 쓰던 한국어 인코딩이 못 적던 글자를 비어 있던 바이트 값에 채워 넣었습니다. 한국어 윈도우가 이 방식을 기본으로 썼습니다. 그래서 윈도우에서 만든 옛 한국어 파일과 데이터에 이 바이트가 많이 남아 있습니다.
쉽고 빠른 이해
CP949 는 한글 한 글자를 두 바이트로 바꾸는 규칙입니다. 「가」는 0xB0 0xA1 이 됩니다. 「갂」은 0x81 0x41 이 됩니다.
먼저 쓰던 한국어 인코딩은 한글 음절을 2,350 자만 담았습니다. 「똠」이나 「뷁」 같은 글자는 아예 적을 수 없었습니다. CP949 는 그 인코딩이 안 쓰고 남겨 둔 바이트 값에 나머지 8,822 자를 얹었습니다.
어떻게 도나:
- 바이트 값이 127(
0x7F) 이하면 영어 글자 하나로 읽습니다 - 그보다 크면 다음 바이트까지 둘을 묶습니다
- 묶은 두 바이트를 글자 목록에서 찾아 글자 하나로 되돌립니다
대가는 둘입니다. 첫째, 새로 얹은 글자의 둘째 바이트가 영어 글자와 값이 겹칩니다. 그래서 바이트에서 A 를 찾으면 한글 조각이 걸립니다. 둘째, 바이트 값의 순서가 가나다 순서와 어긋납니다. 바이트로 줄을 세우면 「갂」이 「가」보다 앞에 섭니다.
지금 새로 만드는 시스템은 세계의 글자를 한 목록에 모은 유니코드를 씁니다. CP949 는 한국어 윈도우와 옛 파일을 넘겨받을 때 만납니다.
상세
이 절은 CP949 가 옛 한국어 인코딩의 빈칸을 어떻게 메웠는지를 바이트 값으로 봅니다. 그다음 그렇게 메운 탓에 생긴 두 가지 문제와, 이 바이트를 오늘 만나는 곳을 차례로 봅니다.
인코딩과 코드 페이지
컴퓨터는 글자를 바로 저장하지 못합니다. 바이트만 저장합니다. 그래서 글자는 두 단계를 거쳐 바이트가 됩니다. 글자마다 번호를 붙이는 단계와, 그 번호를 바이트로 옮기는 단계입니다.
첫 단계에서 쓰는 목록이 문자 집합입니다. 담을 글자를 고르고 글자마다 번호를 붙여 둔 목록입니다. 이 목록을 함께 써야 서로 다른 프로그램이 같은 번호를 같은 글자로 봅니다.
한국어 쪽 목록으로는 KS X 1001 이 있습니다. 한국산업표준(Korean Industrial Standards) 가운데 하나입니다. 한글 음절 2,350 자와 한자, 여러 기호에 번호를 붙여 두었습니다.
둘째 단계의 규칙이 문자 인코딩입니다. 목록의 번호를 몇 바이트로, 어떤 값으로 적을지 정합니다. 같은 목록이라도 인코딩이 다르면 바이트가 달라집니다.
EUC-KR(Extended Unix Code for Korean)는 KS X 1001 의 번호를 바이트로 옮기는 인코딩입니다. 한글 한 글자를 두 바이트로 적습니다. CP949 는 이 EUC-KR 를 넓힌 인코딩입니다.
flowchart TD A["글자 「가」"] --> B["문자 집합 KS X 1001 의 번호"] B --> C["인코딩 EUC-KR · CP949"] C --> D["바이트 0xB0 0xA1"]
이름의 CP 는 Code Page 의 줄임말입니다. 코드 페이지는 윈도우 같은 운영체제가 인코딩마다 붙여 둔 번호입니다. 949 가 한국어 인코딩의 번호입니다. 같은 체계에서 일본어 인코딩은 932 번, 서유럽 인코딩은 1252 번입니다.
EUC-KR 가 남긴 빈칸
현대 한글로 만들 수 있는 음절은 11,172 자입니다. 첫소리 19 개, 가운뎃소리 21 개, 끝소리 28 개를 곱한 수입니다. 끝소리 28 개에는 받침이 없는 경우가 한 칸으로 들어 있습니다.
KS X 1001 에는 그 가운데 자주 쓰는 2,350 자만 들어 있습니다. 목록에 없는 음절은 EUC-KR 로 적을 수 없습니다. 「똠」이나 「뷁」이 그렇습니다. 사람 이름이나 가게 이름에 이런 글자가 들어가면 시스템에 저장할 방법이 없었습니다.
EUC-KR 에는 쓰지 않고 비워 둔 바이트 값이 있었습니다. 첫 바이트로는 0x81 ~ 0xA0 을 쓰지 않았습니다. 둘째 바이트로는 0xA1 보다 작은 값을 쓰지 않았습니다. CP949 는 이 빈 값들에 나머지 8,822 자를 채웠습니다.
기존 2,350 자의 바이트는 하나도 옮기지 않았습니다. 그래서 EUC-KR 로 적은 파일은 CP949 로 읽어도 같은 글자가 나옵니다. 거꾸로는 안 됩니다. 새로 얹은 음절이 들어간 파일은 EUC-KR 로 읽으면 그 글자에서 막힙니다.
바이트를 끊어 읽는 규칙
CP949 바이트열에는 한 바이트짜리 글자와 두 바이트짜리 글자가 섞여 있습니다. 읽는 쪽은 첫 바이트 값을 보고 어느 쪽인지 가립니다. 이렇게 글자마다 바이트 수가 다른 인코딩을 멀티바이트 인코딩이라고 부릅니다.
한 바이트짜리 글자는 ASCII(American Standard Code for Information Interchange)와 같습니다. ASCII 는 영어 글자와 숫자, 문장 부호에 0x00 ~ 0x7F 의 번호를 붙인 인코딩입니다. 영어와 숫자로만 적은 파일은 CP949 로 봐도 바이트가 한 개도 안 달라집니다.
첫 바이트가 0x81 ~ 0xFE 이면 두 바이트짜리 글자의 앞 조각입니다. 이때는 둘째 바이트까지 봐야 어떤 글자인지 정해집니다.
둘째 바이트로 쓰는 값은 세 구간입니다. 0x41 ~ 0x5A, 0x61 ~ 0x7A, 0x81 ~ 0xFE 입니다. 이 셋을 벗어난 값이 둘째 바이트로 오면 잘못된 바이트열입니다.
이 값들은 0xA1 을 경계로 두 무리로 나뉩니다. 0xA1 ~ 0xFE 는 EUC-KR 가 쓰던 구간입니다. 그보다 작은 0x41 ~ 0x5A, 0x61 ~ 0x7A, 0x81 ~ 0xA0 은 EUC-KR 가 비워 둔 세 구간입니다.
아래 그림은 첫 바이트를 세로로, 둘째 바이트를 가로로 놓았습니다. 칸마다 그 두 바이트가 무엇으로 읽히는지 적었습니다.
block-beta columns 3 h0["첫 바이트 ↓ 둘째 바이트 →"] h1["비워 둔 세 구간"] h2["EUC-KR 가 쓰던 구간"] r0["0x00 ~ 0x7F"] a0["ASCII 글자 하나 · 둘째 바이트 없음"]:2 r1["0x81 ~ 0xA0"] n1["새로 얹은 음절"] n2["새로 얹은 음절"] r2["0xA1 ~ 0xC6"] n3["새로 얹은 음절"] e1["EUC-KR 와 같은 글자"] r3["0xC7 ~ 0xFE"] x1["비어 있음"] e2["EUC-KR 와 같은 글자"]
첫 바이트가 0x81 ~ 0xA0 이면 둘째 바이트가 무엇이든 새로 얹은 음절입니다. 첫 바이트가 0xA1 ~ 0xC6 이면 첫 바이트만으로는 정해지지 않습니다. 둘째 바이트가 비워 둔 세 구간에 있으면 새로 얹은 음절입니다. EUC-KR 가 쓰던 구간에 있으면 EUC-KR 와 같은 글자입니다.
새로 얹은 음절은 첫 바이트 0xC6 에서 끝납니다. 그래서 0xC7 ~ 0xFE 줄의 왼쪽 칸은 CP949 에서도 빈 채로 남았습니다.
새로 얹은 음절은 가나다 순서로 빈 값을 채웠습니다. EUC-KR 에 이미 있는 음절은 건너뛰었습니다. 그래서 맨 첫 값 0x81 0x41 이 「갂」입니다. 「가」와 「각」이 이미 EUC-KR 에 있어서, 그다음 음절인 「갂」부터 들어갔습니다.
영어 글자와 겹치는 둘째 바이트
EUC-KR 에서는 한글의 두 바이트가 모두 0xA1 이상이었습니다. 그래서 한글 바이트 안에 영어 글자 값이 끼어들 일이 없었습니다.
CP949 는 둘째 바이트로 0x41 ~ 0x5A 와 0x61 ~ 0x7A 를 끌어다 썼습니다. 이 두 구간은 ASCII 에서 영어 대문자와 소문자의 번호입니다. 새로 얹은 한글의 뒤 조각이 영어 글자와 같은 값을 갖게 됐습니다.
파이썬으로 몇 글자를 바이트로 바꿔 보면 겹침이 바로 보입니다.
"가".encode("cp949") # b'\xb0\xa1'
"A갂".encode("cp949") # b'A\x81A'
"갂".encode("euc-kr") # UnicodeEncodeError
첫 줄의 「가」는 EUC-KR 에도 있는 글자라 두 바이트 모두 0xA1 이상입니다. 둘째 줄에서 「갂」은 0x81 0x41 입니다. 파이썬은 0x41 을 글자 A 로 찍어 보여 줍니다. 셋째 줄은 같은 「갂」을 EUC-KR 로 적으려다 번호가 없어 실패한 것입니다.
둘째 줄의 세 바이트를 두 방식으로 읽어 보면 차이가 드러납니다.
block-beta columns 4 h1["바이트"] b1["0x41"] b2["0x81"] b3["0x41"] h2["CP949 로 읽으면"] l1["A"] l2["갂"]:2 h3["한 바이트씩 훑으면"] c1["A"] c2["모르는 값"] c3["A"]
글자를 모르고 한 바이트씩 훑는 프로그램에는 A 가 두 개로 보입니다. 바이트에서 영어 글자를 찾는 검색은 「갂」의 뒤 조각을 잘못 집습니다. 대소문자를 바꾸는 처리를 바이트에 걸면 0x41 이 0x61 로 바뀝니다. 그러면 「갂」이 전혀 다른 음절이 됩니다.
쉼표, 따옴표, 역슬래시, 숫자, 줄바꿈은 둘째 바이트의 세 구간 밖에 있습니다. 그래서 쉼표로 칸을 나누거나 줄바꿈으로 줄을 자르는 처리는 CP949 에서도 한글을 망가뜨리지 않습니다. 겹치는 것은 영어 글자뿐입니다.
글자를 다루는 처리는 바이트를 먼저 글자로 되돌린 뒤에 합니다. 바이트를 글자로 되돌리는 일을 디코딩이라고 부릅니다. 디코딩한 문자열에서는 「갂」이 한 글자로 서 있으므로 A 로 잘못 집힐 일이 없습니다.
바이트 순서와 가나다 순서
새로 얹은 음절은 EUC-KR 의 2,350 자 뒤에 이어 붙지 않았습니다. 비어 있던 앞쪽 값에 따로 들어갔습니다. 그래서 바이트 값의 순서가 가나다 순서와 어긋납니다.
| 글자 | CP949 바이트 |
|---|---|
| 가 | 0xB0 0xA1 |
| 각 | 0xB0 0xA2 |
| 갂 | 0x81 0x41 |
가나다 순으로는 「가 → 각 → 갂」입니다. 바이트 값으로 줄을 세우면 「갂」이 맨 앞에 옵니다. 첫 바이트 0x81 이 0xB0 보다 작기 때문입니다.
문자열을 어떤 순서로 비교할지 정하는 규칙이 콜레이션입니다. 데이터베이스가 바이트 값으로만 비교하는 콜레이션을 쓰면 이 어긋남이 그대로 드러납니다. 정렬 결과에서 새로 얹은 음절이 엉뚱한 곳에 섭니다. 「가」부터 「나」 사이를 찾는 범위 검색에서는 「갂」이 빠집니다.
유니코드는 세계의 글자를 하나의 목록에 모아 번호를 붙인 문자 집합입니다. 이 목록은 한글 음절 11,172 자 전부에 가나다 순서대로 번호를 붙였습니다.
UTF-8(Unicode Transformation Format 8-bit)은 유니코드 번호를 1 ~ 4 바이트로 적는 인코딩입니다. 번호가 크면 바이트 값도 커지도록 적습니다. 그래서 UTF-8 바이트로 줄을 세우면 가나다 순서와 같아집니다.
적어 둔 인코딩 이름과 바이트가 어긋날 때
파일이나 데이터베이스 설정에는 이 바이트를 무슨 인코딩으로 읽을지 이름을 적어 둡니다. 그런데 CP949 로 적은 바이트에 「EUC-KR」라는 이름이 적혀 있는 일이 흔합니다. 두 인코딩은 2,350 자 안에서는 바이트가 같아서, 오랫동안 같은 것처럼 불렸습니다.
읽는 쪽이 적힌 이름을 믿고 EUC-KR 규칙으로만 읽으면 새로 얹은 음절에서 막힙니다. 첫 바이트 0x81 은 EUC-KR 에 없는 값입니다. 프로그램은 오류를 내거나 그 글자를 대체 문자로 바꿉니다. 대체 문자는 읽지 못한 글자 대신 넣는 � 표시입니다.
EUC-KR 와 전혀 다른 규칙으로 읽으면 막히지도 않습니다. 바이트 값마다 글자를 하나씩 정해 둔 서유럽 인코딩으로 읽으면 오류 없이 읽힙니다. 대신 한글 한 글자가 뜻 없는 기호 두 개로 나옵니다. 이렇게 엉뚱한 규칙으로 읽어 글자가 깨지는 현상을 모지바케라고 부릅니다.
그래서 한국어 옛 바이트는 적힌 이름이 EUC-KR 여도 CP949 로 읽는 편이 안전합니다. 아래 그림처럼 CP949 가 EUC-KR 를 전부 품으므로 잃는 글자가 없습니다.
flowchart TD
subgraph C["CP949 · 새로 얹은 음절 8,822 자"]
subgraph E["EUC-KR · KS X 1001 의 한글 2,350 자와 한자 · 기호"]
A["ASCII · 영어 글자와 숫자"]
end
end
안쪽 규칙으로 적은 파일은 바깥 규칙으로 읽어도 같은 글자가 나옵니다. 쓸 때는 반대입니다. 받는 쪽이 EUC-KR 만 읽는다면 새로 얹은 음절을 보내면 안 됩니다. 그 글자들은 받는 쪽에서 깨집니다.
CP949 의 여러 이름
같은 인코딩을 도구마다 다른 이름으로 부릅니다. 설정 파일이나 코드에서 아래 이름을 만나면 모두 CP949 를 가리킵니다.
| 이름 | 주로 만나는 곳 |
|---|---|
CP949 · cp949 |
윈도우 코드 페이지 번호 · 파이썬 |
MS949 |
[[Java |
windows-949 |
여러 라이브러리의 인코딩 이름 목록 |
UHC |
iconv 같은 변환 도구 |
| 확장 완성형 · 통합형 한글 코드 | 한국어로 쓴 문서와 설명 글 |
KO16MSWIN949 |
오라클 데이터베이스의 문자 집합 이름 |
UHC 는 마이크로소프트가 붙인 이름 Unified Hangul Code 의 줄임말입니다.
확장 완성형이라는 이름은 완성형을 넓혔다는 뜻입니다. 완성형은 「가」·「각」처럼 완성된 음절 하나하나에 통째로 번호를 붙이는 방식입니다.
오늘 CP949 를 만나는 곳
한국어 윈도우는 유니코드를 쓰지 않는 프로그램에 CP949 를 기본 인코딩으로 내줍니다. 인코딩 이름을 따로 적지 않고 파일을 여는 프로그램은 이 설정을 따라갑니다.
파이썬 오류 하나가 그 예입니다. 운영체제의 기본 인코딩을 따르는 환경에서 UTF-8 파일을 인코딩 이름 없이 열면, 파이썬은 CP949 규칙으로 읽으려 합니다. 그러다 'cp949' codec can't decode byte ... illegal multibyte sequence 오류로 멈춥니다. 여는 코드에 encoding="utf-8" 을 적으면 풀립니다.
쉼표로 칸을 나눈 CSV(Comma-Separated Values) 파일도 자주 걸립니다. 한국어 윈도우의 엑셀은 따로 표시가 없는 CSV 를 CP949 로 읽습니다. 그래서 서버가 UTF-8 로 내려 준 CSV 를 그대로 열면 한글이 깨집니다.
이때 쓰는 표시가 바이트 순서 표시입니다. UTF-8 파일이라는 것을 알리려고 파일 맨 앞에 붙이는 세 바이트 0xEF 0xBB 0xBF 입니다. 엑셀은 이 표시가 있으면 CSV 를 UTF-8 로 읽습니다. 그래서 내려 주는 CSV 앞에 이 세 바이트를 붙여 둡니다.
넘겨받은 바이트는 경계에서 한 번만 바꾸는 것이 원칙입니다. 넘겨받은 CP949 바이트는 읽는 즉시 유니코드 문자열로 바꿉니다. 시스템 안에서는 UTF-8 하나만 씁니다. 파일 하나를 한 번에 바꿀 때는 iconv 같은 변환 도구를 씁니다.
거꾸로 CP949 로 내보낼 때는 막히는 글자가 있습니다. CP949 는 현대 한글 음절은 전부 담지만 유니코드 글자 대부분은 못 담습니다. 이모지와 KS X 1001 에 없는 한자가 그렇습니다. 지금은 안 쓰는 자모가 들어간 옛한글도 못 담습니다. 내보내기 전에 그런 글자를 버릴지 다른 글자로 바꿀지 정해 두어야 합니다.
관련 항목
CP949 가 속하는 상위 분류
문자 인코딩 · 문자 집합 · 멀티바이트 인코딩 · 코드 페이지 · 포맷
CP949 가 넓힌 한국어 표준과 방식
EUC-KR · KS X 1001 · KSC-5601 · 완성형 · 조합형 · 한글 음절 · ASCII
CP949 를 대신하는 유니코드 인코딩
Unicode · UTF-8 · UTF-16 · UTF-32
CP949 와 나란히 쓰인 다른 나라 인코딩
Shift_JIS · GBK · Big5 · windows-1252 · ISO-2022-KR · ISO-8859-1
CP949 임을 알리고 바꾸는 수단
Content-Type charset · 바이트 순서 표시 · iconv · 로케일 · 디코딩
CP949 바이트를 잘못 읽었을 때 겪는 문제
모지바케 · 대체 문자 · 인코딩 불일치 · UnicodeDecodeError · 잘린 멀티바이트 문자
CP949 문자열의 순서를 정하는 규칙
콜레이션 · 정렬 · 이진 비교
CP949 를 인코딩 이름으로 받는 플랫폼
Windows · Java · Python · Oracle Database · CSV
다른 이름: cp949 · CP-949 · MS949 · ms949 · windows-949 · 코드 페이지 949 · UHC · Unified Hangul Code · 통합형 한글 코드 · 확장 완성형