Skip to main content

Unicode와 Encoding에 대해 몰랐던 사실들

요새 XML parser를 만들어 본다고 encoding에 대해 좀 살펴보고 있는데 제가 그동안 알아왔던 것들과 많이 차이가 나네요. 사실 그동안 십수년 프로그래밍을 하면서 I18n이나 L10n을 고려해 본적은 없었거든요. ((Internationalization (I18n), Localization (L10n) )) 대부분 UI가 없거나 UI가 있더라도 그다지 한글화같은 것을 고려하지 않았도 되었던 것이기도 했고요.

가장 오해하고 있었던 사실은 Unicode는 16bit 크기안에 다 들어갈 수 있다라고 알고 있던 것이었습니다. 그런데 살펴 보니 이것은 옛날 얘기고 현재는 plane-0에만 해당하는 범위네요. 이외에도 plane-1부터 plane-16까지 가질 수 있더군요. 물론 대부분의 현재 사용되는 문자들은 16bit안, 즉 plane-0에 들어간다고 하지만 제대로 Unicode를 fixed length string으로 지원하려고 하면 16bit char로는 불가능하다는 말이 되죠.

시험삼아 간단히 컴파일을 해보니 Windows XP에서 사용한 VC++ 2005 Express에서는 wchar_t가 16bit지만 Ubuntu의 gcc에서는 32bit네요.

원래 계획은 XML parser를 만들면서 기본적으로 wchar_t로 결과를 리턴하고 encoding이 ascii (utf-8의 subset) 인 경우 char로 결과를 리턴하도록 template class로 만들려고 했었는데 wchar_t가 16bit인 환경에서는 어째야 할지 고민이네요. 어차피 현재 사용되는 대부분의 문자들이 16bit안에 들어가므로 plane-0만 지원해도 될것 같기도 한데... 아니면 basic_string 대신 basic_string를 사용하는 방법을 사용해야 할런지... 이렇게 하려면 저장될때 byte ordering도 고려해야 하고 수많은 traits이나 function overloading을 새로 만들어 주어야 하겠죠. string literal 쓰기도 어렵겠네요. :-|

만들려는 XML parser에서는 최소 ascii, utf-8, utf-16의 encoding을 지원하려고 합니다. 이중 utf-8과 utf-16 encoding의 경우에는 wchar_t를 사용해서 결과를 리턴받도록 하고요. 어차피 utf-8, utf-16과 같이 variable length를 가지는 encoding의 열을 리턴하더라도 사용하려면 fixed length string으로 변환해야 할것 같아 미리 wchar_t의 string, 즉 wstring으로 변환해서 리턴해 준다는 목적입니다. 하지만 모든 경우 wchar_t을 사용하면 encoding이 ascii인 경우 공간의 낭비가 많아지므로 이 경우에 특별히 사용할 수 있도록 char 버전을 제공하고요.

제가 이쪽에 기초가 없다보니 아직 모르는게 많네요. 혹시 제가 잘못 알고 있거나 빠진 내용에 대해 알려주시면 고맙겠습니다. :-)

