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