IT 이야기/유니코드2009. 2. 25. 07:24

이전글에 이어서..

사실 서구권보다는 우리나라가 유니코드에 대해 좀더 절실하지만 영어를 잘하는 개발자의 비율이 낮기때문인지 잘못 알려진 사실들이 많이 있다. 그 중에 대표적인 것은 인코딩과 문자셋의 차이에 대한 오해와 유니코드는 2byte라는 미신이 있다.("유니코드는 2byte"라고 잘못 소개한 책들 "만"의 탓이라고 돌릴수는 없다.) 상식적으로 문자처리에 대해 그렇게 과도한 지식을 요구하는 체계를 만들지는 않으므로 기본 상식선에서 쉽게 접근할수 있는 문제이다.

유니코드에 대한 미신을 깨기위해 가장 먼저 알아야 할것은 캐릭터 셋의 개념이다. 이전까지 euc-kr이 인코딩 그 자체였던 것에 비해 유니코드란 말은 인코딩이 아니라 일종의 문자 처리의 철학에 가깝다. 'A'를 UCS-2로 표현하면 0x0041인데 이때의 0041은 실제 디스크에 해당 바이트로 저장된다는 뜻이 아니라 그냥 관념적인 매핑이다. 굳이 비유하자면 일종의 인터페이스라고 생각할 수도 있다. 'A' 글자를 UCS2 매핑맵에 따라 0x0041이라는 코드 포인트로 매핑시켰을뿐 실제 디스크에 어떻게 저장이 되는가는 인코딩 방법에 따라 다르다. UCS2라는 것은 글자를 2byte의 코드 포인트로 표현한다는 것일뿐 실제 'A'라는 글자가 2byte라는걸 뜻하는 것은 아니다. 만약 'A'를 UCS4로 표현하면 0x00000041이 된다.


좀더 얘기를 진행하기 전에 왜 이런짓을 할까? 그냥 글자대 바이트코드 1:1매핑이 편하지 않을까 하는 의문이 들수 있다. 첫번째 이유는 의도와 구현을 분리시키는 설계의 기본이념에 따라 인코딩의 구현에 상관없이 글자를 정의하는데 있다. 만약 우리가 글자의 캐릭터 셋을 사용하지 않는다면 "에이"의 바이트코드는 어떻게 되지? 라고 누군가가 묻는다면 "에이"라고 발음되는 다른 문자와 혼란될 염려도 있고 그 에이가 만약 영어라고 해도 대문자인지 소문자인지도 확인해야 하며 언젠가 먼 미래에 혹은 과거에 에이는 다른 의미를 가지게 될지도 모른다. 그것보다는 0041문자의 바이트 코드는 어떻게 되지? 라고 묻는게 훨씬 더 손쉽다. 수학은 만국공용의 언어이기 때문이다. 그리고 이렇게 분리되었을 경우 인코딩과 디코딩의 구현의 방법이 변하더라도(다음에 얘기할 빅엔디안 -리틀엔디안의 경우를 보면 이런일이 이해가 될것이다.) 글자의 정의 자체는 변하지 않아도 되는 장점이 있다. 


 이렇게 모든 글자의 매핑 맵을 만들어서 문자별 코드 point을 지정한게 캐릭터 셋이다. 만약 UCS4의 코드포인트 수인 2^32보다 많은 문자를 정의해야 한다면(현재로선 충분해보이지만 이를테면 수만종의 외계인을 만나다던가 수만종의 지구 생물의 언어체계를 이해하게 된다던가) UCS8이라는 매핑맵을 만들어서 추가적인 코드 포인트를 지정하면 그만이기 때문에 이론적으로 유니코드가 정의할 수 있는 글자 개수에는 제한이 없다.

이러한 관념적인 숫자인 0x0041을 어떻게 메모리 혹은 디스크에 저장할 것인가에 대한 - 즉 다시 말해서 인코딩 - 얘기는 비교적 익숙한 UTF-8, UTF-16, UTF-32 등이 있다. 과거 ASCII 시절에는 문자를 표현하는게 복잡하지 않았기 때문에 캐릭터 셋과 인코딩은 굳이 구별할 필요가 없었다. 이를테면 A는 0x41이고 저장장치에 표현될때도 41이라고 저장하면 그만이었다. 그러다가 세상이 복잡해지고 컴퓨터로 표현해야 할 문자도 많아지면서 관념적인 캐릭터 셋과 물리적인 인코딩이라는 것을 구분하여 쓰기 시작했다. 인코딩을 구현하는 입장에서 보면 캐릭터 셋은 문자의 종류와 각 문자들을 구분할 수 있는 기준이기 때문에 캐릭터 셋의 존재를 인지해야 한다.