Comments

  1. 제가 생각하기에 제일 쉬운(!) 방법은.

    다른 인코딩이 들어오면 iconv같은걸 돌려서 죄다 utf-8혹은 wchat_t(16bit)로 바꿔서 처리해버리세요. ㅎㅎ utf-8이라면 내부 처리단위가 std::string이 될것이고, wchar_t라면 std::wstring이 되겠네요. libxml같은 경우도 죄다 utf-8으로 변환해서 처리하던걸로 기억한다는.. :)

    ReplyDelete
  2. iconv 라는 게 있는지도 몰랐네요. ㅎㅎ 그런데 utf-8로 encoding되었더라도 이 encoding된 char가 어차피 unicode이기 때문에 0x10FFFF 까지 저장할 수 있어야 해서 wstring이 되어야 할 것 같아요. 그리고 만약 encoding이 ascii라고 확신하더라도 ascii를 이용하여 $#10FFFF; 해버리면 결국 wstring이 필요할 것 같고... 그나마 VC++에서 wstring은 0xFFFF까지밖에 안되고... oTL

    결국 지금은 wstring-base로 pull parser를 만들어 보고 있습니다. 일단 pull parser가 완성되면 SAX, DOM은 이것으로 만들면 될 것 같아서요.

    암튼 encoding, decoding 다 구현하려고 맘먹고 있었는데 iconv 라이브러리를 사용하는게 좋겠네요. 이제 문제는 input이 왔을때 이 input이 어떤 encoding인지를 검출하는 방법을 알아내는 것이네요. 좋은 라이브러리 알려주셔서 고맙습니다. :-)

    ReplyDelete
  3. 저도 iconv를 쓸 것 같은데요. utf-8라면 한글 "자르기"와 "글자수 세는 것" 외에는 그냥 아스키 쓰듯 string 쓸 것 같습니다. utf-8을 만든 이유도 기존 프로그램의 변경없이 쓸려고 만든 거니깐요. 작업물에서 한글을 제대로 자르는 것이나 글자수를 정확히 세는 것이 중요한가요?

    ReplyDelete
  4. 꼭 제대로 자르거나 글자수 세는게 문제가 아니더라도 utf-8 string가지고는 출력을 할수 없지 않나요? 결국 unicode를 사용해서 출력하려고 해도 decoding을 해야 할것 같아요. 물론 전체 utf-8 문서가 ascii만 가지고 있다면 모르겠지만요.
    아마 기존 프로그램의 변경 없이라는 얘기는 기존 ascii 문서의 변경없이라는 얘기가 아닐까요?

    게다가 전부 ascii라도 &12345; 같은 unicode 문자를 그냥 저렇게 사용하지는 않을 것 같고 하나의 문자로 나타내야 하는데 이런 것까지 고려한다면 결국은 ascii 포함해서 모든 encoding이 fixed length로 변환되려면 최소 21bit(전체 unicode)가 필요하지 않을까 생각중입니다. 물론 16bit만 있어도 현재 사용되는 모든 문자는 가능하다고 하니 이정도만 있어도 될것 같고요.

    2000 이전의 Windows NT 버전들을 이 16bit범위의 unicode만 지원했다고 하네요. 현재는 utf-16 지원.

    그리고 iconv외에 icu라는 ibm에서 시작한 라이브러리가 있군요. 좀 덩치가 큰것 같긴 하지만 살펴보고 있습니다. :-)

    ReplyDelete
  5. 윈도우즈는 utf-8을 표준 입출력으로 뿌려서 인식이 안되나요? 저는 리눅스나 BSD에서 할때는 utf-8 로케일에서 그대로 utf-8 string을 뿌려버립니다. 웹 프로그래밍할때도 utf-8 로케일이기 때문에 그냥 뿌립니다.

    만약에 윈도우즈가 표준입출력에 대한 로케일을 utf-8로 할 수 없다면 디코딩할 수 밖에 없겠네요.

    ReplyDelete
  6. 네. utf-8 인코딩을 지원하는 환경이라면 그냥 사용하셔도 되겠죠. 그리고 웹 프로그래밍에서도 그냥 utf-8을 사용하면 브라우저가 이를 지원할테니 문제 없을거고요.
    하지만 윈도우즈는 native encoding이 utf-16이니 utf-8을 바로 사용할수는 없을 것 같네요.

    현재로써는 어느 encoding이든지 wchar_t를 사용하는 wstring을 사용해서 구현해 볼 생각입니다. :-)

    ReplyDelete
  7. 결론은 iconv를 써서 후다닥 특정 인코딩으로 강제한 뒤에 파싱 ㄱㄱ 이군요.

    :)

    ReplyDelete
  8. 네. 지금 만들고 있는 parser는 일단 wchar_t로 변환 후 파싱합니다. fixed width character가 traversing 하기에 편해서요. ;-)

    ReplyDelete

Post a Comment

Popular posts from this blog

1의 개수 세기 - 해답

