'IT 이야기/유니코드'에 해당되는 글 5건

  1. 2009.06.12 다시 쓰는 UTF
  2. 2009.03.01 Unicode와 Database
  3. 2009.02.26 Global Software - Unicode와 Programming
  4. 2009.02.25 Global Software - Unicode
  5. 2009.02.22 Global Software
IT 이야기/유니코드2009. 6. 12. 16:14

UTF 얘기는 이미 충분히 글을 적었음에도 다시 포스팅을 하는 이유는 사람들이 자주 헛갈리는 캐릭터셋(charset)과 인코딩(enecoding)의 차이를 이야기 하기 위해서이다. 

"A"라는 글자가 컴퓨터에 이진수로 표현되어 저장된다는 것은 누구나 알고 있을 것이다. 중요한건 2단계를 거치는 것이 중요하다. 첫번째는 "해석"이고 두번째가 "저장"이다. 

첫번째 "해석"이란 컴퓨터가 아닌 사람을 위해 존재한다. "A" 글자를 저장해야 한다고 할때 글자 "A"라는 의미를 어떻게 전달해야 할까가 문제의 시발점이다. 다른 사람에게 "A"가 어떻게 저장되지? 라고 묻는건 올바르지 않다. 좀더 정확하게 말하자면 "2009년 현재 미국에서 사용하고 있는 알파벳의 대문자 A"라고 전달해야 한다. 번거롭기 그지 없다. 게다가 그 다른 사람이 사는 문화권에서는 "에이" 라고 불리는 다른 문자가 있을지도 있을지도 모른다. 원래 말이라는건 오해하기 쉬운 법이다. 

그래서 좀더 쉬운 전달을 위해 모든 문자에 숫자를 할당했다.(숫자은 전세계 모든 사람의 공용어 역할을 한다.) 이제 다른 사람에게 (십진수로) 65(16진수로 표현하면 0x41)번 문자는 어떻게 저장하지? 라고 물으면 된다. 이렇게 해석단계에서 사용하는, 문자에 번호를 매겨놓은게 charset (캐릭터 셋)이다. 컴퓨터가 발명된 초기부터 이렇게 별도의 해석 단계가 있었던건 아니었다. 컴퓨터가 처음 발명된 초기에는 해석 == 저장이었기 때문에 65번 글자인 'A'는 65번으로 저장한다 였다. 초기에는 128개(2^7)에만 번호를 할당했고 저장하였는데 구미지역에서는 이정도면 충분하였다. 

다시 말해서 캐릭터 셋은 해당 캐릭터 셋이 표현가능한 문자의 일종의 단순한 매핑 테이블이다. 이를테면 한글 "가"라는 글자는 KSC5601에서 "0116" 번으로 할당되어 있다. 이제 "0116" 에 해당하는 글자(즉 한글 "가")를 어떻게 컴퓨터의 이진 코드화 시키는가가 인코딩이다. 앞서 해석 - 캐릭터셋에 매핑된 숫자를 읽기 -는 논리적이며 저장 - 인코딩 -은 물리적 과정이기 때문에 서로 같지 않고 당연히 "0116" 글자가 0116 라고 저장되리라는 것을 기대하거나 보장할 수 없다. 

왜냐하면 캐릭터 셋은 논리적 과정이라 얼마든지 새로운 캐릭터 셋을 만들어도 상관이 없지만(물론 이 경우 다양한 단체의 알력싸움에 의해 정해진다.) 인코딩은 물리적 단계이며 이경우 가장 중요한 것은 호환성이다. 예컨데 "A"라는 글자는 KSC5601에서 "3305"번에 할당되어 있는데 디코딩은 인코딩의 역함수이기 때문에 기존에 ASCII로 "0x40"으로 저장된 코드를 "A"라고 읽을 수 없다. 따라서 캐릭터 셋의 번호대로 인코딩이 되지는 않고 실제로는 캐릭터셋의 해쉬 함수 인코딩을 하게 된다. 

KSC5601의 해쉬 함수를 통해 (KSC5601은 일반적으로 캐릭터셋의 의미를 가지지만 때로는 인코딩을 의미하기도 한다. 그래서 이전의 책이나 글에서는 문맥을 통해 구별하는 수밖에 없으며 이는 많은 혼란의 요인이 되었다.) "0116"글자는 "0xAC00"로 인코딩(즉 computer에는 AC00로 저장이 된다.) "3305" 글자는 "0x40"으로 바뀌어서 저장이된다. 문제는 디코딩을 할때 "0xAC00"  코드를 읽어서 화면에 "가"라고 보여줄 수 있느냐이다. 왜냐하면 파일에는 해당 파일의 캐릭터 셋에 대한 정보를 저장하기 않기때문에 이 파일을 읽을때 "0xAC00" 가 정말 "가"인지를 확인할 수 없기 때문이다. 어쩌면 다른 캐릭터 셋의 인코딩으로 "0xAC00"는 다른 글자일지도 모르기 때문이다. (어쩌면 초기에 구미에서 컴퓨터를 발명하면서 파일에 캐릭터 셋을 저장하지 않는 것이 문제의 시발점인지 모른다. )

파일을 인코딩한 해쉬 함수의 역함수는 당연히 캐릭터 셋에 종속이 되기 때문에 해당 파일의 캐릭터 셋을 모르고서는 해당 파일이 제대로 읽을 수 없다. 앞문장의 "제대로" 라는 뜻은 "0xAC00" 라는 코드를 읽어도 해당 파일의 캐릭터 셋을 알지못하면 이를 "가"로 제대로 화면에 보여주지 못한다는 이야기 이다. 

만약 자신의 컴퓨터에서 혼자만 쓰는 파일이라면 상관이 없다. 각 컴퓨터에는 기본 캐릭터셋이 지정되어 있기 때문에 아무런 추가 작업을 하지 않는 이상 기본 캐릭터 셋으로 쓰고 기본 캐릭터 셋으로 읽으면 된다. 그러나 A 컴퓨터에서 작성한 파일을 B 컴퓨터로 전달하는 경우(이메일을 쓰거나 웹에 글을 올리거나 하는 경우도 마찬가지이다. )에 해당 파일이 어떤 캐릭터 셋인지 전달해 주지 않으면 상대편의 컴퓨터에서는 제대로 읽을수가 없다. 설사 말로 알려준다고 하더라도 상대편의 컴퓨터에 해당 캐릭터셋에 대한 정보가 없으면 역함수가 없고 디코딩도 당연히 할수 없다. 

그럼 이쯤에서 나올만한 생각이 있다. 지구의 모든 컴퓨터가 공용으로 사용할 수 있는 캐릭터 셋을 만들면 되지 않을까? 정답이다. 그래서 나온게 유니코드이다. 문제는 유니코드의 발상이 나오기 전에 이미 많은 캐릭터 셋이 많은 컴퓨터에서 사용이 되고 있다는 점과 모든 글자를 담기 위해서는 글자당 할당되는 번호가 길어지고 이는 당연히 저장 공간이 길어지는 2가지 문제가 추가적으로 야기된다. 

둘을 묶어 보면 기존에 호환성을 유지하면서도(여기서 호환성이란 보통 ASCII만을 의미한다.) 수많은 글자를 가능한 작은 저장공간에 인코딩을 할 수 있어야 한다는 얘기이다. 


ASCII
당시의 컴퓨터는 유닉스 기반의 컴퓨터로 7비트가 1바이트인 메모리를 사용하였고 따라서 자연스럽게 하나의 문자를 표현하는데 7비트를 사용하였다. null로 시작하는 32개의 제어문자,공백문자, 94개의 프린트문자, 지움문자로 128개의 비트조합을 배정하였다. 이에 따라 1963년 최초의 ASCII 표준이 제정되었는데 이때까지만 해도 많은 비트 조합이 94개의 프린트 문자 번호로 배정되지 않은 상태였으며 1968년에 오늘날과 같은 ASCII 코드가 만들어져 표준으로 지정되었다. 


ISO 8859
같은 라틴계열의 문자이긴 하지만 별도의 강세문자를 가지는 프랑스나 그리스의 문자는 구미의 알파벳과 조금 달랐고 이런 언어의 표현을 포함하는 캐릭터 셋을 만들려는 국제적인 노력은 ISO8859 시리즈에서 나타났다. ISO 8859-1은 서유럽 언어를 다루기에 충분했으며, ISO 8859-7은 기본적인 그리스 문자, 알파에서 오메가, 발음기호 등 현대의 그리스 문자와 영어를 모두 지원했다. ISO 8859에서는 하나의 문자를 표현하는데 8비트를 사용하며 ASCII 외의 각 나라마다 자신이 사용할 문자는 128 이후에 배치하였다. 즉 ISO 8859는 128개의 세계 공통 문자(주로 알파벳과 숫자, 제어코드)와 128개의 지역 문자(지역별로 다른 문자 할당)의 조합이다. 