유니코드의 캐릭터 셋은 대표적으로 UCS2와 UCS4 두가지가 있다. UCS2는 16진수 2개로 코드 포인트를 지정했고 UCS4는 16진수 4개로 코드포인트를 부여하였다. 이를테면 UCS2에 따르면 영문자 A는 '0x0041'이다. 이제 영문 대문자 A라고 부르지 않고 '0x0041'번 문자라고 불러도 된다. 이 '0x0041' 문자가 실제 디스크나 메모리에 어떻게 저장될지는 인코딩을 구현하는 쪽에서 알아서 할일이다. 다만 캐릭터 셋은 서로 구별되는 이만큼의 문자가 있다는 것을 알려주는 것이다. 16진수 2자리로 표현할 수 있는 최대 경우의 수는 65536이기 때문에 UCS2 에는 이론상으론 최대 65536자의 코드 포인트를 지정할 수 있다. 인긴이 사용하는 언어는 아주 많아 보이지만 대부분이 표음언어이기 때문에 일명 CJK언어(중국,일본,한국)의 표의언어를 제외하면 생각보다 많지 않기 때문에 대충 65000 의 코드 포인트로도 표현이 가능하다.

이를테면 한글의 경우 초성19* 중성21* 종성28를 하면 고작(?) 경우의 수는 11172이고 한문의 경우에도 대략 4만자 일본어의 대략 몇천자를 제외하면 다른 표음언어(라틴어-영어,그리스어,키릴어,아르메니아어,히브리어,아랍어,시리아어,벵골어,몽골어 등)를 모두 합쳐도 60000여자 안팍이다. 이를 모두 모아서 BMP(Basic Mulitilingual Plane)이라고 부른다. (2^16개의 코드포인트들을 하나의 Plain이라고 부른다.)


60000자 정도로도 표현할 수 있다면 UCS4의 4byte의 표현의 캐릭터 셋이 존재해야 하는 이유는 무엇일까? 첫째 이유는 현재 사용하지는 않지만 특수한 지명이나 이름에서만 사용되는 한자의 고어나 연구 목적으로 사용되는 다양한 언어들의 고어 - 이를테면 수메르 언어를 쓰지는 않지만 연구목적으로 수메르 언어를 표현해야 할 때는 있다. 추가적으로 수식 기호, 음표등의 다양한 문자들까지 합치면 6만자가 넘는다. 편의를 위해 이런 문자들을 16개의 Plane으로 만들었는데(앞서 말했다시피 하나의 Plane는 65536의 코드 포인트를 가지도록 구분) 이를 보충언어판(SMP, Supplementary Multilingual Plane )이라고 부른다. 

그렇다고 SMP의 18 * 65536의 모든 코드 포인트가 정의되어 있는 것은 아니다. Plane은 단지 비슷한 것끼리 묶어놓은 그룹이며 실제로 SMP에 정의된 코드포인트는 그리 많지 않다. 또 이후 새로운 수학기호가 생길지도 모르니 수학기호와 관련한 Plane을 비워두기 때문에 Plane은 일종의 Grouping 이라고 생각하면 된다. 


고작해야 16*65536 (2^4*2*16 = 2^20)의 SMP와 기존의 BMP를 합쳤을때( (16 + 1) * 2 ^ 16 는 대략 2^21 )  4바이트(2^32)는 과해 보일지 모르지만 기본적으로 컴퓨터를 하는 사람들은 짝수를 좋아하고 우리가 언제 외계인을 만나서 외계인의 언어를 디스크에 저장해야 할때도 없으리라고 할 수 없지는 않은가? 머 어쨌든 실제 UCS4의 많은 공간은 17개의 Plane를 제외하면 비워져 있다. 물론 코드 포인트들은 앞에서 부터 촘촘히 채워져 있는 건 아니기 때문에 각 Plane에도 중간 중간 비어있는 코드 포인트 들이 있다. 여기서 다시 256개의 Plane을 묶은걸 하나의 Group이라고 부르고 현재 17개의 BMP+SMP는 00번 Group에 속해 있다.



