UTF-8 인코딩
정의
UTF-8 은 Unicode code point (00x10FFFF) 를 14 byte 가변 길이로 인코딩하는 방식. 1993년 ISO/IEC 10646 표준의 일부로 도입, 현재 웹에서 가장 널리 쓰이는 문자 인코딩. ASCII (0~0x7F) 는 1 byte 로 동일하게 표현되어 하위 호환성이 완벽.
문제 상황과 동기
Unicode 는 149,000 개 이상의 문자에 정수 ID(code point) 를 할당했지만, 메모리에 어떻게 저장할 것인가 가 별도 문제.
- UTF-32: 모든 code point 를 4 byte 로. 간단하지만 메모리 4배 낭비. ASCII 텍스트도 문자당 4 byte.
- UTF-16: BMP(Basic Multilingual Plane, U+0000~U+FFFF) 는 2 byte, 그 이상은 surrogate pair 로 4 byte. Java/C# 내부 표현. 하지만 ASCII 도 2 byte.
- UTF-8: ASCII 는 1 byte, 그 외 언어권은 2~4 byte. 웹 페이지의 98% 가 UTF-8 사용. 파일 크기 최적 + ASCII 호환.
핵심 통찰: 선두 바이트의 상위 비트 패턴 으로 총 바이트 수를 알 수 있고, 연속 바이트는 항상 10xxxxxx 형태로 sync 가능.
시각화
핵심 아이디어
UTF-8 은 자기 동기화(self-synchronizing) 인코딩. 바이트 스트림 중간부터 읽어도 다음 문자 시작점을 찾을 수 있음.
| Code point 범위 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | 표현 가능 비트 |
|---|---|---|---|---|---|
| U+0000 ~ U+007F | 0xxxxxxx | - | - | - | 7 bit |
| U+0080 ~ U+07FF | 110xxxxx | 10xxxxxx | - | - | 11 bit |
| U+0800 ~ U+FFFF | 1110xxxx | 10xxxxxx | 10xxxxxx | - | 16 bit |
| U+10000 ~ U+10FFFF | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 21 bit |
- 선두 바이트: 선두의
0개수 = 전체 바이트 수 (1바이트는0시작으로 구분). - 연속 바이트: 항상
10으로 시작. 선두 바이트와 절대 혼동되지 않음. - Code point 비트는
x자리에 MSB부터 순서대로 채움.
알고리즘
encode(cp):
if cp < 0x80:
output [ cp ] # 0xxxxxxx
else if cp < 0x800:
output [ 0xC0 | (cp >> 6), # 110xxxxx
0x80 | (cp & 0x3F) ] # 10xxxxxx
else if cp < 0x10000:
output [ 0xE0 | (cp >> 12), # 1110xxxx
0x80 | ((cp >> 6) & 0x3F), # 10xxxxxx
0x80 | (cp & 0x3F) ] # 10xxxxxx
else:
output [ 0xF0 | (cp >> 18), # 11110xxx
0x80 | ((cp >> 12) & 0x3F), # 10xxxxxx
0x80 | ((cp >> 6) & 0x3F), # 10xxxxxx
0x80 | (cp & 0x3F) ] # 10xxxxxx
decode(bytes):
n = leading_ones(bytes[0]) # 선두 1 개수 (0이면 1바이트)
if n == 0: return bytes[0]
cp = bytes[0] & (0x7F >> n) # 선두 바이트에서 x bits 추출
for i = 1..n-1:
assert (bytes[i] & 0xC0) == 0x80 # 10xxxxxx 검증
cp = (cp << 6) | (bytes[i] & 0x3F)
return cp
구현
// code point -> UTF-8 bytes hex 출력
#include <bits/stdc++.h>
using namespace std;
int main() {
unsigned cp = 0xAC00; // '가'
unsigned char b[4]; int n;
if (cp < 0x80) { b[0] = cp; n = 1; }
else if (cp < 0x800) { b[0] = 0xC0 | (cp >> 6); b[1] = 0x80 | (cp & 0x3F); n = 2; }
else if (cp < 0x10000) { b[0] = 0xE0 | (cp >> 12); b[1] = 0x80 | ((cp >> 6) & 0x3F); b[2] = 0x80 | (cp & 0x3F); n = 3; }
else { b[0] = 0xF0 | (cp >> 18); b[1] = 0x80 | ((cp >> 12) & 0x3F); b[2] = 0x80 | ((cp >> 6) & 0x3F); b[3] = 0x80 | (cp & 0x3F); n = 4; }
printf("U+%04X -> %d bytes:", cp, n);
for (int i = 0; i < n; i++) printf(" %02X", b[i]);
printf("\n");
}U+AC00 -> 3 bytes: EA B0 80복잡도
| 항목 | 값 |
|---|---|
| 인코딩 (code point -> bytes) | O(1) (최대 4 byte) |
| 디코딩 (bytes -> code point) | O(1) |
| 바이트 수 판별 | O(1) (선두 바이트만 확인) |
| 검증 (valid UTF-8 여부) | O(N) (전체 스트림 순회) |
변형 / 활용
UTF-8 signature (BOM)
일부 Windows 프로그램이 파일 앞에 EF BB BF (BOM, Byte Order Mark) 를 붙임. Unix/Linux 에서는 불필요하며 오히려 문제.
UTF-8 vs UTF-16 vs UTF-32
| 항목 | UTF-8 | UTF-16 | UTF-32 |
|---|---|---|---|
| ASCII 크기 | 1 byte | 2 byte | 4 byte |
| CJK (한중일) | 3 byte | 2 byte | 4 byte |
| 자기 동기화 | 가능 | 불가 (surrogate 깨짐) | 가능 |
| 웹 점유율 | 98% | 2% | 0% 미만 |
Overlong encoding
code point 를 더 긴 바이트 수로 인코딩하는 것. 예: ASCII ‘A’(0x41) 를 0xC1 0x81 로. 보안 취약점 (유효성 검사 우회) 이므로 모든 구현이 이를 거부해야 함 (RFC 3629).
함정
1. 연속 바이트 검증 누락
디코더가 10xxxxxx 검증을 안 하면 임의 바이트를 문자로 오인. UTF-8 validity 체크 필수.
2. 5/6 byte sequence (CESU-8 / old UTF-8)
초기 UTF-8 은 5~6 byte (0xFC, 0xFD 선두) 를 허용했으나 RFC 3629 에서 폐지. 상호운용성 문제의 원인.
3. WTF-8 / CESU-8
Oracle / Java 가 쓰는 modified UTF-8 (CESU-8) 은 surrogate pair 를 별도 인코딩. 일반 UTF-8 과 혼동 금지.
4. Windows ANSI vs UTF-8
Windows 의 MultiByteToWideChar(CP_ACP) 는 시스템 로케일 인코딩이지 UTF-8 이 아님. CP_UTF8 명시 필요.
BOJ 연습 문제
| 번호 | 제목 | 정답률 | 링크 |
|---|---|---|---|
| BOJ 11654 | 아스키 코드 | 71.2% | kokoa-lab |
| BOJ 10809 | 알파벳 찾기 | 58.7% | kokoa-lab |
| BOJ 11720 | 숫자의 합 | 62.3% | kokoa-lab |
| BOJ 10824 | 네 수 | 28.5% | kokoa-lab |
참고
이 글의 용어 (3개)
- 문자열 (String)algorithm
- 정의 문자열 (String) 은 문자(character)의 순서 있는 나열. 프로그래밍에서는 연속된 메모리에 저장된 문자 배열 로 구현되며, 각 문자는 ASCII(7비트) 또는 …
- 정규 표현식 (Regular Expression, Regex)algorithm
- 정의 정규 표현식 (Regular Expression) 은 문자열 패턴을 형식 언어로 표현한 표기법. Stephen Kleene 이 1956년 정규 언어 (regular lang…
- 파싱 (Parsing) - tokenization, recursive descent, expressionalgorithm
- 정의 파싱 은 입력 문자열을 구조화된 표현 (AST) 으로 변환하는 과정. 컴파일러/인터프리터의 첫 단계. 세 단계: Tokenization (lexing) → Parsing (…
💬 댓글