====== UTF-8 ====== 8-bit [[국제문자집합|UCS]] Transformation Format (8비트 국제 문자 집합 변환 포맷). [[유니코드]] [[문자인코딩]]((The Unicode Standard, Version 6.0, [[http://www.unicode.org/versions/Unicode6.0.0/ch02.pdf|Section 2.5]] (pp. 27--28).)). UTF는 전송·저장에 적합한 유니코드의 표현 방법을 이르는데, UTF-8은 그 중에서도 가장 널리 쓰이는 축에 속한다. [[Ken Thompson]]과 [[Rob Pike]]가 [[Plan 9]]에서 사용할 목적으로---그래서 원래 이름은 **FSS-UTF**(File System Safe UTF)이었다---만들었는데 그 설계가 매우 깔끔하고 효율적이라 온갖 곳에서 다 쓰이고 있다. 보통 메모리 상에 들어 있는 유니코드 문자열([[UTF-16]]이나 [[UTF-32]]인 경우가 대부분) 빼고는 다 UTF-8을 써도 된다고 해도 과언이 아닐 정도. [[웹]]에서도 웬만한 기존 문자 인코딩보다 많이 쓰이고(([[구글]]에 따르면 [[http://googleblog.blogspot.com/2008/05/moving-to-unicode-51.html|2007년 말]]에 과반수를 넘었다.)) 기존 문자 인코딩의 흔한 문제점([[깨진문자]] 등)을 해결할 수 있어서 꾸준히 대체되는 추세. ===== 구조 ===== 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'', ''1110xxxx'' 및 ''11110xxx'' 바이트 다음에 나오는 "나머지" 바이트. 한 개의 나머지 바이트는 코드 포인트의 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에 대응). * 바이트 ''F0'' 뒤에는 ''80''부터 ''8F''까지의 바이트가 올 수 없다(U+0000~FFFF에 대응). * 바이트 ''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]] 헤더가 이미 보내졌다고(이미 바이트 순서 마크를 출력하면서 헤더도 보내 버렸기 때문에) 오류를 낸다.((물론 따지고 보면 ''ob_start'' 따위로 출력 버퍼링을 켜 놓는 게 상식적인 PHP 프로그래머의 자세이겠으나;)) 비슷하게 실행 도중에는 아무 출력도 내지 않아야 하는 라이브러리를 짜거나 할 때도 엉뚱한 부수 효과를 낼 수 있다. * 유닉스 [[셔뱅]] 파일은 파일의 맨 첫 두 바이트가 ''#!''여야 하는데, 바이트 순서 마크가 있으면 그 조건이 깨져서 동작하지 않게 된다. 그러하니 제발 좀 쓰지 말자. 참고로 [[vim]]과 [[파이썬]]에서는 UTF-8 바이트 순서 마크를 필요로 하는 거지같은 상황을 위하여 각각 ''%%'bomb'%%'' 옵션(...)과 ''utf-8-sig'' 인코딩을 별도로 제공하고 있긴 하다. ==== 수정 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이 흔히 쓰이는 자바의 경우 어느 케이스에서나(([[자바클래스파일]]의 [[http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html#7963|문자열 포맷]], [[http://download.oracle.com/javase/7/docs/api/java/io/DataInput.html#readUTF()|java.io.DataInput]] 등의 [[직렬화]] 구현 등등 포함)) 문자열 길이를 앞에 붙이고 있다. -_-; 아마 내부적으로만 쓰고 있는 듯. ==== CESU-8 ==== Compatibility Encoding Scheme for UTF-16: 8-Bit ([[http://www.unicode.org/reports/tr26/|UTR #26]]). 내부적으로 [[UTF-16]]을 쓰고 외부 표현으로 UTF-8을 쓰는 구현체 중 안타깝게도 UTF-8 구현을 좀 밥솥같이 또는 대강 대충 한 것들을 위한 호환용 문자 인코딩. CESU-8에서 U+10000 이상의 문자는 4바이트로 인코딩되는 게 아니라, 서로게이트 문자 두 개에 대응되는 3바이트 표현 두 개로 인코딩된다. CESU-8은 기본적으로는 밥솥같은 구현체를 위한 안타까운 인코딩이지만, 기술적으로 아주 장점이 없는 것은 아니다. 내부적으로 UTF-16을 사용할 경우 CESU-8의 바이트 기반 정렬 순서는 UTF-16 정렬 순서와 동일하고, 변환 또한 "진짜" UTF-8을 쓸 경우보다 눈꼽만큼 더 쉬워진다(적어도 덧셈 한 번은 줄어 든다). {{tag>문자인코딩}}