이것은 문서의 이전 버전입니다!


UTF-8

유니코드 문자 인코딩1). UTF는 "UCS 변환 포맷"으로 전송·저장에 적합한 표현 방법을 이르는데, UTF-8은 그 중에서도 가장 널리 쓰이는 축에 속한다.

Ken ThompsonRob PikePlan 9에서 사용할 목적으로—그래서 원래 이름은 FSS-UTF(File System Safe UTF)이었다—만들었는데 그 설계가 매우 깔끔하고 효율적이라 온갖 곳에서 다 쓰이고 있다. 보통 메모리 상에 들어 있는 유니코드 문자열(UTF-16이나 UTF-32인 경우가 대부분) 빼고는 다 UTF-8을 써도 된다고 해도 과언이 아닐 정도. 에서도 웬만한 기존 문자 인코딩보다 많이 쓰이고2) 기존 문자 인코딩의 흔한 문제점(깨진문자 등)을 해결할 수 있어서 꾸준히 대체되는 추세.

구조

UTF-8은 8비트 바이트를 기준으로 하는 인코딩으로, 각 문자는 1바이트에서 4바이트까지의 가변 길이로 표현된다. 실제로 몇 바이트를 쓰는지는 문자의 코드 포인트에 따라 결정된다:

코드 포인트 UTF-8
시작
비트 패턴 비트 패턴 바이트 시작
바이트 끝
U+0000
U+007F
0aaaaaaa 0aaaaaaa 00
7F
U+0080
U+07FF
00000ccc bbaaaaaa 110cccbb 10aaaaaa C2 80
DF BF
U+0800
U+FFFF
ddddcccc bbaaaaaa 1110dddd 10ccccbb 10aaaaaa E0 A0 80
EF BF BF
U+10000
U+10FFFF
000fffee ddddcccc bbaaaaaa 11110fff 10eedddd 10ccccbb 10aaaaaa F0 90 80 80
F4 8F BF BF

반대로, 각 바이트가 문자의 어떤 부분인지는 비트 패턴을 보고 파악할 수 있다:

비트 패턴 바이트 범위 설명
0xxxxxxx 00 - 7F 코드 포인트 0xxxxxxx.
10xxxxxx 80 - BF 110xxxxx, 1110xxxx11110xxx 바이트 다음에 나오는 "나머지" 바이트. 한 개의 나머지 바이트는 코드 포인트의 6비트를 인코딩한다.
110xxxyy C0 - DF 코드 포인트 00000xxx yy......의 시작. 한 개의 나머지 바이트가 따라온다.
1110xxxx E0 - EF 코드 포인트 xxxx.... ........의 시작. 두 개의 나머지 바이트가 따라온다.
11110xxx F0 - F7 코드 포인트 000xxx.. ........ ........의 시작. 세 개의 나머지 바이트가 따라온다.
11111xxx F8 - FF (사용하지 않음)

추가 규칙으로, U+0000 같은 경우 기본적으로는 00, C0 80, D0 80 80, F0 80 80 80 같은 여러 가지 방법으로 인코딩될 수 있지만 이 중 가장 짧은 00만이 올바른 UTF-8 표현이 된다. 따라서 실제로는 다음과 같은 제약이 추가된다:

  • 바이트 80(U+0000~003F에 대응)과 81(U+0040~007F에 대응)은 사용하지 않는다.
  • 바이트 D0 뒤에는 80부터 9F까지의 바이트가 올 수 없다(U+0000~07FF에 대응).
  • 바이트 F4 뒤에는 90부터 BF까지의 바이트가 올 수 없다(U+110000~13FFFF에 대응).
  • 바이트 F5(U+140000~17FFFF에 대응)부터 F7(U+1C0000~1FFFFF에 대응)까지는 사용하지 않는다.

결과적으로 모든 유니코드 문자열은 정확히 하나의 올바른 UTF-8 표현을 갖는다. (종종 이 성질을 무시하고 대강 구현하는 경우가 있는데 십중팔구 보안취약점으로 자주 깨지곤 한다…) 기술적으로 말하면 서로게이트 문자도 오면 안 되는데, 현실에서는 안 그런 경우도 있다(수정 UTF-8 참고).

