UTF-8(Unicode Transformation Format – 8bit) 인코딩은 유니코드 문자를 표현하기 위한 인코딩 방식입니다. 현재 가장 널리 쓰이는 방식입니다.
UTF-8 인코딩은 가변 길이 인코딩으로, 1개 문자당 최소 1바이트에서 최대 4바이트(이론상 6바이트)까지의 길이를 갖습니다. 또한, UTF-16과는 달리 바이트 순서의 차이(Big Endian vs Little Endian)로 인한 혼란이 없다는 장점이 있고, 아스키(ASCII) 코드와의 호환성을 갖추고 있습니다.
UTF-8에서 1바이트로 표현될 수 있는 문자는 기본 아스키 문자와 동일한 U+0000부터 U+007F입니다. 예를 들어, 기본 아스키 문자 코드에서 0x5A로 표현되는 ‘Z'(U+005A)라는 글자의 경우 UTF-16에서는 ‘0x5A 0x00′(Little Endian) 또는 ‘0x00 0x5A'(Big Endian)으로 표현되지만, UTF-8에서는 ‘0x5A’ 하나로 표현됩니다.
이 범위를 벗어나는 글자는 2바이트 이상으로 표현되는데, 다음과 같이 표현됩니다.
- 먼저 첫 번째 바이트는 2바이트임을 표시하기 위해 맨 앞 3비트를 110으로 합니다.
- 뒤에 오는 바이트는 앞 바이트에서 이어짐을 표시하기 위해 맨 앞 2비트를 10으로 합니다.
- 앞의 연속된 두 바이트에서 고정 비트를 제외하고 남는 11비트에 유니코드 포인트 값을 뒤쪽 비트부터 채워 넣습니다.
이 방식으로 하면 2바이트로 표현 가능한 유니코드 포인트는 U+0080부터 U+07FF까지입니다. 예를 들어, 유니코드 포인트 값이 U+03A3인 그리스 문자 대문자 시그마(Σ)를 UTF-8로 표현하자면, 0x3A3을 2진수로 변환하면 0b1110100011이고 이것을 뒤에서부터 6비트씩 끊으면 1110 100011이 되므로, 110xxxxx 10xxxxxx의 x 자리에 이 비트를 집어넣으면 11001110 10100011, 이를 16진수로 표현하면 0xCE 0xA3이 됩니다. 즉 Σ의 유니코드 포인트는 U+03A3이지만 UTF-8에서는 0xCE 0xA3으로 저장되는 것입니다.
3바이트인 경우 첫 바이트의 맨 앞에 1이 하나 더 늘어나 1110xxxx의 형식이 되고 그 뒤에 10xxxxxx 형식의 바이트가 두 개 붙습니다. 가변 비트가 11개에서 16개로 늘어나므로 3바이트로 표현 가능한 유니코드 포인트 범위는 U+0800에서 U+FFFF까지입니다. 즉 기본 다국어 평면(BMP; Basic Multilingual Plane)에 속하는 문자는 모두 1문자당 3바이트 이내로 저장 가능합니다.
예를 들어, 유니코드 포인트 값이 U+D55C인 한글 완성자 ‘한’을 UTF-8로 나타내고자 한다면, 위와 같은 방법으로 0xD55C를 2진수로 변환하고 뒤에서부터 6비트씩 끊으면 1101 010101 011100이 되고 이를 1110xxxx 10xxxxxx 10xxxxxx 자리에 채우면 11101101 10010101 10011100, 이를 다시 16진수로 변환하면 0xED 0x95 0x9C, 따라서 UTF-8에서 ‘한’이라는 글자는 0xED 0x95 0x9C로 저장됩니다.
U+10000 이상의 확장 평면은 4바이트가 되고 역시 첫 바이트가 11110xxx, 이어지는 10xxxxxx 형식으로 이어지는 바이트가 3개가 됩니다. 4바이트로 표현 가능한 범위는 U+10000부터 U+1FFFFF까지입니다. 이런 식으로 이론상 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 형식의 6바이트까지 가능해 U+7FFFFFFF까지 표현할 수 있지만, 유니코드 표준에서는 U+10FFFF까지만 사용하도록 정해져 있어서 실제로는 4바이트까지만 사용하게 됩니다.
알기 쉽게 표로 정리하자면,
Z (U+005A) 1011010 |
||
바이트 | 비트 | Hex |
---|---|---|
1번째 | 01011010 | 0x5A |
Σ (U+03A3) 1110 100011 |
||
바이트 | 비트 | Hex |
---|---|---|
1번째 | 11001110 | 0xCE |
2번째 | 10100011 | 0xA3 |
한 (U+D55C) 1101 010101 011100 |
||
바이트 | 비트 | Hex |
---|---|---|
1번째 | 11101101 | 0xED |
2번째 | 10010101 | 0x95 |
3번째 | 10011100 | 0x9C |
위와 같이 됩니다.
그런데, 만약 여기서 멀티바이트 구조의 모순이 발생한다면 어떻게 될까요? 예를 들어, ‘안녕하세요’를 UTF-8로 저장하면
EC 95 88 / EB 85 95 / ED 95 98 / EC 84 B8 / EC 9A 94
이렇게 저장됩니다. 그런데 만약 가운데 한 바이트를 잘라버리면,
EC 95 88 / EB 85 95 / ED 95 / EC 84 B8 / EC 9A 94
‘하’ 부분의 첫 번째 바이트는 1110xxxx 형식으로 3바이트임을 표시하고 있는데 뒤에 하나가 지워져서 2개가 이어져야 할 10xxxxxx 바이트가 1개인 상황입니다. 이 경우에는 디코딩 오류를 일으키거나 아래와 같이
안녕�세요
이와 같이 오류가 난 ‘하’ 부분만이 대체 문자(U+FFFD)로 변해서 표출되고 나머지 글자는 정상적으로 표시됩니다.
EC 95 88 / EB 85 95 / 95 98 / EC 84 B8 / EC 9A 94
이번에는 위처럼 ‘세’ 부분의 첫 바이트가 지워져서 앞의 ‘하’ 부분의 10xxxxxx 바이트가 2개에서 4개로 늘어나게 될 경우
안녕하��요
이와 같이 첫 바이트가 1110xxxx이므로 뒤에 이어지는 10xxxxxx 바이트는 2바이트까지만 인식하고 나머지 2바이트는 오류로 보아 대체 문자로 변해서 표출됩니다.
이처럼 UTF-8은 글자간 바이트 경계가 명확하다는 장점도 가지고 있습니다.