먼저 UTF-16 인코딩에 대해 먼저 알아보자.

처음에는 그냥 코드 포인트의 숫자를 그대로 두 바이트로 저장하는 것은 어떨까 하는 누구나 할수 있는 생각을 하게 되는데 이 생각이 잘못 전해져서 유니코드는 2byte라는 미신을 만든다. 기본적인 생각은 '0x0041'이라는 코드포인트를 디스크에 '00 41'로 저장하자 라는 생각이다.

그러나 앞에서 말했다시피 SMP에 지정되어 있는 (UCS2에서는 지정되지 않았지만 UCS4에 지정되어 있는) 코드포인트를 가지는 글자들은 어떻게 처리할까? SMP영역의 글자들을 표현하기 위해 DBCS에서 했던 편법을 응용해서 적용한다. 즉 2byte가 만약 D800 - DBFF(이 영역은 UCS2에서 원래 비어있는 영역이다.) 사이에 있으면 다음 2byte와 같이 읽어서 하나의 글자를 처리하는 방식이다. 이 경우 다음 2byte는 DC00..DFFF(이것도 역시 UCS2에서는 비어있는 영역이다.) 사이에 있어야 한다. 즉 D800 - DBFF + DC00..DFFF의 4byte로 하나의 글자를 인코딩하게 된다. 앞의 범위를 상위 대행코드 뒤의 범위를 하위 대행코드라고 하는데 이 경우 총 표현할수 있는 글자수는 2^10*2^10 = 2^20 의 경우의 수를 가지게 된다. (D800 에서 DBFF 사이의 경우의 수는 2^2*2^8=2^10이다.) 2^20 = 2^4 * 2^16 = 16 * 65636 이므로 16개의 SMP는 이렇게 4바이트를 사용하여 표현할 수 있게 되었다.

상하위 대행 코드 영역으로 2^10*2영역은 비어 있어야 하기 때문에 실제 BMP의 표현 가능한 갯수는 2^16 -  2^10*2가 되지만 원래 BMP에도 충분한 여유 공간이 있기 때문에 상관이 없다. 그래서 UTF-16인코딩은 BMP에 있는 문자들은 2byte로 SMP에 있는 문자들은 4바이트로 표현을 한다. 이렇게 UTF-16 인코딩은 현재로서는 17개의 BMP+SMP만 표현하도록 만든 인코딩이기 때문에 외계인의 언어를 저장하는데 이 인코딩은 사용할 수 없지만 최소한 지구내에서 쓸때는 충분하다.


UTF-16에 대한 얘기를 끝내기전에 처음 들었다면 당황스런 얘기일지 모르지만 실제로 초기 구현가는 특정 CPU가 가장 빨리 동작할 수 있는 모드에 맞춰 '0x0041'이라는 코드 포인트를 '00 41'과 '41 00' 두가지 모드 형태로 사용하고 싶어했고 그러다 보니 유니코드 문자열 시작부분에 FE FF 를 저장하는 관례가 생겼다. (FE FF는 순서대로 읽은 빅 엔디안 방식이고 FF FE는 순서를 뒤집어서 읽는 리틀 엔디안 방식이다.)

UTF-16 인코딩을 사용하는 대표적인 사례는 바로 자바의 문자열 처리방법이다. "1Ab가햏"라는 글자의 경우 자바는 메모리상에 실제로 "FE FF 00 31 00 41 00 62 AC 00 D5 8F"와 같이 저장한다. 처음 FE FF는 엔디안이고 1 = 00 31, A = 00 41, 가 = 00 62 AC, 햏 = 00 D5 8F로 표현된다. 자바가 UTF-16 인코딩을 사용하는 이유를 추측하자면 아마도 프로그래머들이 사용하는 대부분의 글자는 2byte로 표현이 가능해서 - SMP인 수메르 언어로 주석을 다는 별종은 없을테니까 말이다.-  다음에 얘기할 1-6 가변 바이트인 UTF8보다 처리가 쉽고 또 BMP의 글자는 2byte 고정바이트이기 때문에 그만큼 처리 속도에 우위를 가지기 때문이라고 추측된다. 