ISO 2022
상식적으로 공통으로 사용하는 128개의 문자말고 추가적인 128개의 문자는 지역마다 다르게 정의되었기 때문에 제대로된 문서 교환이 이루어질리 없다. 파일에는 그 파일이 어떤 캐릭터 셋을 사용했는지에 대한 정보가 저장되어 있지 않기 때문에 wrirte할때는 불어 지역문자를 사용하면 러시아에서 read 할때는 제대로 읽혀지지 않는다. 초기에는 네트워크가 활발하지 않았고 대부분 컴퓨터에는 혼자 사용하는 것이라는 인식이 강했지만 이게 문제가 되는 대표적인 예는 이메일(Email)이다. 

프랑스 사람이 러시아 사람에게 프랑스어로 이메일을 작성했을때 러시아인 컴퓨터에서 프랑스어가 제대로 보이기 위해서 이메일은 이스케이프 시퀀스(escape sequence)로 글자별로 캐릭터 셋을 설정하도록 하였다. 즉 프랑스 이메일 클라이언트는 프랑스어 코드가 나올때마다 ISO 2022 문자 인코딩들은 이어서 나오는 문자들에 대한 문자세트를 지시하는 escape sequence를 포함하고 있다. 그럼 저장단계에서 escape를 보고 해당 캐릭터셋의 번호로 저장 하였다. 즉 아직까지는 해석과 저장의 명확한 분리를 하지 않았기 때문에 기본 ASCII가 아닌 문서를 교환하는 것은 이렇게 비효율적인 면이 존재한다. (escape 문자로 인해 data 길이가 늘어나고 여전히 read하는 곳에서 해당 charset을 인식하지 못한다면 읽을 수 없었다.)

ISO 2022 문자세트들은 여전히 많이 사용되고 있으나, 최근에는 최신 이메일 소프트웨어 등 에서 UTF-8과 같은 유니코드 문자 인코딩들을 사용하는 것으로 차츰 변환되고 있다.


KSC 5601

KSC 5601은 94x94의 각 위치(행열)에 한글 문자를 일정한 순서에 따라 배열해 놓은 charset이다. 앞서 말했듯이 한글은 표의문자와 표음 문자 2가지 성격을 모두 가지고 있는 독특한 특성때문에 여러가지 다양한 방법중에서 한글 코드의 KS 제정에서 완성형이 채택된 것은 내부적으로 한글의 출력이 모아쓰기 형태로 이루어지면서 동시에 한자를 섞어서 쓸 수 있어야 한다는 사회적 요구로 조합형을 수용 하기가 어려웠기 때문이다.(물론 정치적인 이유가 없던것은 아니다.)

또 다른 배경은 국가간의 정보교환을 위한 코드 표준화 과정에서 앞의 ISO 2022에서 제정한 코드 체계에 따라 세계 각국의 문자를 처리하는데 기인한다. 이는 1바이트 코드로 한 문자 표현이 불가능한 CJK(Chinese, Japanese, Korean) 문자를 2바이트 코드 영역의 첫 번째 영역에 넣을 수 있도록 영역을 확보해야 했기 때문이다.


KSC 5601은 완성형 한글 2,350자, 한자 4,888자, 기술/학술기호 등 특수문자 432자, 숫자 30자, 한글 낱자 94자, 로마문자 52자, 그리스 문자 48자, 괘선 조각 68자, 라틴 문자 27자, 일본 문자 169자, 러시아 문자 66자 등 총 8,224자와 기타 사용자 정의 영역으로 한글 96자, 한자 95자 정도를 사용하도록 배정하고 있다.(이후 KSX 등의 명칭이 변동된 역사는 이전글을 참조)

를 보면 94*94(8836) 의 매트릭스에 위 문자들이 지정되어 있다. 'A'가 03 * 33에 'a'가 03*65에 지정되어 있고 '가'는 16*01에 할당되어 있다. 위 PDF를 보면 캐릭터 셋과 인코딩의 차이에 대해 좀더 이해할 수 있다. KSC 5601 캐릭터 셋이란 "한국"이라는 지역에서 최소한 지원해야 하는 문자들에게 각각의 번호를 지정해놓은 것이고 인코딩이란 그 번호들을 컴퓨터 이진수로 어떻게 저장할 것인가에 대한 문제이다. 

일반적으로 KSC 5601을 지원하는 인코딩은(이를테면 euc-kr)은 1byte의 첫번째 비트가 0이면 이후 7bit를 ASCII문자에 대응시키고 첫번째 bit가 1이면 다음 바이트까지 묶어서 KSC 5601의 문자번호를 지정된 해쉬함수 계산을 통해 저장된 문자로 인식한다. 

네트워크가 발전하고 CJK에 컴퓨터가 많이 보급되면서 위와같이 지역적인 문사셋을 사용하는 것은 글자깨짐의 문제(파일의 메타정보에는 문자셋을 지정할 수 없다는 것을 기억하자. 대표적으로 HTML안에도 HTML의 문자셋을 HTML 문자 안에 지정해야 한다. 이러한 - 파일 해석을 위한 방법이 파일안에 있는 - 방법이 먹혀드는 이유는 HTML은 규약상 charetSet을 지정하는 부분까지 ASCII를 제외한 다른 문자가 나오지 않아야 한다는 가정때문이다.)때문에 UniCode가 나왔다. 


UNICODE

유니코드(Unicode)는, 컴퓨터에 저장하려는 모든 문서의 글자에 대해 캐릭터 셋을 제공하기 위한 국제 표준이다. 유니코드는 오늘날 실제 사용되는 모든 문자들과, 학자들만 사용하는 문자들, 또 수학기호, 언어학기호, 프로그래밍 언어 기호 등까지 포함하고 있다. 보통 유니코드는 말하거나 문서에 적을때 캐릭터 셋과 인코딩의 두가지 의미를 동시에 가지기 때문에 다소 헛갈리지만 

보통의 경우 
유니코드라고 말하면 캐릭터 셋을, 
USC-2, UCS-4는 캐릭터 셋 혹은 인코딩 2가지 의미를
UTF-8, UTF-16, UTF-32라고 말할때는 인코딩 방법을 의미한다. 

UCS-2는 2byte 숫자로 전세계에서 자주사용하는 문자를 지정해놓은 캐릭터 셋이고(여기에는 기본 ASCII 와 서유럽언어, CJK 언어 일본어 등이 할당되어 있다. 단 65536의 제약이 있기 때문에 모든 글자가 할당되어 있지는 않다.) 이 캐릭터 셋의 번호로 컴퓨터에 저장하면 UCS-2 인코딩을 사용한 경우가 된다. 

USC-4는 4byte의 숫자로 전세계의 모든 문자와 학자들만 사용하는 문자들, 수학기호, 언어학기호, 프로그래밍 언어등까지 포함하고 이후에 새로이 나타나게 될지도 모를 문자들를 위해 많은 여유공간이 있다. 실제로는 0번부터 256^4 까지 순차적으로 번호를 부여하는게 아니라 32bit를 영역별로 쪼개서 언어판(Plane)과 Group를 지정한다. ( Group 1byte + Plane 1byte + CodePoint 2byte를 사용한다. UCS-4가 인코딩의 의미로 사용될때에는 0x00000000 ~ 0x0010FFFF 범위만을 합법적인 코드값으로 갖는다. 00그룹 + (00 - 10 까지의 17개 Plane) + codepoint 2byte)


인터넷에서는 카더라 통신이 많아서 혼란의 소지가 많지만. 위 문서는 유니코드에 대해 가장 잘 설명한 그리고 정확한 문서이다. 

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

Unicode와 Database  (0) 2009.03.01
Global Software - Unicode와 Programming  (0) 2009.02.26
Global Software - Unicode  (0) 2009.02.25
Global Software  (0) 2009.02.22
Posted by bleujin
IT 이야기/유니코드2009. 3. 1. 09:53

유니코드를 적용하는데 있어 어려운 점은 단순히 프로그래밍에서 Content-Type만을 바꿔서는 할 수 없다는 것이다. 유니코드를 적용하려면 프로그래밍 외에도 메시지등의 텍스트 리소스와 데이타베이스 등도 모두 Unicode로 전환해야 한다.

그 중 문제가 되는 것은 기존의 레거시 파일들과 데이타 베이스이다. 새로 시작하는 프로젝트가 아닌이상 기존의 데이타베이스 레거시 데이타를 유니코드로 바꾸는 것은 매우*100 힘들다. 단순히 기술적으로 어렵다는 얘기가 아니라 여러가지 제한이 있기 때문에 완벽한 변환은 사실상 불가능하다.

일단 오라클의 경우 한글 처리에 대해 살펴보자.



