코드 유닛
고친 사람 github-actions[bot]
코드 유닛은 인코딩된 문자열을 정해진 크기로 끊어 읽고 쓰게 해 주는 단위입니다. 글자 하나가 코드 유닛 하나에 들어가기도 하고 여러 개에 나뉘어 들어가기도 합니다. 코드 유닛의 크기는 인코딩마다 달라서 1바이트인 것도 있고 2바이트인 것도 있습니다. 여러 프로그래밍 언어가 문자열 길이를 글자가 아니라 이 단위로 셉니다.
쉽고 빠른 이해
문자열을 크기가 똑같은 조각으로 끊어 두는 단위입니다. 어떤 인코딩은 1바이트짜리 코드 유닛을 씁니다. 어떤 인코딩은 2바이트짜리 코드 유닛을 씁니다.
글자마다 번호가 하나씩 붙어 있습니다. 그 번호는 작은 것도 있고 큰 것도 있습니다. 크기가 제각각인 번호를 같은 크기의 조각으로 끊어 두어야 메모리에 나란히 놓을 수 있습니다. 그래야 몇 번째 조각인지로 바로 찾습니다.
어떻게 도는가:
- 글자마다 붙은 번호를 받습니다
- 번호가 코드 유닛 하나에 들어가면 하나에 적습니다
- 안 들어가면 코드 유닛 여러 개에 나눠 적습니다
대가도 있습니다. 코드 유닛의 개수가 글자 수와 달라집니다. 길이를 코드 유닛으로 세거나 코드 유닛 위치에서 문자열을 자르면 글자 하나가 반으로 쪼개질 수 있습니다.
상세
이삿짐을 크기가 정해진 상자에 담는다고 해 보겠습니다. 컵은 상자 하나에 들어갑니다. 분해한 책상은 상자 셋에 나눠 담습니다. 트럭에 실린 상자가 열 개라고 해서 짐이 열 개인 것은 아닙니다.
이 절은 코드 유닛을 A · 한 · 웃는 얼굴 이모지 세 글자로 따라갑니다.
글자 번호와 코드 유닛
Unicode(유니코드)는 세상의 글자마다 번호를 하나씩 붙인 표준입니다. 그 번호를
코드 포인트라고 부릅니다. A 는 U+0041, 한 은 U+D55C, 웃는 얼굴 이모지는 U+1F600
입니다. U+ 뒤의 값은 십육진수입니다.
코드 포인트는 수일 뿐입니다. 메모리나 파일에 담으려면 이 수를 정해진 크기의 비트 묶음에 적어야 합니다. 그 비트 묶음 하나가 코드 유닛입니다. 코드 유닛은 인코딩된 텍스트를 처리하거나 주고받을 때 다루는 가장 작은 단위입니다.
번호를 어떤 크기의 코드 유닛에 어떻게 나눠 적을지는 문자 인코딩이 정합니다. 그래서 같은 글자라도 인코딩이 다르면 코드 유닛의 개수와 값이 달라집니다. 번호는 인코딩과 상관없이 하나입니다.
코드 유닛을 따로 이름 붙여 부르는 까닭은 프로그램이 문자열을 다루는 방식에 있습니다. 프로그램은 문자열을 코드 유닛의 배열로 둡니다. 코드 유닛의 크기가 모두 같으니 n번째 코드 유닛을 처음부터 훑지 않고 바로 찾을 수 있습니다.
인코딩마다 다른 크기
유니코드 번호를 적는 인코딩은 셋이 널리 쓰입니다. 이름은 UTF(Unicode Transformation Format, 유니코드 변환 형식) 뒤에 숫자를 붙인 꼴입니다. 그 숫자가 코드 유닛 하나의 비트 수입니다.
| 인코딩 | 코드 유닛 크기 | 코드 포인트 하나에 쓰는 코드 유닛 |
|---|---|---|
| UTF-8 | 8비트 · 1바이트 | 1~4개 |
| UTF-16 | 16비트 · 2바이트 | 1개 또는 2개 |
| UTF-32 | 32비트 · 4바이트 | 언제나 1개 |
UTF-32 는 코드 유닛이 커서 어떤 번호든 하나에 들어갑니다. 대신 A 처럼 작은 번호도 4바이트를
씁니다.
UTF-8 은 코드 유닛이 작습니다. 작은 번호는 1바이트로 끝납니다. 큰 번호는 코드 유닛 여러 개를 이어 씁니다.
세 글자를 세 인코딩으로 적은 코드 유닛입니다. 값은 모두 십육진수입니다. 띄어 적은 값 하나가 코드 유닛 하나입니다.
| 글자 | 코드 포인트 | UTF-8 | UTF-16 | UTF-32 |
|---|---|---|---|---|
A |
U+0041 |
41 |
0041 |
00000041 |
한 |
U+D55C |
ED 95 9C |
D55C |
0000D55C |
| 웃는 얼굴 | U+1F600 |
F0 9F 98 80 |
D83D DE00 |
0001F600 |
UTF-32 열은 코드 포인트를 32비트에 옮겨 적은 것과 같습니다. UTF-8 열은 한 에 셋, 웃는 얼굴에
넷을 씁니다. UTF-16 열은 웃는 얼굴에서만 둘로 늘어납니다.
글자 하나를 여럿으로 나눠 적는 방식
앞 표에서 한 은 UTF-8 로 ED 95 9C 셋이었습니다. 웃는 얼굴은 UTF-16 으로 D83D DE00
둘이었습니다. 이 절은 그 둘이 어떻게 나뉘었는지 봅니다.
UTF-8 에서는 첫 코드 유닛의 앞쪽 비트가 이 글자가 코드 유닛 몇 개짜리인지 알립니다. 한 의 첫 코드
유닛 ED 를 비트로 펴면 1110 1101 입니다. 앞쪽 1110 은 1 이 셋이라 코드 유닛 셋짜리라는
표시입니다.
뒤따르는 코드 유닛은 모두 앞 두 비트가 10 입니다. 표의 값은 십육진수라 비트로 펴야 보입니다.
95 는 1001 0101, 9C 는 1001 1100 입니다.
글자를 시작하는 코드 유닛은 앞 두 비트가 10 이 아닙니다. 그래서 중간 어디서 읽기 시작해도
앞 두 비트가 10 이 아닌 코드 유닛을 만나면 거기서 다음 글자가 시작합니다.
UTF-16 의 코드 유닛 하나는 16비트라 번호를 65,536개까지만 담습니다. 이 범위가
기본 다국어 평면입니다. 한글 음절과 자주 쓰는 한자가 여기 들어 있습니다. 그래서 한 은
D55C 하나로 끝났습니다.
기본 다국어 평면 너머의 번호가 모인 범위는 보충 평면입니다. 웃는 얼굴의 U+1F600 이
여기 있습니다. 이 번호는 16비트 하나에 들어가지 않습니다.
그래서 UTF-16 은 보충 평면의 번호를 코드 유닛 둘로 나눠 적습니다. 이 두 코드 유닛 묶음이
대리 쌍입니다. 웃는 얼굴의 D83D DE00 이 대리 쌍입니다.
대리 쌍의 두 코드 유닛은 D800 부터 DFFF 까지의 값에서 고릅니다. 이 구간의 번호는 어떤
글자에도 주지 않습니다. 그래서 반쪽 하나만 보고도 대리 쌍의 반쪽인지 알 수 있습니다.
둘 중 어느 인코딩이든 규칙은 같습니다. 여러 코드 유닛으로 나뉜 글자에서 하나만 떼어 내면 그것은 어떤 글자도 아닙니다.
코드 유닛으로 세는 문자열 길이
Java 와 JavaScript 는 문자열을 UTF-16 코드 유닛의 열로 둡니다. Java 의 char 하나가
UTF-16 코드 유닛 하나입니다. 두 언어 모두 문자열 길이를 물으면 코드 유닛 수를 돌려줍니다.
A · 한 · 웃는 얼굴 이모지를 이은 문자열을 Java 로 세어 보겠습니다. 웃는 얼굴은 대리 쌍
\uD83D\uDE00 으로 적었습니다. 줄마다 돌려주는 값을 오른쪽 주석에 붙였습니다.
String s = "A한\uD83D\uDE00";
s.length() // 4
s.codePointCount(0, 4) // 3
s.charAt(2) // '\uD83D'
s.substring(0, 3) // "A한\uD83D"
length() 는 코드 유닛 넷을 셌습니다. codePointCount(0, 4) 는 0번부터 4번 앞까지의 코드
유닛 구간에서 코드 포인트 셋을 셌습니다.
charAt(2) 는 대리 쌍의 앞쪽 반쪽만 꺼냈습니다. substring(0, 3) 은 웃는 얼굴을 반으로 잘라
화면에 깨진 글자가 나오는 문자열을 만듭니다.
같은 문자열이라도 어느 단위로 세느냐에 따라 길이가 갈립니다.
| 단위 | A한 + 웃는 얼굴 |
|---|---|
| UTF-8 코드 유닛 · 바이트 | 8 |
| UTF-16 코드 유닛 | 4 |
| UTF-32 코드 유닛 · 코드 포인트 | 3 |
UTF-8 의 코드 유닛은 1바이트라 코드 유닛 수가 바이트 수와 같습니다. UTF-32 는 코드 유닛 하나가 코드 포인트 하나라 두 수가 같습니다. UTF-16 만 둘 사이 어딘가에 섭니다.
눈에 보이는 글자 수는 이 셋과 또 다를 수 있습니다. 코드 포인트 여럿이 모여 글자 하나로
그려지는 경우가 있기 때문입니다. 그 묶음이 그래핌 클러스터입니다. e 뒤에 악센트 부호
U+0301 을 이으면 코드 포인트 둘이 화면에 é 한 글자로 보입니다.
코드 유닛으로 셀 때와 안 셀 때
코드 유닛으로 세는 쪽이 맞는 일이 있습니다. 문자열이 메모리를 얼마나 차지하는지 셀 때가 그렇습니다. 같은 언어 안에서 문자열 위치를 주고받을 때도 코드 유닛 위치가 가장 빠릅니다.
사용자에게 보이는 글자 수를 제한할 때는 코드 유닛이 맞지 않습니다. 이모지 하나가 둘로 세어져 입력 칸이 글자를 덜 받습니다. 이때는 그래핌 클러스터로 셉니다.
데이터베이스 컬럼의 바이트 한도를 지킬 때도 맞지 않습니다. 이때는 저장할 인코딩의 바이트로
셉니다. 한 은 UTF-16 코드 유닛으로 하나지만 UTF-8 로는 3바이트입니다.
다른 언어로 만든 시스템과 문자열 위치를 주고받을 때도 조심합니다. 한쪽은 UTF-16 코드 유닛으로, 다른 쪽은 코드 포인트로 위치를 세면 이모지 뒤부터 위치가 어긋납니다. 주고받기 전에 어느 단위로 센 위치인지 맞춥니다.
코드 유닛과 바이트 순서
코드 유닛이 1바이트보다 크면 파일이나 네트워크에 적을 때 바이트 순서를 정해야 합니다. UTF-16
코드 유닛 D55C 는 D5 5C 로 적을 수도, 5C D5 로 적을 수도 있습니다. 여러 바이트로 된
값을 어느 쪽부터 적느냐가 엔디언입니다.
값의 윗부분을 맡은 바이트를 앞에 두는 방식이 빅 엔디언입니다. 아랫부분을 맡은
바이트를 앞에 두는 방식은 리틀 엔디언입니다.
읽는 쪽이 순서를 모르면 한 이 전혀 다른 글자로 읽힙니다.
그래서 UTF-16 과 UTF-32 파일은 맨 앞에 바이트 순서 표시를 두기도 합니다. U+FEFF 라는
정해진 번호를 첫 코드 유닛으로 적어 두는 것입니다. UTF-16 파일이라면 읽는 쪽은 첫 두 바이트를
봅니다. FE FF 면 빅 엔디언, FF FE 면 리틀 엔디언입니다.
UTF-8 은 코드 유닛이 1바이트라 이 문제가 없습니다.
UTF-8 의 코드 유닛은 옥텟 하나라고도 씁니다. 옥텟은 8비트 묶음 하나를 가리키는 이름입니다. 바이트는 기계에 따라 8비트가 아닌 때가 있었습니다. 그래서 8비트임을 못 박아야 하는 문서는 바이트 대신 옥텟이라고 씁니다.
글자 하나가 바이트가 되기까지 지나는 단계를 한 으로 그리면 이렇습니다.
flowchart TD
A["글자 · 한"] --> B["코드 포인트 · U+D55C"]
B --> C["UTF-16 코드 유닛 · D55C"]
C --> D["빅 엔디언 바이트 · D5 5C"]
C --> E["리틀 엔디언 바이트 · 5C D5"]
번호를 코드 유닛으로 바꾸는 단계와 코드 유닛을 바이트로 바꾸는 단계가 따로 있습니다. 코드 유닛은 그 두 단계 사이에 놓인 값입니다. 코드 유닛이 1바이트인 UTF-8 에서는 뒤 단계가 할 일이 없습니다.
관련 항목
코드 유닛으로 번호를 적는 인코딩
문자 인코딩 · UTF-8 · UTF-16 · UTF-32 · UCS-2 · EUC-KR
코드 유닛과 헷갈리는 세는 단위
코드 포인트 · 바이트 · 옥텟 · 비트 · 그래핌 클러스터 · 글리프
코드 유닛 여럿으로 글자를 적는 방식
대리 쌍 · 기본 다국어 평면 · 보충 평면 · 가변 길이 인코딩 · 결합 문자
코드 유닛을 바이트로 적을 때 정하는 순서
엔디언 · 빅 엔디언 · 리틀 엔디언 · 바이트 순서 표시 · 문자 인코딩 스킴
코드 유닛 수를 문자열 길이로 돌려주는 언어와 타입
Java · JavaScript · C · char · 문자열
코드 유닛을 잘못 다뤄 나는 문제
모지바케 · 대체 문자 · 문자열 자르기 · 인코딩 불일치 · 짝 잃은 대리 코드
글자에 번호를 매기는 표준
다른 이름: code unit · 코드 단위 · 부호 단위