설계 원칙

UTF-8의 설계 원칙은 당시 요구되던 "적절한" 유니코드 문자 인코딩의 성질을 정확히 만족한다:

  • U+0000부터 U+007F까지는 ASCII 및 그에 기반한 다양한 8비트 문자 인코딩과 호환된다. 따라서 ASCII만 쓰는 경우 대응되는 UTF-8 문자열은 변환이 필요 없다!
    • 문자열 안에 U+0000이 들어 가지 않는다면 UTF-8 문자열은 C널로 끝나는 문자열로 그대로 사용할 수 있다. 따라서 파일시스템을 비롯해 널 문자가 들어 가면 엉망이 되는 기존 시스템에 수정 없이 사용할 수 있다.
  • 인코딩 및 디코딩은 비트 연산 몇 개만으로 쉽게 할 수 있다. UTF-8이 처음 나오던 당시 고려되던 가변 길이 인코딩은 1~5바이트를 사용하는 UTF-1이었으나 느린 나눗셈 연산을 사용해야 하는 문제가 있었다.
  • 각 코드포인트의 처음에 올 수 있는 바이트와 그 뒤에 올 수 있는 바이트가 명확히 구분되어 있다.
    • 다른 말로 하면, 한 코드포인트에 대한 문자열이 다른 코드포인트에 대한 문자열에 포함되는 일이 없다는 얘기다. 따라서 일반적인 바이트 단위 문자열 검색을 그대로 UTF-8 문자열에 쓸 수 있다. (대부분의 멀티바이트 인코딩은 인코딩에 맞춰서 문자열 검색 루틴을 새로 짜야 한다.)
    • UTF-8 문자열을 바이트 단위로 잘랐을 때 손쉽게 에러가 나는 문자를 지울 수 있다. 예를 들어 문자열에 바이트 크기 제한이 있어서 뒷부분을 자를 때, 자르는 위치가 코드포인트의 중간이라면 잘못된 UTF-8 문자열이 나오겠지만 그 상태에서 그 코드포인트를 삭제하는 건 어려운 일이 아니다.
    • 마찬가지로 문자열 중간에서 디코딩 오류가 발생했을 때, 다음 "올바른" 문자로 넘어 가서 계속 디코딩을 진행하는 것(재동기화)이 간단하다. 웬만한 멀티바이트 인코딩은 문자열 안에 한 바이트가 삭제되면 그 뒤의 여러 글자가 깨지는 경우가 부지기수이다.
  • UTF-8 문자열을 바이트 단위로 정렬하면 코드 포인트를 기준으로 사전식 정렬이 된다. 물론 유니코드 문자열을 제대로 정렬하려면 별도의 알고리즘이 필요하긴 한데, 그 정도의 복잡도가 필요 없는 경우 그럭저럭 쓸만한 결과를 내 보낼 수는 있다.

그 밖에 바이트순서마크가 붙은 UTF-16과의 구분이 자명하다거나(바이트 FE/FF가 나올 수가 없으므로) 하는 소소한 장점도 있지만 딱히 의도한 것 같지는 않다.

변종

바이트 순서 마크가 붙은 UTF-8

기술적으로, UTF-8은 항상 빅엔디안을 사용하기 때문에(높은 자리의 비트가 앞쪽 바이트에 온다) 바이트순서마크(U+FEFF)는 필요가 없다. 하지만 윈도 메모장(…) 같은 것들이 굳이 필요하지도 않은데 꾸준히 U+FEFF에 대응되는 UTF-8 문자열 EF BB BF를 맨 앞에 붙이는 경우가 많은데, 이게 보통 성가신 것이 아니다.

별로 장점은 없는 것 같지만 굳이 장점을 따지자면:

  • 첫 몇 바이트만 보고 이 문자열이 UTF-8인지 아닌지를 판단할 수 있다. 바이트 순서 마크가 없어도 UTF-8과 바이트 순서 마크가 붙은 UTF-16/UTF-32를 구분하는 건 어렵지 않지만, 다른 인코딩과 비교하기 시작하면(이를테면 ISO 8859-1) 약간의 휴리스틱이 필요할 수 있다. 이를테면 문자열에는 액센트가 붙은 문자가 일정 비율 이하로 나온다거나….