오라클을 설치중에 위와 같은 화면이 나오는데 여기서 한국어의 선택은 한국어의 저장여부와 아무 상관이 없다. 여기서 한국어를 선택하면 오라클 클라이언트의 메시지가 한글로 나오는 것과 한글 폰트를 사용하고 Territory 지원을 한국으로 설정한다는 뜻이다.(즉 숫자나 날짜 포멧등을 Korea에 맞춘다.)

오라클의 환경변수중 하나인 NLS_LANG을 보면  KOREAN_KOREA.KO16MSWIN949, AMERICAN_AMERICA.KO16MSWIN949, KOREAN_KOREA.KO16KO16KSC5601 와 같이 사용하는데. 언어_영역.캐릭터셋의 구조로 되어 있다. 흔히 UTF를 사용하고자 한다면 NLS_LANG을 바꿔야 하는거냐고 오해하는데 NLS_LANG은 데이타베이스 캐릭터 셋과 상관없다. (NLS_LANG가 데이타베이스 캐릭터 셋과 항상 같아야 한다면 굳이 따로 설정해야 할 필요가 없다.) NLS_LANG은 원격지의 데이타베이스 환경이 아니라 자신이 속해 있는 환경을 데이타베이스에 알려주는 역할을 한다. 이 NLS_LANG 값의 의거해 데이타 베이스는 실제 UTF8로 저장이 된 데이타를 MSWIN949 코드로 변경하여 사용자에게 보여준다.



이 화면 뒤에 선택할수 있는 오라클에서 한글을 저장할 수 있는 캐릭터 셋은 KO16KSC5601, KO16MSWIN949, UTF 계열(오라클의 버전별로 UTF8, AL32UTF8, AI16UTF16 등이 있다.) 3가지가 있다. 한글 처리에 대해 익숙하지 않던 아주 오래된 레거시 데이타베이스의 경우 US7ASCII 캐릭터 셋에 한글이 저장되었다고 하는 경우도 있으나 이때의 한글은 한글이 아니라 단순히 바이트 덩어리이므로 한글이 저장되는 캐릭터 셋이라고 할 수 없다. (DB에서는 캐릭터 셋과 인코딩의 의미를 명백히 구분하지 않는다. 인코딩을 담당하는게 오라클 DB 자체이므로 사용자는 인코딩의 구현에 알 필요가 없으며 캐릭터 셋의 선택은 곧 인코딩이라고 할 수 있다. <- 이게 앞에서 말한 의도와 구현을 분리한 장점이 된다.)

KO16KSC5601
앞에서 말한 2350자의 완성형 한글만을 지원하는 캐릭터 셋이다. 유닉스에서 Lang=ko 셋팅시 기본값으로 선택이 되고 이름에서 나오는 그럴듯함때문에 선택되기도 하지만 새로운 프로젝트라면 위 캐릭터 셋 사용은 자제해야 한다. 혹자는 insert into test(a) values('숖') 같은 insert문이 실행되기 때문에 확장 한글이 들어간다고 오해하지만 실제로 select 시에 ?로 나온다. MS949를 사용하는 윈도우의 클라이언트 툴로 글자를 적을 수는 있지만 인코딩 해쉬함수를 거칠때 엉뚱한 숫자로 저장이 된다. 그래서 insert가 되는 것처럼 보이지만 실제로 select는 되지 않는다.)

KO16MSWIN949
Windows-949라는 캐릭터 셋은 MS만 사용하는 캐릭터 셋은 아니다. 캐릭터 셋 자체가 일종의 개념적인 코드 맵이므로 어느 소프트웨어든 상관없이 사용할 수 있으며 KO16MSWIN949는 오라클의 MS949 같은 캐릭터 셋이다. 완성형(KO16KSC5601)을 그대로 포함하고 있으며 추가적인 조합인 8822자의 한글을 추가적으로 포함하고 있는 KSC5601의 슈퍼셋이다. . KSC5601에서 사용하지 않는 바이트 코드를 사용하므로 한글 자모순서의 정렬이 제대로 되지 않기 때문에 일반적은 Order by문으로는 정렬 되지 않는다. 제대로 정렬하려면 Select * From test Order by NLSSORT(a, ‘NLS_SORT=UNICODE_BINARY’) 와 같은 구문을 사용해야 한다. 물론 인덱스도 위와 같이 캐릭터 셋을 적용하지 않고 만들었으면 인덱스를 사용할 수 없고 별도의 정렬과정을 거쳐야 하기 때문에 느리다.

UTF8(UTF8/AL32UTF8 등)
UTF8은 유니코드를 구현한 캐릭터 셋중에 가변길이 인코딩을 택하고 잇는 캐릭터 셋이다. 인코딩에 대한 얘기는 앞에서 했으니 생략하겠지만 가변 길이를 위해 일종의 플래그 비트를 각 바이트마다 포함시켜야 하다보니 한 글자를 표현하는데 필요한 바이트의 길이가 최대 1-4바이트(AL32UTF8의 경우 최대 6바이트)까지 늘어날 수 있다. 유니코드는 잘 알려진 바와 같이 현대 한글 11172자를 모두 가나다 순으로 정렬된 상태로 포함하고 있다. CJK 한자가 3바이트의 물리적 공간을 차지하기때문에 약간 공간을 많이 차지하지만 한글 이외에도 다른 언어들을 함께 데이타베이스에 저장해야 한다면 다른 선택의 여지가 없는 유일한  선택이 된다.

캐릭터 셋을 통한 방법 말고도 한글을 저장할 수 있는 방법이 한가지 더 있는데 데이터베이스 character set에 관계없이 NCHAR (National Character) 데이터타입을 이용하는 방법이다. 오라클 9i부터 NCHAR 데이터타입에 유니코드 character set을 지정할 수 있게 되었으므로 두 번째 방법은 9i부터 고려할 수 있는 방법이다. (NCHAR 데이터타입의 character set은 기본값으로 데이터베이스 생성시 NATIONAL CHARACTER SET에 지정된 값에 따르기 때문에 단순히 NCHAR를 사용한다고 해서 Unicode로 저장되지 않는다는 걸 주의하자.즉 만약 어떤 이유에 따라 NCS를 바꾸게 되면 이전에 저장한 글자는 제대로 보이지 않고 한가지 언어(+ASCII)만을 저장할 수 있다는 것을 주의해야 한다. 같은 컬럼에 한글과 일본어가 같이 저장되야 한다면 NCHAR는 사용해서는 안된다. ) 언뜻 보이기에 편해 보일지 모르지만 프로그래밍 환경, 마이그레이션의 용이성, 성능, 데이터 타입, 어플리케이션의 타입 등 많은 요소들을 고려해야 하기 때문에 결코 쉬운 일은 아니다.


   KO16KSC5601 KO16MSWIN949  UTF8  AL32UTF8 
 한글 지원  한글 2350자  2350+8822(11172자) 11172자   11172자 
 인코딩 버전  한글 완성형  확장은 MS949에 따라 배열  Unicode 2.1, 3.0  Unicode 3.0, 3.1, 3.2, 4.0
 한글 바이트  2 byte  2 byte  3 byte  3 byte
 지원 버전  7.x  8.0.6 이상  8.0 이후  9i R1 이상
 NChar로 설정가능  불가  불가  가능  불가
 한글 정렬  단순 바이너리 정렬  KOREAN_M, UNICODE_BINARY  한글 : 바이너리
 한자 : KOREAN_M
 
 장점  .  2byte로 모든 한글 입출력 가능  정렬이 효과적, 다른 언어들과 같이 저장되어야 할 경우 다른 대안이 없음  동일
 단점  한글 2350만 가능
 가능한 사용자제
 완성형과 호환문제로 글자 배열순과 정렬 순서가 다름  공간의 소모
 인코딩/디코딩 시간
 

가장 이상적인 방법은 AL32UTF8을 database character set으로 정하고 UTF8을 지원하는 언어로 프로그램을 개발해서 character set conversion이 없이 데이터가 오가는 것이다.

유니코드만을 비교하면 오라클의 경우 V7에서는 유니코드 버전 1.1을 지원하는 AL24UTFFSS (V7에서만 사용가능) 도입하면서 유니코드를 지원하기 시작하였고 V8에서 유니코드 버전 2.0을 지원하는 UTF8이 도입되고 V9에서는 유니코드 버전 3.1을 지원하는 AL32UTF8 / AL16UTF16이 도입되어 유니코드를 지원하고 있다. 자세한 것은 아래 표를 보면 알 수 있다.

Character set

지원 버전

유니코드

인코딩

유니코드 버전

Database character set

National character set

AL24UTFFSS

7.2-8i

UTF-8

1.1

지원

지원 안함

UTF8

8.0-9i

UTF-8