정리하자면 UTF-16은 2byte-4byte의 가변 처리 인코딩이며 DBCS에 했던 비슷한 방법인 대행코드라는 방식을 사용하기 때문에 SMP에 있는 글자의 경우 UCS4의 코드 포인트와 실제 저장되는 바이트 숫자와는 다르다. 만약 UFT-16으로 인코딩된 글자의 UCS-4의 코드 포인트를 보고 싶다면 (상위대행코드-0xD800)*(하위대행코드-0xDC00) + 0x10000 의 간단한 수식을 통해 확인할수 있다.

UTF-16의 인코딩은 비교적 이해하기가 손쉽지만 눈에 띄는 단점이 있다. 
첫번째 이전의 ASCII로 저장된 레거시 문서와 호환이 되지 않는다. 
두번째 BMP+16 SMP(고어+수학기호, 음표 등등)의 문자만 인코딩이 가능하다. 앞서 얘기했듯이 앞으로 생길지도 모르는 외계인의 언어를 저장할수는 없다. 



이제 UTF-8 인코딩에 대해서 알아보자

앞에서 UCS-4는 매핑셋이기때문에 총 표현가능한 코드 포인트는 2^32이지만 UTF-16의 인코딩은  (2^16 -  2^10*2) + 16 * 2^16 대충 17 * 2^16 만을 표현할 수 있기 때문에 모든 UCS-4의 글자를 표현하지 못하는 단점이 있고(물론 외계인과 조우하지 않는다면 최소한 지구상의 과거와 현재 존재하는 모든 언어는 표현이 가능하지만 말이다.) 라틴어 계열의 프로그래머들은 'ABC'를 저장하기 위해 '00 41 00 42 00 43'와 같이 사용 되는데 절대 다수인 라틴어(영어) 계열의 프로그래머들은 수많은 00 들을 낭비라고 생각했다. 

그냥 참고 말지 라는 단순히 낭비의 문제를 넘어서 UTF-16은 과거의 ASCII로 사용되었던 문서와의 호환문제가 있었기 때문에 다른 인코딩을 만들게 되는데 그게 UTF-8이다.

여기에도 DBCS에서 사용되었던 아이디어(사실 이 아이디어는 네트워크의 서브넷 마스크에도 쓰인다.)를 조금 바꿔서 첫 byte의 비트를 사용해 (0xxxxxxx, 110xxxxx, 1110xxxx 등) 추가 글자를 인코딩, 디코딩을 한다.

UTF-8과 UCS-4간의 변환 규칙
UCS-4 UTF-8

위의 표에서 보다시피 UTF-8로 인코딩된 파일을 디코딩할경우 바이트의 앞의 비트를 읽어서 이게 몇바이트 글자인지 확인하고 해당 바이트만큼 묶어서 읽게된다. 예컨데 첫번째 비트가 0이라면 해당 글자는 ASCII 문자라는 것을 의미하고 뒤의 7비트를 읽어서 표현한다. 처음 3비트가 110이면 두 바이트를 묶어서 글자를 읽는다. 만약 처음 4비트가 11110이면 표현할수 있는 가지수는(x의 숫자를 모두 더하면 된다) 2^21이고 이는 앞에서 말한 BMP와 SMP의 합인 17 * 2^16보다 크면서 최소의 2의 승수이다. 따라서 UTF-8인코등은 4byte로 17개의 Plane을 표현할 수 있다. 

UTF-8로 UTF-16처럼 17개의 BMP+SMP만 표현하고자 한다면 빨간색으로 칠한 부분을 0x0010FFFF로 변경하고 나머지는 삭제해야 정확한 유니코드의 UTF-8 인코딩이라고 할 수 있다.(우리는 아직 우주인과 접축하지 않아서 17개의 Plane을 제외하면 모두 영역만 구분하였을뿐 모두 비어있다.) 다만 이를 모두 표현한 것은 6바이트까지 확장하면 UTF-8로도 UCS-4의 전역을 인코딩할 수 있음을 보여주기 위한 것이다.

UTF8은 유니코드에서 가장 널리 쓰이는 인코딩이다. 기존의 BMP에 있던 한글들은 UTF-16에서 2byte로 표현되던게 UTF-8에서는 3byte로 표현되지만 절대 다수인 ASCII 코드들이 UTF-16에서 2byte로 표현되던 것이 UTF-8에서는 1byte로 표현되고 이는 다시 말해서 기존 과거의 레거시 아스키 문서들을 UTF-8로 디코딩해도 아무런 문제가 없기 때문이다.


