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