8.0-8.1.6 : 2.1

8.1.7이후 : 3.0

지원

9i만 지원

UTFE

8.0-9i

UTF-8

8.0-8.1.6 : 2.1

8.1.7이후 : 3.0

지원

지원 안함

AL32UTF8

9i

UFT-8

9iR1 : 3.0

9iR2 : 3.1

지원

지원 안함

Al16UTF16

9i

UFT-16

9iR1 : 3.0

9iR2 : 3.1

지원 안함

지원



캐릭터 셋 선택

1. 한글 지원을 위해서는 반드시 위의 3가지 선택(KO16KSC5601, KO16MSWIN949, UTF 계열)중의 하나를 선택해야 한다.

2. 한국에서만 쓸거고 한글만 쓸거다. 그리고 하드디스크 값이 비싸다 라는 시스템이라면 KO16MSWIN949을 선택해도 된다. 다만 정렬에 문제가 있다.

3. 한국어 뿐 아니라 중국어, 일본어, 러시아 어 등 다양한 데이타를 저장해야 한다면 UTF 계열을 쓴다. 인코딩 시간으로 MSWIN949보다 조금 속도 저하가 있다고 알려져 있으나 사실상 거의 영향을 미치지 않는다.

4. 대부분이 한글이며, 특정 나라의 외국어가 필요하다면 KO16MSWIN949를 사용하면서 NChar 컬럼에 외국어를 저장하는 것도 고려해볼만 하다.

5. KO16KSC5601은 쓰지 말자.
오라클 얘기만 했는데 MSSQL2000은 다국어 지원이 상대적으로 미비하다. 일반적으로 MSSQL은 윈도우 위에 설치되기 때문에 다국어를 사용하기 위해서는 사실 위의 방법 4밖에 대한이 없다. MSSQL의 NCHAR 계열의 컬럼은 UCS-2의 인코딩(UCS2의 캐릭터 셋의 바이트를 그대로 저장한다. 그래서 1개의 BMP만을 지원하며 16개의 SMP 문자는 사용할 수 없다.)을 사용하기 때문에 NCHAR의 모든 문자는 2byte다.(아스키도 마찬가지) 그래서 만약 MSSQL과 오라클이 둘다 지원되는 유니코드 다국어 시스템을 만든다면 두 DB가 길이 계산이 다르고 MSSQL은 SMP는 지원하지 않는 다는 사실을 유념해야 한다.

 

일반적인 프로그래밍 과정을 보자.

1. Client(Browser - JSP)
사용자가 메시지를 작성한다. 사용자의 request는 Stream 형태로 Server에 전송되는데 해당 웹 페이지가 UTF-8로 보여주고 있었다면 사용자가 텍스트 박스에 작성한 메시지도(request Body) UTF-8인코딩 바이트로 넘어가게 된다. (익스플로어의 경우 보기-인코딩 메뉴를 보면 현재페이지의 인코딩을 확인할 수 없다. )

HTTP 규약상 request는  request Header와 request Body로 구성되어 있는데 header에는 URI와  GET Parameter 그리고 쿠기 등으로 되어 있고 request Body는 Post 방식으로 넘어간 parameter들이 저장된다. 

request Body가 아닌 URI는 디폴트로 ISO-8859-1 방식으로 넘어가므로 Get 방식의 파라미터는 UTF-8이 아닌 ISO-8859-1로 인코딩 된다. (익스플로러의 경우 UTF-8로 URL을 보내기가 기본 false로 설정되어 있다.) 그렇기 때문에 GET방식을 사용하면 파라미터로 다국어를 넘기는게 사실상 어렵다.(a=한국어&b=일본어가 넘어올때랑 a=일본어&b=한국어 인 경우를 상상해보자.) GET으로 다국어 메시지를 넘기는 유일한 방법은 직접 파라미터를 UTF-8로 인코딩해서 16진수 숫자로 넘기는 방법이 있는데 이 경우 이미 GET의 가장 큰 장점인 간결을 희생한 결과다. 다만 이 방법을 사용하면 결과페이지를 즐겨찾기에 추가할 수 있는 장점이 있다.


2. Client(Browser - JSP) -> Server Program
사용자가 Send 버튼을 누른다.
Server 프로그래밍에서는 자바의 필터에 request.setCharacterEncoding("UTF-8"); 구문을 넣어주어 사용자의 request Body가 UTF8로 인코딩되었다는 사실을 알려준다. 정보를 설정하였을뿐 실제로 컨버팅이 되는건 아니라는 사실에 주의하자. 4점대의 구버전의 톰캣의 경우 Get으로 넘긴 파라미터로 넘긴 정보도 영향을 받았으나 많은 혼란을 일으켰고 이후 수정되었다. (보통 web.xml 파일에 필터를 설정)


3. Server Program(Java)
Java에서 사용하는 모든 변수는 UTF-16 인코딩을 사용하고 있지만 request.getCharacterEncoding()를 통해 Java는 사용자가 작성한 메시지의 인코딩 타입을 알 수 있으므로 request.getParam(name)의 값을 연산에 사용하는데 무리가 없다.

String a1 = request.getParam("name");   
// a1은 자바의 메모리에 저장되는 변수이므로 UTF16 인코딩을 사용한다. request.getCharacterEncoding() 값을 읽어서 자동 컨버팅한다. 만약 request.setCharacterEncoding을 하지 않았다면 디폴트로 request는 ISO-8859-1이 설정되어 있다.
인코딩에 대해서 잘 모르면 아래와 같은 웃긴 코드를 작성하게 된다.
// bad case1
String a2 = new String(a1.getBytes("MS949"),"UTF-8"); // a1을 MS949로 convert한 byte[]를 얻어서 이를 UTF-8로 디코딩한 값을 a2에 저장한다. 당연히 a2를 출력하려면 깨진다 -ㅅ-;

// bad case2
String a3 = new String(request.getParam("name").getBytes("EUC-KR"), "UTF-8")
request를 euc-kr로 컨버트한 byte[]을 얻어서 이를 UTF-8로 디코딩한 값을 a3에 저장한다. 물론 깨진다. new String(byte[], CharSet)은 앞의 byte[] 배열은 뒤쪽의 CharSet으로 디코딩을 해야 한다고 java에게 알려주는 역할을 한다.

// bad case3
String a3 = new String(request.getParam("name").getBytes("ISO-8859-1"), "MS949")
2000년 초반에 인코딩에 대한 충분한 지식이 없었을때 작성하던 코드들로 기본으로 브라우저는 ISO-8859-1 인코딩을 사용하기 때문에 별도의 설정 등이 없이 페이지를 만들었을때 사용자가 작성한 한글도 ISO-8859-1로 인코딩된다.(물론 잘못 인코딩되어 있는 것이다.) 이걸 ISO-8859-1의 byte[] 읽어서 이 녀석은 원래는 MS949로 인코딩이 된거라고 알려준다.(사용자는 requestBody에 한글을 입력하였으므로) 물론 이렇게 해도 한글이 읽히긴 읽힌다. 다국어를 사용할 수 없고 모든 parameter에 이 과정을 해줘야 한다는 불편이 있지만 말이다. 호미로 막을것을 가레로 막는 코드이기에 그냥 request.setCharacterEncoding를 해주는게 낫고 다국어를 사용하려면 코드에 지역 캐릭터 셋이 사용되서는 안된다.


4. Server Program -> DB(Oracle - UTF-8)
지금까지 제대로 되어 있다면 자바의 변수는 UTF-16, request Body는 UTF8로 되어 있을테고 DB에는 UTF-8로 컨버팅되어 들어간다. 자동으로 컨버팅이 될 수 있는 이유는 변환되기 전의 값들의 인코딩 정보를 알 수 있기 때문이다. 그러나 모든 DB가 자동으로 convert가 되는게 아니고(Driver URL의 옵션으로 알려주어야 하는 DB도 있다.) 모든 DB가 Unicode를 지원하는게 아니다.


5. DB(Oracle UTF-8) -> Server Program
DB에 저장된 UTF-8로 인코딩된 byte[]들을 InputStream으로 읽어오지만 String a = rs.getString(name); 로 자바 힙에 스트링 객체를 생성할때는 다시 UTF-16으로 컨버트 되어 연산에 참여한다.


