본문으로 건너뛰기
김신건의 로그

UTF-8 인코딩

· 수정 · 📖 약 4분 · 1,113자/단어 #algorithm #string #utf8
utf8, UTF-8, Unicode, 유니코드

정의

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 1Byte 2Byte 3Byte 4표현 가능 비트
U+0000 ~ U+007F0xxxxxxx---7 bit
U+0080 ~ U+07FF110xxxxx10xxxxxx--11 bit
U+0800 ~ U+FFFF1110xxxx10xxxxxx10xxxxxx-16 bit
U+10000 ~ U+10FFFF11110xxx10xxxxxx10xxxxxx10xxxxxx21 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-8UTF-16UTF-32
ASCII 크기1 byte2 byte4 byte
CJK (한중일)3 byte2 byte4 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 (…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기