사실 이것도 그다지 장점같아 보이지는 않는데, 단점은 좀 많이 심각하다. 간단히 말하면 UTF-8 바이트 순서 마크는 UTF-8을 딱히 고려 안 하는 프로그램을 무참히 깨뜨린다. 예를 들어:

  • PHP 코드 앞에 바이트 순서 마크가 있으면 setcookie 같은 함수를 쓸 때마다 HTTP 헤더가 이미 보내졌다고(이미 바이트 순서 마크를 출력하면서 헤더도 보내 버렸기 때문에) 오류를 낸다.3) 비슷하게 실행 도중에는 아무 출력도 내지 않아야 하는 라이브러리를 짜거나 할 때도 엉뚱한 부수 효과를 낼 수 있다.
  • 유닉스 셔뱅 파일은 파일의 맨 첫 두 바이트가 #!여야 하는데, 바이트 순서 마크가 있으면 그 조건이 깨져서 동작하지 않게 된다.

그러하니 제발 좀 쓰지 말자. 참고로 vim파이썬에서는 UTF-8 바이트 순서 마크를 필요로 하는 거지같은 상황을 위하여 각각 'bomb' 옵션(…)과 utf-8-bom 인코딩을 별도로 제공하고 있긴 하다.

수정 UTF-8

Modified UTF-8. 자바에서 특히 많이 볼 수 있는데, 기본적인 목적은 UTF-8의 목적을 최대한 살리면서 U+0000도 널 바이트로 인코딩되지 않게 하자에 있다. 따라서 수정 UTF-8에서는 U+0000을 00이 아닌 C0 80으로 인코딩한다. (다른 문자는 전혀 영향을 받지 않는다. 따라서 C0 81은 U+0001로 디코딩되는 게 아니고 여전히 오류이다.)

수정 UTF-8의 큰 장점은 내부적으로는 U+0000가 들어 가도 널로 끝나는 문자열에서 널 문자 때문에 문자열을 잘라 먹는 일이 없도록 하기 위해서일텐데, 정작 수정 UTF-8이 흔히 쓰이는 자바의 경우 어느 케이스에서나4) 문자열 길이를 앞에 붙이고 있다. -_-; 아마 내부적으로만 쓰고 있는 듯.

CESU-8

Compatibility Encoding Scheme for UTF-16: 8-Bit (UTR #26). 내부적으로 UTF-16을 쓰고 외부 표현으로 UTF-8을 쓰는 구현체 중 안타깝게도 UTF-8 구현을 좀 밥솥같이 또는 대강 대충 한 것들을 위한 호환용 문자 인코딩. CESU-8에서 U+10000 이상의 문자는 4바이트로 인코딩되는 게 아니라, 서로게이트 문자 두 개에 대응되는 3바이트 표현 두 개로 인코딩된다.

CESU-8은 기본적으로는 밥솥같은 구현체를 위한 안타까운 인코딩이지만, 기술적으로 아주 장점이 없는 것은 아니다. 내부적으로 UTF-16을 사용할 경우 CESU-8의 바이트 기반 정렬 순서는 UTF-16 정렬 순서와 동일하고, 변환 또한 "진짜" UTF-8을 쓸 경우보다 눈꼽만큼 더 쉬워진다(적어도 덧셈 한 번은 줄어 든다).

1) The Unicode Standard, Version 6.0, Section 2.5 (pp. 27–28).
2) 구글에 따르면 2007년 말에 과반수를 넘었다.
3) 물론 따지고 보면 ob_start 따위로 출력 버퍼링을 켜 놓는 게 상식적인 PHP 프로그래머의 자세이겠으나;

도쿠위키DokuWiki-custom(rev 9085d92e02)을 씁니다.
마지막 수정 2011-05-30 18:25 | 외부 편집기