6. JSP - Server Program(Servlet)
JSP는 첫 호출시에 Servlet 엔진에 의해 개별적인 Servlet으로 변환되는데 이때 JSP 파일을 Load하는데 이때 참조하는 정보가 JSP 제일 위에 있는 page 지시자인 <%@ page contentType="text/html; charset=euc-kr"> 이다. 파일 그 자체에는 Charset 정보가 없기 때문에 파일 첫머리에 해당 파일의 인코딩 정보를 기록한다. 유니코드 세상으로 가기위해서는 JSP도 UTF8로 작성해야 한다고 알려져 있는데 맞는 말이기도 하고 틀린말이기도 하다. 사실 JSP 편집을 디폴트로 MS949 인코딩을 사용하는 이클립스나 UEdit를 사용해도 문제가 없고 JSP의 contentType을 euc-kr로 설정해도 상관없다. 단 JSP에는 ASCII문자만 사용해야 한다. 물론 html Body안의 모든 문자도 마찬가지이다. 일단 ASCII의 0-127번은 모든 인코딩에서 겹치지 않으므로 문서를 euc-kr로 작성하나 UTF-8로 작성하나 차이가 없다. JSP에 한글을 사용하고 UTF-8로 인코딩을 해도 되지만 메시지를 한글로 작성한다는 자체가 이미 다국어를 지원하는 시스템이 아니다.

ASCII를 제외한 모든 message 문자는 별도의 파일에서 읽어들어 사용한다. 현재 이부분이 아직 이해가 되지 않는다. 일단 jsp는 어플리케이션 서버 내부적으로 지역적 인코딩을 사용하는 getWriter를 호출해서 사용한다. 그리고 메시지 파일에서 읽어온 값들은 UTF-8로 저장되어 있지만 자바 맵에 저장하는 순간 UTF-16으로 바뀐다. 서블릿에서 어떻게 연산이 되길래 UTF-8포맷으로 제대로 보이는걸까?? -ㅅ-??


7. Server Program -> Browser(Client)
Servlet은 Stream을 반환하는데 해당 Stream의 인코딩 정보를 Client Browser에게 알려주는게 response.setContentType("text/html; charset=UTF-8"); 이다. String이라는건 자바 내부 메모리 구조에서 UTF-16인코딩일뿐 기본적인 Network 통신은 항상 byte[] 형태의 Stream 형태로 이루어지므로 해당 Stream의 byte[]의 인코딩 정보를 알려줘야 한다. 만약 response의 setContentType을 설정하지 않았을경우 익스플로어는 독특한 짓을 하는데 특정 언어를 사용할때 나타나는 바이트 빈도를 토대로 언어와 인코딩의 방식을 추측한다. 다만 이경우 일반적인 분포도와 다른 글자들을 사용할 경우 잘못 인식하므로 화면이 깨진다.


여기서 헛갈리지 말아야 할것은 request나 response의 setCharacterEncoding은 Stream의 인코딩 정보를 알려주는 것일뿐 실제로 컨버팅을 하는게 아니라는 거다.

 

참고

 

http://www.oracle.com/technology/global/kr/pub/columns/oracle_lns_1.html
http://www.oracle.com/technology/global/kr/pub/columns/oracle_lns_2.html

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

다시 쓰는 UTF  (0) 2009.06.12
Global Software - Unicode와 Programming  (0) 2009.02.26
Global Software - Unicode  (0) 2009.02.25
Global Software  (0) 2009.02.22
Posted by bleujin
IT 이야기/유니코드2009. 2. 26. 04:14


Unicode에 대해 이해가 됐더라도(원래 유니코드는 복잡하지 않다. 다만 잘못 알려진 정보들과 혼합되어 혼란스웠을뿐 - 원래 진실을 숨기는 가장 좋은 방법은 노이즈 데이타를 많이 발생시키는 것이다. - 유니코드의 경우 의도된것은 아니었지만 - 진실과 거짓이 뒤섞이게 되고 대부분의 사람들은 귀찮아서 진실을 애써 찾기 않게 된다.) "이제 난 i18n 프로그래머야 핫핫" 이라고 말할 수 없다.

Unicode가 복잡하다고 사람들이 말하는 것은 이론적인 UNICODE 보다는 실제적인 프로그래밍과 DB에서의 사용에 있다. 프로그래머들이 가장 자주하는 삽질은 

// write code - bad
  File file = new File(hanFile);
  FileWriter w = new FileWriter(file);

  w.write(message);
  w.close();

// read code - bad
   BufferedReader r = new BufferedReader(new FileReader(file)) ;
   String m = r.readLine() ;
   r.close() ;

와 같은 코드이다. 앞서 언급한바와 같이 파일 그 자체에는 캐릭터 셋 정보가 들어가 있지 않다. 파일은 그냥 바이트 덩어리 일뿐이다. 그래서 Java와 같은 프로그래밍 언어로 파일을 읽거나 쓸때 캐릭터 셋 정보를 명시하지 않으면 시스템 기본 캐릭터 셋과 인코딩/디코딩을 사용하게 된다. 만약 사용하는게 한글 윈도우일 경우 기본으로 CP949(MS949) 인코딩을 사용하게 된다. 이 경우 파일을 쓰는 컴퓨터와 파일을 읽는 컴퓨터가 다르면 같은 자바로 작성되었다고 하더라도 서로 이해할 수 없는 문자가 나오게 된다. (코드 작성자의 컴퓨터와 실행하는 컴퓨터가 다를경우도 마찬가지이다. )

예컨데 Writer는 한글 윈도우에서 하고 유닉스에서 Reader를 하려면 종종 글자가 깨지는 것이다.(예컨데 윈도우는 MS949를 쓰고 리눅스는 기본 캐릭터 셋으로 UTF-8을 사용한다.) 즉 위 코드는 해당 프로그램이 실행되는 OS와 OS 셋팅에 따라 다른 결과를 야기할 수 있다.

OS에 상관없이 같은 바이트 배열을 가지는 파일을 생성하려면

// write code - not bad
  File file = new File(hanFile);
  FileWriter w = new FileWriter(new OutputStreamWriter(new FileOutputStream(file, charset)));

  w.write(message);
  w.close();

// read code - not bad
   BufferedReader r = new BufferedReader(new InputStreamReader(new InputStream(file, charset))) ;
   String m = r.readLine() ;
   r.close() ;

와 같이 명시적으로 Stream을 이용하여 charset을 설정해야 한다. 이 말은 달리 말해서 파일은 인코딩된 캐릭터 셋을 모르면 - 다른 프로그램이 생성한 파일 등 - 제대로 읽을수 없다는 뜻이다.



유니코드를 제외한 한글을 사용할 수 있는 캐릭터 셋은 KSC5601이며 인코딩은 cp949, MS949, euc-kr, ks_c_5601-1987 가 있다. KSC5601은 엄밀히 말해서 인코딩이 아니라 94*94 매트릭스에 정의된 캐릭터 셋이다. 그리고 유니코드 조직에 등록된 한글의 표준 캐릭터 셋이기도 하다. 공식적인 명칭은 KS 표준 완성형 코드 KSC5601-1987이며 좁은 의미로는 캐릭터 셋이지만 당시에는 코드포인트=물리적 바이트코드 였기 때문에 인코딩의 의미로도 쓰인다. 참고로 KSC5601-1992는 앞글에서 언급한 1+5+5+5의 조합형 한글 표준이다. (그러나 별로 쓰이지 않는다.)

이미 많은 사람들이 제기한대로 KSC5601은 문제가 많은 캐릭터 셋이었다. 이미 실질적으로 표준이나 마찬가지인 아스키 코드의 128번 문자 이하는 사용할 수 없는 상태에서 모든 한글이 아니라 자주 사용 가능한 문자 94 * 94(8836)자에 대해서만 코드포인트를 지정하였으므로 현대 한글 11172자를 모두 표현할 수가 없는 문제점이 발생한다. 8836자에는  많이 사용하는 한글 음절 2350자("똠", "햏", "먄" 등의 글자를 사용 못했다.), 한자 4888자, 특수문자 1128자, 나머지 470자를 배정한다. (이후 KSC5657-1991 확장 표준코드가 지정되었는데 한글 1930자, 한자 2856자, 옛한글 1677자 등이 추가되었다. )

어쨌건 조합형 한글을 지지하는 사람들이 이런 제한의 문제점을 제기하였으며 그래서 완성형 지지자였던 MS는 한글 코드포인트를 추가한 "확장 한글 완성형"인 CP949(UHC)를 내놓았고 이후 수천개의 한글을 더 추가하여 현재의 11000자 정도를 지원하는 MS949(MS-Window 확장 완성형 한글)를 사용하였다. MS949는 기존의 KSC5601의 코드 포인트 그대로 포함하고 있으며 기존의 문서와의 호환성 때문에 이전에 사용하지 않던 빈 코드 포인트에 추가 했기 때문에 한글이 제대로 정렬되지 않는 단점이 있다. MS949는 KSC5601의 슈퍼셋이 되기때문에 KSC-5601로 쓴 파일을 MS949로 읽어도 잘 읽힌다.(물론 반대는 안된다.)