UTF8의 가장 큰 장점은 이전의 레거시 문서와 호환이 된다 이지만 단점은 대부분의 CJK가 3byte로 표현된다는데 있다. 
이게 왜 단점이 되냐면 기존의 UTF16 인코딩에 비해 평균적으로 저장공간이 더 많이 소모되고 length 체크시에 다소 더 복잡하다는데 있다. 이를테면 오라클이 UTF8을 사용할때 varchar2(4000)이라는 의미는 4000byte라는 뜻이므로 CJK와 ASCII가 혼합된 문자열의 저장시 length를 정확히 체크해야 한다. 



UTF-32는 이해하기 쉽다.

다만 UTF-32는 고정길이인 4바이트로 모든 유니코드를 표현하기 때문에(UTF-16은 2혹은 4byte, UTF-8은 1-4(6)으로 표현한다.) 그냥 UCS-4의 매핑셋을 일대일 매핑시켜서 현재는 17개의 언어판만을 대상으로 하는 UCS-4의 코드 포인트만을 사용한다. 즉 표준상 즉 UTF-32의 인코딩 영역은 0x00000000에서 0x0010FFFF로 제한된다.(이 제한을 무시하면 UCS-4와 동일하기때문에 UCS4의 경우 인코딩의 의미로도 사용하기도 한다. UCS2의 경우에도 BMP만을 사용하기로 한다면 그냥 1:1 매핑시켜서코드 포인트를 바로 바이트로 표현해도 되기 때문에 인코딩의 하나의 방법이 될 수는 있다. 다만 이렇게 말하면 글의 문맥상 의미를 읽어야 하기 때문에 이해하기 어려워서 그냥 UCS2와 UCS4는 캐릭터 셋이라고 생각하는게 맘이 편하다. ) 그러나 실제로 UTF-32 인코딩을 사용하는 사례는 별로 없다. 개념적으로는 간단하지만 저장공간의 낭비가 다른 인코딩에 비해 너무 심하기 때문이다.




정리하자면 외계인과의 조우 가능성등을 이유로-ㅅ- UCS-4의 코드 포인트를 정해놓았다. (산술적인 경우의 수는 42억이지만 인간은 분류하기를 좋아하기때문에 2^16의 코드포인트를 가지는 256개의 언어판을 하나의 Group을 묶어 128개의 Group을 만들어 놨으므로 실제 UCS4에 정의된 코드 포인트는 128*256*2^16 = 2^31이다. 128개의 Group이 있으며 한개의 Group에는 256개의 Plane이 있으며 하나의 Plane에는 2^16개의 코드 포인트를 정해 놓았다. ) 이때 'A'를 UCS-2의 코드 포인트로 표현하면 '0x0041' 이지만 실제 메모리나 디스크에 저장될때는 UTF-8, UTF-16, UTF-32 같은 인코딩 방법에 따라 다르다.

character set은 문자에 숫자코드를 부여한 문자집합이다. 이 상태는 아직 논리적 상태로 컴퓨터에서 어떻게 표현되는가는 정해지지 않는 상태이다. encoding는 물리적으로 컴퓨터에서 어떻게 표현되는가까지가 정해진 상태의 문자 집합이다. 예컨데 KSC5601 완성형(KSX1001로 1987년 이름이 변경)은 charset이고 이걸 UNIX에서 encoding한게 euc-kr, DOS에서 encoding한게 codeset 949이다.

character set과 encoding은 오래전에는 같은 뜻이었다. 그러다가 시스템의 종류도 많아지고 Unicode 지원 등이 생기면서 논리적인 character set과 물리적인 encoding이 분리된 의미로 사용되었다. 그리고 이러한 이유로 혼란이 야기되었다-ㅅ-







이 글을 쓰며 흥미있었던건 5년전에 Unicode란 무언인가에 대해 말하기 위해 무려 4시간짜리 세미나를 했다는 거다. 무언가를 더 잘 이해하면 더 짧고 쉬운 말로 적을 수 있다는걸 새삼 깨닫게 되었달까..


'IT 이야기 > 유니코드' 카테고리의 다른 글

다시 쓰는 UTF  (0) 2009.06.12
Unicode와 Database  (0) 2009.03.01
Global Software - Unicode와 Programming  (0) 2009.02.26
Global Software  (0) 2009.02.22
Posted by bleujin