'유니코드'에 해당되는 글 2건

  1. 2009.02.26 Global Software - Unicode와 Programming
  2. 2009.02.25 Global Software - Unicode
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