한글은 중국의 한문같은 계열의 표의 언어와 알파벳류의 표음 언어의 중간적인 존재다.(일본어는 잘 모르지만 일본어도 중간적 위치에 있다고 알고있다.) 그러나 IT에서 문자는 표음 언어가 좀 더 용이한 표현이 가능하기 때문에 자판에서 영어 알파벳과 자모를 대칭시킨것처럼 한글을 "ㅎ ㅏ ㄴ ㄱ ㅡ ㄹ" 식으로 저장했다면 좀더 효율적이었을 것이다. 하지만 대부분의 상식과는 달리 효율이 우선시 되어 정해지는 일은 거의 없다.


일반 사용자는 많이 사용하지 않았지만 지역화의 문제를 일찍 겪었던 유닉스 계열은 영문은 KSC5636(영문자에 대한 표준 - 기존 ASCII와 역슬래스가 \으로 바뀐것만 빼고 동일)로 처리하고 한글은 KSC5601-1987로 처리하는 euc-kr(Extended Unix Code-Korean)를 사용하고 있었다.

앞서 말한대로 당시의 표준원의 공식 완성형 표준인 KSC5601은 2바이트로 가능한 655,536개 문자중에 한자와 특수부호를 사용할 공간 확보를 위해 한글은 자주 사용되는 2,530자만 표현 가능하게 제안하였다. (이후 명칭을 KSX1001로 바꿈) 하지만 제안 당시에 편의주의적 발상이라는 비판에 조합형이 함께 존재하는 상태에서 독단적으로 채택되었는데다 윈도우즈 95가 나오고 인터넷이 발달하면서 KSC5601은 현실에서 사실상 표준으로 인정받지 못했다.




WIN95가 나올때 MS는 사실 완성형 한글 보다는 이미 Windows 내부적으로 사용하고 있는 Unicode를 사용할 것을 권했지만 KSC5601이 공식 표준으로 지정된 상태였기때문에 이를 무시할수는 없었고 추가 한글을 표현하기 위해 앞의 CP949를 한글 윈도우즈의 기본 캐릭터셋으로 사용할것을 밝혔다. 

이에 편가르고 싸우고 있던 조합형 지지자뿐 아니라 KSC5601 완성형 지지자까지, 게다가 멋모르던 정부까지 나서서 사용거부 및 수입규제 검토등을 내세우며 강력히 반발하였다. 확장 완성형 한글은 앞서 말한대로 새로운 확장 글자를 호환을 이유로 빈영역에 추가하였기 때문에 정렬시 문제가 있고, 이미 표준안이 존재하는데 추가적인 한글 코드는 논란을 가중시킬뿐 아니라 일단 완성형은 한글 제작 원리에 맞지 않는다는게 반대 이유였다.(당시의 신문 사설에 '세종대왕이 통곡한다니~, 마이크로소프트가 조합형을 처리할 기술이 없느니 하면서 이른바 애국심 주장을 펼쳤던 과거'를 기억하는 개발자로서 쓸데 없는데 애국심을 끌어들이는 주장은 대부분 헛소리라는걸 깨닫게 해준 사건이다. 사실 유니코드는 초중성의 모든 조합의 한글이 표현가능했기 때문에 그냥 완성형인 KSC5601이나 현재의 MS949보다는 나은 선택이었다고 생각한다. 조합형을 고려하지 않은 건 아니었지만 기존 파일과의 호환 문제를 고려하지 않을 수 없었다.)



그러나 한국 정부의 목소리 큰것과 상관없이 힘은 MS에 있었기에 MS는 CP949 캐릭터셋(이후 MS949)를 밀어붙였고, 절대 다수인 Windows 사용자들은 왜 윈도우 에서는 제대로 나오는 글자가 인터넷에서는 제대로 쓸 수 없는지 불평했다. 사용자의 불평에 대응하기 위해 불쌍한 프로그래머들이 케이스별로 작성한 온갖 이상야릇한 코드로 치장되던 혼란의 시간이 흐른 후 사실상 인터넷에서의 한글 표준은 MS949가 되었다. 다만 여전히 공식적인 RFC상에는 MS949가 표준이 아니었기 때문에 HTML에서 <META> 태그에는 MS949라고 적는게 아니라 EUC-KR로 적어야만 확장 한글 표현이 가능하면서 캐릭터 셋의 명칭에 혼란이 야기 되었다. (즉 이전의 유닉스 계의 euc-kr - KSC5601에 기반을 두었다 - 과 meta tag에 적는 euc-kr - 익스플로러에서는 CP949에 기반을 두었다 - 은 의미가 다르다. 이런 엽기적인 짓이 가능했던 것은 당시(2003 - 2005년) 브라우저의 90% 이상을 차지하고 있던 MS 익스플로러 였기에 가능했다. )


그러나 진정한 혼란은 여기서부터다. 한글 표준이 이렇게 아웅다웅 하고 있을 시절에 옆에서 한글지원을 하는 소프트웨어를 판매하는 다른 업체들도 혼란스러워졌고 발표시기와 정책에 따라 차이점을 보였다. 

표준과 현실사이의 갭에서 사람들이 우왕좌왕하고 있는거와 상관없이 어쨌건 기준을 정해야 했던 Sun은 "euc-kr"과 cp949를 "그냥 완성형 한글"이라는 의미를 사용하였고 이후에 "확장 완성형 한글" 인코딩으로 MS949를 추가하였다. 그래서 자바에서 "확장 한글 완성형"을 사용하기 위해서는 MS949 인코딩을 사용해야 한다. (단 특정회사의 자바버전의 경우 MS949 is not supported by the current VM operation system 란 예외가 나오면서 MS949를 인식하지 못하는 런타임도 있으니 이때는 Sun의 1.5이상의 자바를 설치해야 한다.) 그래서 자바 request는 MS949로 보내고 브라우저가 인식하기 위해 HTML Meta Tag에는 EUC-KR로 적는 우스운 상황이 되었다.

그렇지만 또 다른 언어인 Perl의 경우에는 euc-kr은 "그냥 완성형"이고 cp949는 "확장 완성형 한글"을 의미한다. 당시의 역사에 좀더 자세한 내용은 http://sluvy.tistory.com/entry/%ED%8D%BC%EC%98%A8-%EA%B8%80%ED%95%9C%EA%B8%80-%EC%A1%B0%ED%95%A9%ED%98%95%EC%99%84%EC%84%B1%ED%98%95%EC%9C%A0%EB%8B%88%EC%BD%94%EB%93%9C%EC%9D%98-%EB%AA%A8%EB%93%A0%EA%B2%83 에서 확인할 수 있다.


언어에 따라 그리고 인터넷에서는 euc-kr란 의미가 달리 사용되면서 프로그래머들은 완전히 혼란스러웠다. 어떤 책에는 euc-kr은 "그냥 완성형"이고 어떤 책은 "확장 완성형"이란 의미로 사용했으며 프로그래밍 언어에 따라 의미도 달랐기 때문에 의사소통시 심각한 오해와 장애를 불러 일으켰다. 과거의 책을 보고 euc-kr은 "그냥 완성형"이라는 의미였지만 인터넷에서는 확장 완성형 한글을 쓰기 위해서는 META 태그에 EUC-KR을 써야 했고 Perl책의 한글 인코딩과 자바책의 한글 인코딩은 다른 의미가 되어버렸다. 이런식으로 노이즈가 섞이면서 어느게 진실인지 알 수 없게 되어버렸다.


이쯤에서 그럼 이런 뒤죽박죽한 상황에서 어떤 한글 인코딩을 써야 하는가? 라는 의문으로 돌아가보자
대답은 간단하다. 안쓰면 된다. (그러면 모두 잊어버려도 상관없다.-ㅅ-) 한글을 버리고 국제화에 맞춰 Unicode를 프로그래머는 사용해야 한다.


여기서의 문제는 20세기 교수들이 21세기 아이들을 가르킨다는 농담처럼 아직도 많은 책들과 인터넷 문서에는 여전히 한글 처리라는 명목으로

PrintWriter out = new PrintWriter(new OutputStreamWriter(res.getOutputStream(),”KSC5601”),true);
new String(searchWord.getBytes("iso-8859-1"),"euc-kr")

와 같은 코드를 소개하고 있다. 이런 코드가 나온다면 뒤도 돌아보지 말고 Back 버튼을 눌러야 한다. 제대로 한글 처리를 위해 유니코드를 사용하면 위와 같이 String.getByte의 Charset변환을 전혀 사용하지 않아야 한다. 즉 모든 인프라와 리소스를 UTF8인코딩 하나로 사용하기 때문에 변환을 해야할 이유가 없다.


이 얘기는 나중에 다시 하기로 하고 일단 유니코드를 쓰기전에 간단히 알아야 할 것이 있다.

package test.unicode;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.OutputStreamWriter;
import java.io.Writer;

import junit.framework.TestCase;

public class TestFile extends TestCase {