벌써 어제 말한 내일이 되었는데 답을 주신 분이 아무도 없어서 좀 뻘쭘하네요. :-P 그리고 어제 문제에 O(1)이라고 적었는데 엄밀히 얘기하자면 O(log 10 n)이라고 적었어야 했네요. 죄송합니다. ... 문제를 잠시 생각해보면 1~n까지의 수들 중 1의 개수를 얻기 위해서는 해당 숫자 n의 각 자리의 1의 개수가 모두 몇개나 될지를 구해서 더하면 된다는 사실을 알 수 있습니다. 예를 들어 13이라는 수를 생각해 보면 1~13까지의 수에서 1의 자리에는 1이 모두 몇개나 되는지와 10의 자리에는 모두 몇개나 되는지를 구해 이 값을 더하면 됩니다. 먼저 1의 자리를 생각해 보면 1, 11의 두 개가 있으며 10의 자리의 경우, 10, 11, 12, 13의 네 개가 있습니다. 따라서 2+4=6이라는 값을 구할 수 있습니다. 이번엔 234라는 수에서 10의 자리를 예로 들어 살펴 보겠습니다. 1~234라는 수들 중 10의 자리에 1이 들어가는 수는 10, 11, ..., 19, 110, 111, ... 119, 210, 211, ..., 219들로 모두 30개가 있음을 알 수 있습니다. 이 규칙들을 보면 해당 자리수의 1의 개수를 구하는 공식을 만들 수 있습니다. 234의 10의 자리에 해당하는 1의 개수는 ((234/100)+1)*10이 됩니다. 여기서 +1은 해당 자리수의 수가 0이 아닌 경우에만 더해집니다. 예를 들어 204라면 ((204/100)+0)*10으로 30개가 아닌 20개가 됩니다. 이런 방식으로 234의 각 자리수의 1의 개수를 구하면 1의 자리에 해당하는 1의 개수는 ((234/10)+1)*1=24개가 되고 100의 자리에 해당하는 개수는 ((234/1000)+1)*100=100이 됩니다. 이들 세 수를 모두 합하면 24+30+100=154개가 됩니다. 한가지 추가로 생각해야 할 점은 제일 큰 자리의 수가 1인 경우 위의 공식이 아닌 다른 공식이 필요하다는 점입니다. 예를 들어 123에서 100의 자리에 해당하는 1의 개수는 ((123/1

std::map에 insert하기

얼마전 회사 동료가 refactoring한 코드를 열심히 revert하고 있어서 물어보니 다음과 같은 문제였습니다. 원래 코드와 refactoring한 코드는 다음과 같더군요. nvp[name] = value; // original code nvp.insert(make_pair(name, value)); // refactored 아시겠지만 위의 두 라인은 전혀 다른 기능을 하죠. C++03에 보면 각각 다음과 같이 설명되어 있습니다. 23.1.2/7 Associative containers a_uniq.insert(t): pair<iterator, bool> inserts t if and only if there is no element in the container with key equivalent to the key of t. The bool component of the returned pair indicates whether the insertion takes place and the iterator component of the pair points to the element with key equivalent to the key of t. 23.3.1.2/1 map element access [lib.map.access] T& operator[](const key_type& x); Returns: (*((insert(make_pair(x, T()))).first)).second. 원래 코드는 매번 새 값으로 이전 값을 overwrite했지만 새 코드는 이전에 키가 존재하면 새값으로 overwrite하지 않습니다. 따라서 원래 기능이 제대로 동작하지 않게 된것이죠. 그래서 물어봤죠. "왜 이렇게 했어?" "insert가 성능이 더 좋다 그래서 했지." :-? 사실 Fowler 아저씨는 Refactoring 책에서 refactoring은 성능을 optimizing하기 위한 것이 아니다라

C++ of the Day #9 - Boost.Python 사용하기 #1

Python 은 가장 인기있는 interpret 언어중의 하나입니다. Python의 장점 중 하나는 C/C++ 모듈과 쉽게 연동할 수 있다는 점입니다. 물론 손으로 일일히 wrapper를 만드는 것은 손이 많이 가고 에러를 만들수 있는 작업이나 SWIG 등과 같은 도구를 사용하면 쉽게 python 모듈을 만들 수 있습니다. Boost.Python 은 이런 SWIG와 같이 python 모듈을 쉽게 만들 수 있도록 도와주는 라이브러리로 순수 C++만을 사용한다는 점이 SWIG와 다른 점입니다. 그리고 개인적으로는 Boost 라이브러리에 포함되어 있는 것들이 왠지 좀 더 믿음직스러워서... :-) 이번 글에서는 Boost.Python 문서에 나와 있는 예제 를 가지고 간단하게 python 모듈을 만드는 방법에 대해서 알아보겠습니다. Requirements 리눅스 이 글에서는 리눅스 환경에서의 사용 방법을 설명한다. Boost.Python 라이브러리 (1.33.1) Boost 라이브러리를 다운로드받아 아래와 유사한 명령으로 라이브러리를 빌드한다. bjam -sTOOLS=gcc -with-python install bjam의 --prefix 옵션으로 라이브러리가 설치될 위치를 변경할 수 있다. Python 라이브러리 (2.4.3) Python을 다운로드 받아 빌드하여 설치한다. 위의 경우와 유사하게 configure의 --prefix 옵션으로 설치될 위치를 변경할 수 있다. Write C++ Code 다음과 같이 코드를 작성한다. // greet.cpp #include <stdexcept> char const* greet(unsigned x) { static char const* const msgs[] = { "hello", "Boost.Python", "world!" }; if (x > 2) throw std::range_error("