  String message = "한글햏ABC123";
  String hanFile = "c:\\temp\\han.txt";
  String utf8File = "c:\\temp\\utf8.txt";
  String utf16File = "c:\\temp\\utf16.txt";
  String utf16LEFile = "c:\\temp\\utf16le.txt";
  String utf16BEFile = "c:\\temp\\utf16be.txt";

  String utf8ByUEdit = "c:\\temp\\utf8ByUEdit.txt"// UEdit를 사용하여 UTF8모드로 저장한 파일 ...-_-

  public void testScenario() throws Exception {
    File file = createHan();
    System.out.println(ByteUtils.toHex(file));

    file = createUTF8();
    System.out.println(ByteUtils.toHex(file));

    file = createUTF16();
    System.out.println(ByteUtils.toHex(file));

    file = createUTF16LE();
    System.out.println(ByteUtils.toHex(file));

    file = createUTF16BE();
    System.out.println(ByteUtils.toHex(file));

    System.out.println(ByteUtils.toHex(new File(utf8ByUEdit)));

  }

  public File createHan() throws Exception {
    File file = new File(hanFile);
    FileWriter w = new FileWriter(file);

    w.write(message);
    w.close();
    return file;
  }

  public File createUTF8() throws Exception {
    File file = new File(utf8File);
    Writer w = new OutputStreamWriter(new FileOutputStream(file)"UTF8");

    w.write(message);
    w.close();
    return file;
  }

  public File createUTF16() throws Exception {
    File file = new File(utf16File);
    Writer w = new OutputStreamWriter(new FileOutputStream(utf16File)"UTF16");

    w.write(message);
    w.close();
    return file;
  }

  public File createUTF16LE() throws Exception {
    File file = new File(utf16LEFile);
    Writer w = new OutputStreamWriter(new FileOutputStream(utf16LEFile)"UTF-16LE");

    w.write(message);
    w.close();
    return file;
  }
  public File createUTF16BE() throws Exception {
    File file = new File(utf16BEFile);
    Writer w = new OutputStreamWriter(new FileOutputStream(utf16BEFile)"UTF-16BE");

    w.write(message);
    w.close();
    return file;
  }

}


를 작성해서 실행시키면

createHan()
결과 : C7D1 B1DB C164 41 42 43 31 32 33
윈도우 XP에서 실행하였기에 기본 인코딩인 MS949가 적용되어 C7D1-한, B1DB-글, C164-햏 (41,42,43) ABC, (31,32,33) 123 에 해당하는 Hex 코드가 나온다. 확장 완성형 한글 한자당 2byte다.

createUTF8()
결과 : ED959C EAB880 ED968F 41 42 43 31 32 33
UTF8 인코딩은 "ED959C-한, EAB880-글, ED968F-햏" 로 한글 한글자당 3byte가 할당되고 영어와 숫자는 1byte이다. 기본적으로 ASCII는 모든 캐릭터 셋의 서브셋(슈퍼셋의 반대의미)이기 때문에 코드 포인트는 동일하다.

createUTF16()
결과 : FEFF D55C AE00 D58F 0041 0042 0043 0031 0032 0033
UTF16 인코딩을 하면 "D55C-한 AE00-글 D58F-햏" 이다. 대부분의 한글은 BMP에 속하므로 2byte가 할당된다. ASCII문자의 경우 코드 포인트는 동일하지만 UTF16인코딩의 특성상 2byte가 할당되기 때문에 "00"이 앞에 붙었다. 젤 처음에 있는 FEFF는 빅 엔디안 즉 유니코드 바이트 순서 표시이다. 관례상 유니코드는 바이트 순서 표시를 하게 되 있으나 이상과 달리 현실에서는 모든 유니코드에 표시하지는 않으며 표시 할때도 있고 안할때도 있다. 자바의 경우 UTF16 인코딩 시에만 엔디안 표시를 하고 있으나 사실 프로그래밍 언어나 툴마다 지멋대로다 -ㅅ-

createUTF16LE()
결과 : 5CD5 00AE 8FD5 4100 4200 4300 3100 3200 3300
UTF-16LE는 리틀 엔디안 UTF-16을 말한다. 영문자 A의 경우 0041이 아니라 4100으로 표현되기 때문에 2바이트씩 끊어서 순서를 뒤집어 읽어야 한다. 5CD5 00AE 8FD5를 2바이트씩 끊어서 앞뒤 바이트를 바꾸면 위의 createUTF16과 결과가 같다. UTF-16LE라고 명시적으로 인코딩에 엔디안을 명시하면 생성되는 파일에도 엔디안을 표시하지 않는 것도 주의해야 한다.(물론 자바에 한정한 말이고 다른 언어는 어케 될지 모른다.)


createUTF16BE()
결과 : D55C AE00 D58F 0041 0042 0043 0031 0032 0033
자바는 빅 엔디안이 기본이므로 앞의 엔디안 마크를 제외하면 createUTF16()와 동일하다. 참고적으로 윈도우는 리틀 엔디안을 기본값으로 하고 있기때문에 윈도위 계열의 리틀엔디안을 사용하는 편집기로 위 파일을 열면 제대로 보이지 않는다.


맨 마지막의
EFBBBF ED959C EAB880 ED968F 41 42 43 31 32 33 는 자바가 아니라 울트라 에디트로 UTF8 편집모드로 같은 내용을 직접 저장해서 Hex코드를 읽은 것이다. 자바로 생성한 createUTF8()와는 달리 맨 앞에 UTF8에 해당하는 EFBBBF의 엔디안 표시가 붙었다.(참고적으로 UTF-32/LE의 표시는 FF FE 00 00를 사용한다.) 그래서 울트라 에디터로 작성한 UTF8 파일을 자바로 읽으면 "?한글ABC123"와 같이 첫글자가 깨진다. 자바는 UTF8 인코딩/디코딩시 엔디안을 표시하지 않기 때문에 엔디안으로 인식하지 못하고 깨진 문자로 나오게 된다. (참고로 UltraEdit의 Hex모드로 보기는 UTF 파일 편집때 제대로 동작하지 않기 때문에 무시하는게 좋다.)


....................

인생 사는게 쉬운게 아니다. -ㅅ-
이상과는 달리 유니코드의 현실도 - 한글의 시궁창만큼은 아니지만 - 그리 만만하지 않다.

유니코드를 사용하기 위해 첫번째로 생각해야 할것은 어떤 인코딩(UTF8,16,32)을 사용할 것이냐 인데 이는 보통 과거 ASCII 파일과의 호환 문제로(UTF-16, 32는 레거시 ASCII 파일을 제대로 읽지 못한다.) UTF8 포멧을 주로 사용하는데 이때 엔디안의 종료와 엔디안 표시 여부가 툴마다 언어마다 다르다.

그래서 UTF8로 통일하더라도 이기종간이나 여러 언어로 작성한 컴포턴트간 통신에는 역시 주의하여야 한다.





-- 엔디안
조나단 스위프트의 걸리버 여행기에 나오는 이야기로 삶은 달걀을 둥근쪽을 깨서 먹는 사람들 Big Endian과 뾰족한 쪽을 깨서 먹는 사람들 Little Endian이 서로 나뉘어 대립을 하는 소인국 이야기에서 따온 말이며 빅엔디안은 우리가 평소에 보던 방식으로 메모리에 쓰는 방식이고 리틀엔디안은 뒤집어 져서 쓴다고 생각하면 된다.  

우리가 사용하는 인텔(Intel)의 x86 계열의 CPU등은 리틀엔디언 방식의 CPU이고 Motorola, Sun, Sparc 같은 Risc 타입은 빅엔디안 방식이다.

리틀 엔디안을 쓰는 이유는 산술연산유닛(ALU)에서 메모리를 읽는 방식이 메모리 주소가 낮은 쪽에서부터 높은 쪽으로 읽기 때문에 산술 연산의 수행이 더 쉽기 때문이다.

빅엔디안 방식의 장점이라면, 정수로 정렬된 수에대한 비교가 메모리에서 읽는 순서대로 바로 비교가 가능 정수와 숫자타입의 데이터를 같은 순서 방향으로 읽을수 있다는 점이 있다.

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

다시 쓰는 UTF  (0) 2009.06.12
Unicode와 Database  (0) 2009.03.01
Global Software - Unicode  (0) 2009.02.25
Global Software  (0) 2009.02.22
Posted by bleujin
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
IT 이야기/유니코드2009. 2. 22. 04:42

유전자 비율의 보존에 대한 하디-바이베크로 법칙대로 그리고 조엘이 언급한대로 변하지 않는 비율중의 하나는 Global Software에 대해 이해하고 있는 프로그래머의 비율이다. 이는 많은 비율의 서구에서는 굳이 고려하지 않아도 경제적 손실이 크지 않다 - 즉 별로 상관없다 - 는 점에서 기인하는 것이겠으나 사실 딱히 우리나라 라고 낫지는 않다. 

GS의 Global이란 의미는 크게 2가지로 나눌수 있는데 첫째는 Territory 지원 둘째는 Lanaguage 지원이다. 

Territory(영역) 지원은 문화적 차이에 의한 지원 기능이다. 예컨데 첫째 요일이 월요일이나 일요일이냐가 서로 다르고 2009년의 1월의 첫째주를 1-3일로 보느냐 혹은 4-10일로 보느냐도 서로 다르다. 날짜 포맷이 05/08/10라고 했을때 이를 2005년 8월 10일로 해석하는 나라도 있고 2010년 5월 8일로 해석하는 나라도 있다. 中華民國94年07月21日 같은 날짜 포맷도 있다. 가까운 일본의 경우만 봐도 에도 몇년 혹은 쇼와 몇년 식의 정권을 기준으로 삼은 날짜 포맷도 있다. 
숫자의 경우 동양은 만단위로 끊는 2,000.00 이 익숙하지만 서양은 천단위인 20,000.00 으로 사용한다. 게다가 20.000,00(오타가 아니다) 같은 포맷을 사용하는 터키같은 나라도 있다. 영역지원이 되는 대표적인 소프트웨어는 윈도우와 오라클이 있다.


이 포스트의 주요 내용인 Language 지원은 어찌보면 간단하기도 달리 보면 복잡하기도 한 문제다. 언어 지원에 대해 많이 나오는 Term들은 인코딩, 디코딩, ContentType, ISO-8859-1, 이메일(브라우저)이 깨져 보여요, UTF-8, Unicode, UTF32, Cdoe 947, i18n, NLS-Lang, KO16MSWIN949, KSC5601 등등 매우 많다. 이 얘기는 IT에서 언어지원의 역사가 그리 순조롭게 진행되지 않았다는 뜻이기도 하다.

실제 컴퓨터는 문자 그 자체를 저장하는게 아니고 전기신호를 통한 이진 비트를 통해 정보를 저장한다. 그래서 인코딩-디코딩 영어 그대로 해석하자면 문자를 비트를 통해 코드화 시키는 것을 인코드라고 하고 반대로 코드를 화면에 문자로 보여주는 것을 디코드라고 부른다. 여기서 중요한 점은 인코드와 디코드 방식이 동일해야 문자를 제대로 표현할 수 있다는 점이다. 예컨데 A를 65(0100 0001)라고 인코드 시켰으면 디코드시에 65는 A라고 읽어야 하는 상호 합의의 약속 같은 것이다.

컴퓨터가 처음 발명된 구미에서는 알파벳과 약간의 특수문자만 사용해도 충분했기 때문에 1byte=8bit의 00-FF까지의 256개의 코드를 사용해도 충분했었다. 아니 오히려 남았기 때문에 00-7F 까지의 128개만 사용하는 ASCII라는 코드 페이지를 가지고 있었다. 문제의 시초는 10000000 - 11111111 까지의 여분이었다. 많은 사람이 128-255 사이에 위치한 코드에 대해 지엽적인 생각으로 몇몇 강조문자와 선그리기 문자따위에 사용하였다. 미국 이외의 지역에 PC는 각종 OEM 문자 집합을 지역적으로 정의하였기 때문에 컴퓨터간 문자호환은 되지 않았다. 여기까지는 비교적 상식이다

아시아권으로 넘어오면서 문자체계는 더욱 복잡해졌는데 알파벳이 아닌 수천개의 문자를 저장해야 했으므로 1byte로 문자를 저장하긴 무리였다. 그래서 다소 이상해보이지만 첫번째 비트가 1이면 2byte를 묶어서 하나의 문자를 표현하고 첫번째 비트가 0이면 1byte 문자를 표현하는 DBCS라는 인코드 방식이 생겼다.(물론 이전의 ASCII 문서의 호환을 위해서이다.) 이를테면 16bit로 표현했을때 B0 A1 41 은 B0는 첫째 비트가 1이므로 B0 A1은 묶어서 디코딩을 하고 41은 첫째 비트가 0 이므로 41 한 바이트만을 디코딩해서 "가a"로 해석하는 방식이다.

그러나 앞서 말했듯이 인코딩과 디코딩 방식은 서로 합의가 되어 있어야 한다고 했는데 "가"를 "B0 A1"으로 디스크에 저장하는 인코딩과 디스크에 저장되어 있는 "B0 A1"을 "가"로 표현하는 디코딩 방식은 한국에서 사용하는 방식일뿐 일본은 일본 나름대로 중국은 중국 나름대로의 각기 다른 인코딩과 디코딩을 사용했기 때문에 여전히 지역적으로 다른 컴퓨터간의 문서 교환은 불가능했다. 머 그렇다라도 현실적으로 지역이 다른 컴퓨터간 문서교환은 그리 자주 일어나지 않았기 때문에 그리고 소프트웨어를 다른 나라에 팔때 이런 노가다식의 코드 변환 작업을 해주는 누군가가 있었기 때문에 크게 불편을 느끼지 않았다. 게다가 컴퓨터를 사용하는 인구는 아주 소수였기 때문에 이런 문제는 널리 인지되지는 않았다.

이 시기에 우리나라에서는 2byte를 패리티 1bit + 초성 5 bit + 중성 5 bit + 종성 5bit로 표현하자는 조합형 한글과 패리티 1bit + 그냥 15bit 코드로 사용하자는 완성형 한글 사이의 표준 다툼이 있었는데 당시 대표적인 아래아 워드 프로세스는 조합형 한글을 밀고 있었고 MS 워드는 완성형 한글을 지원하고 있었다. 국문학 대학 교수들도 사설을 통해 우리나라 문자는 조합형 한글이 더 적절하다고 주장하였지만 컴퓨터가 단순히 국내에서만 사용되는 것은 아닌 만큼 상대적으로 복잡한 코드 표현방식과 한글이 아니지만 자주 사용하는 다른 글자를 표현하는데 문제(즉 16bit가 모두 한글을 코드화하는데 사용되면 다른 그림문자나 한문등은 어떻게 해야 하는문제가 있다.)가 있었던만큼 승패는 이미 예견된 것이었다.

많은 사람들이 이때 MS Word의 로비나 밀어붙이기식의 방법때문에 어쩔수 없이 완성형 한글이 도입된거라고 오해하지만 사실 컴퓨터를 하는 입장에서는 이후 네크웍을 통한 문자 교환을 생각하면 완성형 한글이 좀 더 적합한 방식이었다고 생각한다. 조합형 한글은 현재 한글의 모든 한글을 표현할 수 있었지만 워드 프로세스에서 자주 사용되는 한자나 특수문자 등을 표현하기 위해 추가적인 코드 페이지를 필요로 했고(1+5+5+5= 2byte였기 때문에 여유 코드가 없다.) 완성형 한글은 패리티 1비트를 제외하고 32000개의 문자집합을 영역별로 나누어 한글뿐 아니라 자주 사용되는 한자, 일본어, 특수문자 까지 2byte의 코드 페이지에 같이 담아둘수 있는 장점이 있는 반면에 "햏"이나 "똠" "믜" "먄" "숖" 같이 자주 사용되지 않는다는 이유로 코드화 되지 않는 한글은 사용할 수 없는 단점이 있었다.

한국의 완성형과 조합형 같은 지역적인 문제들은 다른 아시아권에서도 마찬가지긴 했지만 그 해결책이 완벽한것은 아니더라도 어쨌든 그럴듯하게 작동하였고 컴퓨터에서 문자를 표현하는 문제는 잠시 해결된듯 보였지만 사실 더 큰 문제를 내포하였다.. 컴퓨터가 네트워킹화 되고 인터넷이라는게 생기면서 컴퓨터간 문자 교환의 문제는 지역간에 서로 다른 인코딩과 디코딩은 심각한 문제가 되었다. 이전까지 각 나라에서는 자기 나라만의(혹은 나라에서도 몇가지 계파로 갈라지거나) 인코드 디코드 코드 페이지를 사용하였는데 당연히 다른 나라 컴퓨터와 통신시 A나라에서 A'라는 인코등 방식을 사용해 글자를 보내면 B 나라에서는 B'라는 디코딩 방식으로 문자를 표현할려고 하였으므로 제대로 된 문서로 보이지 않았다. 특정 텍스트 파일의 인코딩 방식은 텍스트 파일 자체에 저장된 것이 아니었기 때문에 어떤 글자나 텍스트를 읽을때 어떤 디코딩 방식을 사용해야 할지 알수 없으므로 모든 글자는 ?로 표현되거나 알수 없는 - 소위 깨진 글자로 보이게 된다.


다음에..

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

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