출처: http://blog.naver.com/kbs3033?Redirect=Log&logNo=70003251127

윈도우에서 사용하는 문자 코드에 대한 이해

 


컴퓨터의 기본 용도가 문자열 처리를 필두로한 데이터 처리라는 점에는 이견이 없을 것이다. 그럼에도 불구하고 개발자들이 문자열 처리에 대한 문제를 간과하기 쉬운 것은 이와 관련된 내용을 다루는 책이 별로 없기 때문이다. 따라서 이번호의 주제를 '기본기를 다지자' 정했다. 글에서는 컴퓨터 내부에서 다루는 문자열 처리를 비롯해 이와 관련된 함수에 대해 살펴보겠다.




지난 3월호에 첫 글이 나간 후 많은 독자들이 메일을 보내왔는데, 이번 글을 쓸 때 그 의견들을 많이 참고했다. 메일의 대부분은 한글과 영문 문자열의 처리 문제와 유니코드 처리, 다국어 문자의 표현에 관련된 것이었는데 한결같이 제대로 정리된 자료가 없다는 내용이었다. 물론 MSDN에 자료가 있긴 하지만 사람들이 주로 관심을 갖는 내용이 아니고, 또 체계적으로 정리가 돼 있거나 번역된 것이 전무하다는 것도 문제점으로 지적할 수 있다.

요즘 나오는 프로그래밍 관련 책들은 모두 사용자 인터페이스에 집중된 얘기일 뿐 가장 기본이 되는 문자열 처리에 대해서는 거의 다루고 있지 않아 프로그램을 시작하는 사람들이 두려움을 갖기 쉽다. 필자가 알고 지내는 어떤 사람이 프로그래머 채용 시험에 기본적인 텍스트 파일 처리 - 라인 수를 세거나 단어를 세는 등 - 에 대한 문제를 출제한 적이 있는데, 시험에 응시한 대부분의 사람이 거의 풀지 못하는 것을 보고 한탄했다는 얘기를 들은 적이 있다. 컴퓨터의 기본 용도가 데이터 처리, 특히 문자열 처리라는 것을 감안할 때, 얼마나 많은 프로그래머 입문자들이 기본은 무시한 채 눈에 보이는 겉멋 부리기에만 열중하고 있는지 짐작할 수 있다. '기본'을 다루는 책은 팔리지 않기 때문에 서점에 안나오지는 모르겠지만, 최소한 이 글에서 만이라도 기본기를 다뤄 보도록 하자. 이번호에는 컴퓨터 내부에서 다루는 문자열 처리에 대해 살펴보고, 이와 관련된 함수들을 정리해 보겠다.

 

한 바이트 문자 집합

컴퓨터를 처음 배울 때는 영문자를 표현하는 아스키(ASCII) 코드라는 것을 배운다(지난호에서 이를 ISO/IEC 646이라고 하고, 이것의 8비트 확장인 ISO/IEC 2022도 사용되고 있다고 했다). 그러면 아스키가 컴퓨터에서 표현된 맨 처음 문자 코드인가하면 그렇지는 않다. IBM 대형 컴퓨터에서 사용했던 EBCDIC 코드도 지금까지 사용되고 있고, 7비트인 아스키 코드를 기반으로 8비트 확장을 통한 그래픽 문자를 정의해 사용한 적도 있다. 그리고 이렇게 만들어진 문자에 번호를 붙인 것을 문자 집합(character set)이라고 한다. 7비트였던 아스키는 0x20부터 0x7E까지 정의돼 있으며, 8비트인 한 바이트 내에서 다른 영역(0x80 이후의 값)은 컴퓨터 제조회사마다 달라졌다. 이를 OEM 문자 집합(Original equipment manufacturer character set)이라고 하며, 흔히 MS 도스에서 사용하는 문자 집합을 말한다.

하지만 윈도우에서 문자를 출력할 때 사용하는 문자 집합은 OEM 문자 집합이 아닌, ANSI 문자 집합이다. 따라서 아스키 128 이후의 값은 OEM 문자 집합과는 다를 수밖에 없다. OEM 문자 집합은 각 국가마다 자신들의 언어 특성을 반영해 변형시켰는데, 이렇게 변형된 문자 집합을 일컬어 코드 페이지라고 한다. 미국에서 일반적으로 사용되는 OEM 코드 페이지 값은 437이며, 우리나라는 949이다. 도스 창에서 hcode /e chcp 437이란 명령을 주면 영문 코드 페이지를 사용하겠다는 것이고, hcode /k chcp 949로 주면 한글 코드 페이지를 사용하겠다는 의미이다. <화면 1>은 보조 프로그램 중 글꼴이 표현할 수 있는 글자들을 보여주는 '문자표'라는 프로그램으로, Times New Roman 글꼴을 보여주고 있다.

코드페이지 437 ANSI 문자 집합에서 한글 글꼴을 선택하면 128번부터 255사이의 글자들이 나타나지 않는다. 이는 이 사이의 글자들이 DBCS(Double-byte Character Set)에서 사용되기 때문인데, 영문 프로그램 중에서 나 과 같은 128번 이후의 글자를 이용해서 문서를 만든 경우 화면에 이들 글자 대신 '', ''과 같은 이상한 글자들이 나타나는 것이다. 이것은 코드 페이지를 혼용해 사용하는 것을 윈도우의 출력 시스템이 영문만으로 이뤄진 문자열임을 파악하지 못하고 한글을 처리하듯 했기 때문인데, 이 부분은 쉽게 고쳐지지 않을 것으로 보인다.

다시 OEM ANSI 문자 집합 얘기로 돌아가면 코드 윈도우의 파일 시스템인 FAT16은 파일 이름을 OEM 문자 집합을 사용하며, 윈도우는 ANSI 문자 집합을 사용하기 때문에 윈도우 프로그램을 만드는 경우 문자 변환을 위해 윈도우 API에서는 두 개 코드 간의 변환을 위한 함수를 마련해 놓고 있다. 이 함수들이 Win32 API로 넘어오면서 다른 이름으로 바뀌고 예전의 이름들은 단지 호환을 위해서 제공되고 있을 뿐이다. < 1>을 보면 Ansi라는 접두어가 Char로 바뀐 것을 볼 수 있는데, 이는 Char로 시작하는 함수가 Ansi 문자 집합만을 의미하는 것이 아니라 컴파일 옵션으로 _UNICODE를 정의하면 유니코드 문자 집합을 의미하기 때문에 Ansi라는 접두어를 표기한 것이다.

<표 1> OEM과 ANSI 코드 간의 변환을 위한 함수
Win16 Win32
AnsiLower CharLower
AnsiLowerBuff CharLowerBuff
AnsiNext CharNext, CharNextExA
AnsiPrev CharPrev, CharPrevExA
AnsiToOem CharToOem
AnsiToOemBuff CharToOemBuff
AnsiUpper CharUpper
AnsiUpperBuff CharUpperBuff
OemToAnsi OemToChar
OemToAnsiBuff OemToCharBuff

 

< 1>의 함수 중 CharNext CharPrev와 같은 함수들은 문자열 안에서 포인터가 가리키는 문자로부터 한 글자 앞이나 뒤로 이동하는 경우에 사용되는데, 현재 윈도우의 코드 페이지가 DBCS 코드를 다룰 수 있다면 단순히 한 바이트 앞이나 뒤로 이동하지 않고 글자의 코드를 판단해 두 바이트 앞이나 뒤로 이동한다. 명확하게 코드 페이지를 지정하고자 한다면 CharNextExA CharPrevExA를 사용할 수 있을 것이다.

현재 시스템이 사용하고 있는 코드 페이지를 알아내는 함수는 GetACP이고, 현재 시스템의 OEM 코드 페이지를 알아내는 함수는 GetOEMCP이다. 함수에서 돌아오는 값에 대한 정보는 도움말을 찾아보면 알 수 있고, 현재 사용하고 있는 코드의 최대 바이트 수를 알려면 GetCPInfo 함수를 사용해야 한다. 유니코드를 사용하거나 DBCS 코드 시스템이라면 한 글자가 나타낼 수 있는 최대 바이트 수는 2를 가리키며, 그렇지 않다면 1을 가리킬 것이다.

<예제 1>은 간단하게 이들 함수의 사용 예를 보인 것이다. 컴파일하는 방법은 도스 창을 열고 이 소스가 있는 곳에서 다음과 같이 하면 getcp.exe 실행 파일이 만들어진다.

cl getcp.c

물론 도스 창에서 컴파일할 수 있게 컴파일러와 환경 변수가 설정돼 있어야 할 것이다.

<예제 1> getcp.c

#include <windows.h>

 

int main(void)

{

   CPINFO cpInfo;

   int i;

   BYTE* q;

 

   printf("Current Ansi Code Page is %d\n", GetACP());

   printf("Current OEM Code Page is %d\n", GetOEMCP());

 

   if (GetCPInfo(CP_ACP, &cpInfo)) {

      printf("MaxCharSize is %d\n", cpInfo.MaxCharSize);

      printf("The size of DefaultChar is %d,

         Char is ", MAX_DEFAULTCHAR);

      for (i = 0; i < MAX_DEFAULTCHAR; i++)

         printf("%c", cpInfo.DefaultChar[i]);

      printf("\n");

      printf("The size of LeadByte is %d:\n",

         MAX_LEADBYTES);

      printf("Byte : ");

      q = cpInfo.LeadByte;

      for (i = 0; i < MAX_LEADBYTES; i++)

         printf("%0x ", *q++);

      printf("\n");

   }

   return 0;

}

 

[실행 결과]

 

c:\docs\techcolumn>getcp

Current Ansi Code Page is 949

Current OEM Code Page is 949

MaxCharSize is 2

The size of DefaultChar is 2, Char is ?

The size of LeadByte is 12:

Byte : 81 fe 0 0 0 0 0 0 0 0 0 0

두 바이트 문자 집합

DBCS는 소위 확장 8비트 문자 집합이라고 한다. 문자를 나타내는 단위가 한 바이트 혹은 두 바이트이기 때문에 일종의 ANSI 문자 집합으로 간주된다. 한자(漢字)를 사용하는 국가들은 한 바이트에서는 표현이 불가능하기 때문에 DBCS 코드 시스템을 사용하고, 한글처럼 넓은 코드 영역을 차지하는 경우에도 DBCS를 사용한다. 대표적인 국가들이 중국, 일본, 한국(줄여서 CJK라고 한다)이다. 중국이나 일본 문화에서는 한자가 아주 큰 비중을 차지하는 반면, 우리나라의 경우는 한자를 사용하고 있긴 하지만 예의 두 나라에 비해서는 사용 빈도가 떨어진다고 볼 수 있다. 이들 국가가 사용하는 한자(ideography)들은 유사성도 많지만 각기 다른 한자를 사용하고, 사용 빈도도 다르다(중국은 본토와 대만이 서로 다른 코드 시스템을 사용하고 있다). 하지만 유사한 한자들을 통합하는 것이 중요한 문제로 대두돼 그것이 유니코드에서 결실을 보았다.

그러나 유니코드는 각 국가 코드 간의 교환을 목적으로 하기 때문에 쉽게 받아들여지지 않아 국가마다 기존의 코드 체계를 계속 사용하고 있다. 이런 문제들로 인해 개발자들은 DBCS 코드 시스템을 사용하기 위한 많은 테크닉들을 익혀야 했다. 얼마 전까지만 해도 대부분의 문서들은 조합형과 완성형의 두 가지 형태가 비슷한 비율로 만들어졌지만, 요즘은 인터넷 사용자의 증가로 인해 완성형이 주로 사용되고 있다. 여기에는 윈도우 시스템에서 제공하는 DBCS가 완성형을 바탕으로 구성돼 있는 것도 큰 영향을 미쳤다(이 글에서는 네트웍을 통한 한글 문자의 교환에 대해서는 다루지 않고, 한글 코드 인코딩 방식이 다양하며 통합에 어려움을 겪고 있다는 정도만 언급하겠다).

현재 한글 윈도우에는 통합형(확장 완성형, 또는 통합 완성형이라고도 한다) DBCS가 사용되고 있다. 한글 윈도우 95에서 코드 시스템은 기존 데이터의 이용과 유니코드와의 코드 변환을 쉽게 하기 위해 기존의 완성형 2,350자 외에 11,172자에 속한 한글을 코드 영역 여기저기에 분산, 배치했다. 물론 기존의 DBCS와 빠른 변환을 위한 것이었겠지만 다른 방법도 있지 않았나 하는 아쉬움이 남는다(조합형에서 침범한다던 코드 영역을 확장 완성형은 그대로 사용하고 있다). 덕분에 코드의 배열이 사전 순서와는 완전히 별개로 이뤄져 통합형 코드를 이용한 정렬은 strcmp와 같은 함수로는 정확한 결과를 얻을 수 없기 때문에 이런 함수를 이용한 프로그램은 정상적인 동작을 기대할 수 없다. 더구나 2,350자 이외의 한글은 순서를 어떻게 잡아야할지 대책도 없는 것 같다. CompareString과 같은 함수나 _mbscoll과 같은 DBCS용 런타임 라이브러리 함수로 처리하면 된다고 말할 수도 있겠지만, 기존 프로그램을 고려했다기보다는 개발하는 측의 편의만을 앞세웠다는 것에는 변명할 여지가 없을 것이다.

이런 모든 문제는 기존의 코드가 유니코드로 변환되면 해결된다지만 유니코드로 표현하지 못하는 한글도 있다는 게 또 다른 문제로 등장한다. 혹자는 11,172자가 한글이 표현할 수 있는 글자의 전부가 아니냐고 반문할 지도 모르겠다. 물론 초성 19, 중성 21, 종성 27개의 자소로 표현할 수 있는 한글의 가지수가 총 11,172개이지만(종성이 없는 경우도 고려했다), 여기에는 초성 없이 중성과 종성으로 이뤄진 567개의 글자가 빠져 있다. 사전에서는 '- 다', '- 다'와 같이 초성이 없는 글자를 표현하는 경우가 있다. 즉 유니코드만으로는 초등학교 국어 교과서는 만들 수 없는 것이다.

여기까지는 분명 DBCS 코드와는 좀 거리가 있는 내용이다. 하지만 한글 조합에 대한 명확한 이해 없이 완성형 코드만을 고집했기에 드러나는 문제점을 그냥 접어 둘 수만은 없어 이렇게 얘기를 꺼내게 된 것이다. 혀재 한글 윈도우 95에는 Iso10646.exe라는 프로그램이 제공되는데, 이것은 이전에 MS가 통합형이라는 코드를 들고 나오면서 국내 표준의 혼란을 우려한 반응 때문에 완성형만 입력 가능하게 했지만, 이 프로그램을 사용하면 통합형 11,172자가 모두 입력 가능해진다. 결국 코드를 사용하면 기존의 완성형만 고려한 프로그램들은 정상적인 동작을 못하거나 잘못된 결과를 나타낼 것이다. 통합형에 대한 여러 가지 대처 방법이 있겠지만 현재로서는 사용자가 완성형만을 입력하기를 바랄 수 밖에 없을 것이다(<화면 2>). 통합형은 유니코드와 완성형의 교량 역할을 하는 코드로서는 분명 적절한 선택이지만 그 파급 효과에 대해서는 무책임했다고 할 것이다.

다시 본론으로 돌아가자면 DBCS 코드는 첫번째 바이트의 영역에 따라 문자가 한 바이트 코드인지 두 바이트 코드인지를 결정한다. 보통은 0x80보다 크면 두 바이트로 문자를 처리한다. 따라서 문자열을 다룰 때 현재 위치의 바이트만을 보고는 이 글자가 두 바이트로 표현된 문자의 첫째 바이트인지, 둘째 바이트인지 알 수 없다. 윈도우에서는 이것을 검사하는 함수를 제공하는데, 현재 바이트가 첫째 바이트인지를 알아내는 함수가 IsDBCSLeadByte. 하지만 이 방법은 현재 코드 페이지에서 두 바이트를 갖는 문자의 첫째 바이트의 범위(완성형의 경우 0x81부터 0xFE까지)를 검사해 알아내는 것이고, 문자열 중간에서 검사하는 방법은 런타임 라이브러리 안에 있는 함수를 사용해야 한다. 문자열 안에서 첫번째 바이트(Lead Byte)를 검사하는 방법은 문자열의 처음부터 검사해서 현재 가리키고 있는 문자가 DBCS 코드의 첫째 바이트(lead)인지 둘째 바이트(trail)인지를 판별하는 방법이다. 비주얼 C++에서 제공하는 런타임 라이브러리 중에 _ismbslead, _ismbstrail이라는 함수가 있는데, 이들은 문자열의 시작 포인터도 같이 받는다. 즉 문자열 중간에서 단순히 문자만을 비교하면 정확한 값을 얻을 수 없는 것이다.

<예제 2> ismbslead ismbstrail 함수 사용 예

#include <windows.h>

 

int main(void)

{

   char str[]= "abc 1.대한민국 2.완성형 3.조합형 4.통합형 efg";

   char* p = str;

 

   printf("str : %s\n", str);

   printf("byte: ");

   while (*p) {

      if (_ismbblead(*p))

         printf("^");

      else if (_ismbbtrail(*p))

         printf(".");

      else

         printf(" ");

      p++;

   }

   printf("\nstr : ");

   p = str;

   while (*p) {

      if (_ismbslead(str, p))

         printf("|");

      else if (_ismbstrail(str, p))

         printf(".");

      else

         printf(" ");

      p++;

   }

   printf("\n");

   return 0;

}

 

[실행 결과]

c:\docs\techcolumn>islead

str : abc 1.대한민국 2.완성형 3.조합형 4.통합형 efg

byte: ...^^   ^^^^^^^^   ^^^^^^   ^^^^^^   ^^^^^^ ...

str :    |.   |.|.|.|.   |.|.|.   |.|.|.   |.|.|.

결과는 ismbslead이나 ismbstrail를 쓰지 않으면 문자열에서 첫째 바이트인지 둘째 바이트인지 정확하게 판단할 수 없다는 것을 보여준다. ismbblead IsDBCSLeadByte는 동일한 결과를 보인다. 문자열 안에서 정확하지 않지만 이 함수들은 각기 다른 상황에서 유용하게 쓰인다.

그밖에 DBCS 코드를 위한 런타임 함수들이 있다. 이 함수들을 사용하려면 windows.h include하기 전에 #define _MBCS라고 하든지 컴파일 옵션으로 /D _MBCS라고 준다. locale.h include하고 main이 시작한 후 관련 함수들을 사용하기 전에 다음과 같이 지정해 주어야 한다.

setlocale(LC_ALL, "kor");

뒤의 문자열은 각 국가별로 다르므로 유의하기 바란다. 이 명령이 없으면 디폴트로 영문자를 처리하기 때문에 정상적인 동작을 하지 않는다.

 

<표 2> DBCS 문자열에 유용한 함수들
_mbslen DBCS 문자열의 글자 수를 세어 준다. 바이트 수가 아님에 주의!
_mbstrlen DBCS 문자열의 글자 수를 세어 준다. 각 문자가 범위 안에 있는지 검사한다.
_mbsinc DBCS 문자열에서 다음 문자로 이동한다. DBCS 문자를 처리해 준다.
_mbsdec DBCS 문자열에서 이전 문자로 이동한다. DBCS 문자를 처리해 준다.
_mbscoll DBCS 문자열끼리 사전 순서로 비교한다.
mbstowcs DBCS 문자열을 유니코드로 변환한다(MultiByteToWideChar 참조).
wcstombs 유니코드 문자열을 DBCS 문자열로 변환한다(WideCharToMultiByte 참조).

 

DBCS 코드를 다룰 때 주의할 점은 0x80이 넘어가는 문자를 담고 있는 변수가 비교나 다른 연산을 하는 경우 0보다 작은 값으로 취급돼 에러가 발생할 수 있다는 것이다. 이 때는 변수를 unsigned char로 선언해 쓰든가 연산할 때 캐스팅하는 방법을 쓰면 안전하다. 좀더 강력한 방법으로 컴파일 옵션에서 /J 옵션을 주면, 디폴트로 컴파일할 때 char unsigned char로 간주된다.

그 밖의 주의사항으로 문자열에서 DBCS 문자가 취급되는 바이트의 배열 순서는 빅 엔디안(Big endian) 방식으로, 두 바이트로 코드를 표시할 때 상위 바이트가 문자열에서 먼저 나타나기 때문에 다음과 같은 코드는 상위 바이트와 하위 바이트가 반대로 돼서 들어간다. 실수하기 쉬운 것이므로 문자열 이외의 곳에서는 바이트 순서를 뒤바꿔 관리하는 경우가 없도록 주의하는 것이 에러를 막는 방법이다.

char* p;

unsigned short code;

....

if (IsDBCSLeadByte(*p))

   code = *(unsigned short*)p;

OEM 코드 페이지 중 한글 완성형은 949이고, 한글 조합형은 1361이다. 완성형을 조합형으로 변환하거나 조합형을 완성형으로 변환하려면 유니코드로 일단 변환시킨 후에 다시 다른 코드로 변환해야 한다. 이때 사용하는 함수가 MultiByteToWideChar WideCharToMultiByte이다. 이 함수의 시작 파라미터로 코드 페이지를 줄 때 1361이나 949를 주면 해당 DBCS를 조합형이나 완성형으로 인식하고 변환한다.

 

유니코드

윈도우 프로그램 작성 시에 유니코드를 사용할 것을 권장하는 가장 큰 목적은 소프트웨어의 각 나라별 지역화(localization)가 쉽기 때문이다. 또 여러 말로 쓰인 텍스트를 처리하기 쉽기 때문인데, 이는 영어권 국가의 프로그래머들이 일본과 같이 DBCS 코드 시스템을 사용하는 프로그램에 따로 노력을 들이지 않고 프로그램을 만들 수 있도록 하는데 실제 목적이 있다고도 하겠다. 이것은 국내 프로그래머들에게는 상대적으로 좋은 점이 없다고도 할 수 있겠지만, 일본이나 중국과 같은 곳에 프로그램을 수출할 때는 많은 도움이 될 것이다. 하지만 현실은 국내의 한글 처리조차 유니코드로 처리되지 않고 있다. 이는 대부분의 문서가 완성형으로 만들어져 관리되고 있고, 또 대량의 문서를 만들어 내는 프로그램들이 유니코드로 데이터를 처리하고 있지 않기 때문이다.

지난 번에도 언급한 적이 있지만 한글 윈도우 95는 유니코드를 지원하는 함수가 기본적인 것밖에 없기 때문에 유니코드를 처리하려면 프로그램 내에서 윈도우 API를 사용하지 않고 wcs*로 시작하는 런타임 라이브러리를 사용하는 것이 좋다. NT 95에서 동시에 동작하는 바이너리 파일을 만든다면 프로그램 안에서 문자열을 다루는 API 함수를 부르는 경우에는 반드시 ANSI API를 부르도록 해야 한다. 이렇게 하면 NT에서는 속도가 느려지겠지만, 95에서 안정적인 동작을 보장하기 위해서는 어쩔 수가 없다. 따라서 MFC를 사용해 프로그래밍하는 경우에도 NT에서만 동작하려고 하는 것이 아니라면 컴파일 옵션으로 _UNICODE를 주어서는 안된다. MFC도 이 옵션에 따라서 호출되는 함수가 유니코드용과 ANSI용으로 바뀐다.

한 소스로 유니코드용과 ANSI용으로 컴파일하려 한다면 TCHAR 사용을 권장하지만, 유니코드용과 ANSI용 함수를 섞어 부른다면 TCHAR와 혼동되지 않게 주의해야 한다. 유니코드 문자열을 다루는 것은 2바이트 정수 배열을 다루는 것과 동일하므로 포인터 연산을 하는 경우 바이트 문자열을 다루는 것에 주의해야 한다. 다음은 간단한 코딩 예이다.

LPTSTR psEnd, psStart, pText;

....

nCount = psEnd - psStart;          // 두 포인터 사이의

      // 문자 개수

nLen = (psEnd - psStart) * sizeof(TCHAR); // 두 포인터 사이의

      // 바이트 수

....

chCur = *pText;   // 한 글자를 chCur

    // 대입하고 다음 글자로 포인터 이동

pText = CharNext(pText);

 

앞의 코드에서 CharNext 대신 pText++와 같은 명령을 사용할 수 있겠지만, DBCS의 경우에는 쓸 수 없으므로 CharNext _tcsinc 함수를 쓰면 _wcsinc _mbsinc, _strinc로 맵핑돼 쓸 수 있다.

단순히 이런 문자열 처리 이외에도 유니코드를 사용하기 전에 주의해야 할 것이 있다. MFC를 사용하면 실수가 적겠지만 API를 직접 호출하는 경우라면 문자열을 넘기는 부분이나 메시지에서 받는 문자열들은 유니코드 옵션을 주고 컴파일했는지 아닌지에 따라서 유니코드일 수도 있고 ANSI 코드일 수도 있다. 참고로 해당 윈도우가 유니코드 문자열로 메시지를 처리하는지를 판단하는 함수는 IsWindowUnicode이다. 윈도우 프로시저를 서브 클래싱하는 경우에 있어서 유니코드를 처리하는 프로그램이 ANSI로 처리하는 윈도우의 프로시저를 서브 클래싱하거나 그 반대의 경우에도 자동으로 메시지 변환이 이뤄지므로 서브 클래싱할 때 어떤 방식으로 했는지만 주의한다면 문제가 없을 것이다. SetWindowLongW로 서브 클래싱을 하면 유니코드로 처리하겠다는 것이고, SetWindowLongA로 서브 클래싱을 하면 ANSI로 프로시저를 처리하겠다는 것을 말한다.

이밖에도 유니코드 관련 함수들로는 유럽의 발음 기호들을 유니코드 내의 코드로 변환시켜 주는 FoldString이나 일어에서 히라가나나 가타카나 간의 변환을 해주는 LCMapString과 같은 복잡한 기능을 하는 변환 함수도 있다. 좀더 자세한 것을 알고 싶으면 NLSAPI(National Language Support API) 스펙 4.5 MSDN 안에 있으므로 잘 읽어보기 바란다. MS는 자사 제품이 세계적으로 널리 사용되기를 원하기 때문에 각 국가 언어에 대한 지원을 무척 잘하고 있다. 체계적인 정리와 충실한 구현까지 웬만한 함수들은 런타임 라이브러리 안에 모두 들어있다. 대부분은 일본어에 대한 지원이라 아쉽지만 그 덕택에 한국어와 중국어가 쉽게 지원되는 것은 다행이라 인정해야 할 것이다.

유니코드가 분명 현재 전세계적으로 사용하는 문자의 대부분을 표현할 수 있는 것은 사실이지만, 각 국가의 모든 요구 조건을 충족시키지는 못한다. 여전히 각 국가별로 사용되는 문자 집합들은 유니코드와 병행해 사용되는 것이 있으리라 생각한다. 또한 옛 한글이나 옛 한자와 같이 현재 사용하지는 않지만 문서 처리상 필요하다면 유니코드를 바탕으로 한 변형 유니코드 등을 생각할 수 있을 것이다. 하지만 여전히 유니코드와의 호환 문제는 남아있고, 한 글자의 크기가 16비트밖에 안된다는 것이 제한점으로 남아 있다. 이런 경우는 결국 한 문자의 크기를 32비트로 표현할 수밖에 없는데 이것에 대한 얘기는 다음으로 미루기로 하겠다.

 

IME

앞서 얘기한 것은 한 나라에서 사용하는 글자를 컴퓨터의 메모리에서 어떻게 표현하고 다루는가에 대한 것이었다. 지금부터 얘기할 것은 글자를 입력하는 방법과 화면이나 종이에 출력하는 방법이다. 여기서는 글자를 입력하는 방법에 대해서 간단히 살펴보려고 한다.

MS는 응용 프로그램의 변경을 최소화하면서 세계 각국의 나라에서 자신들의 글자를 쉽게 입력할 수 있는 방법을 제공하려고 했다. 영어권에서 사용하는 글자들은 단순히 키보드에 있는 글자와 일대일 대응을 하기에 하나의 키를 누를 때마다 해당 키에 대응되는 문자가 눌린 것으로 판단해 간단하게 프로그램에서 처리할 수 있다. 하지만 발음 기호와 영문자가 조합되는 유럽권 글자나 자소가 결합하는 한글, 이루 셀 수 없이 다양한 입력 방식을 가진 한자 등은 키보드와 일대일로 대응할 수 없다. 한 개의 글자가 이뤄지기 위해서는 몇 번의 키를 입력해야 하고 입력된 키를 해석해서 문자를 만들어 내야 하는 프로그램이 있어야 하기 때문이다. 이러한 프로그램들은 글자를 입력받는 응용 프로그램의 일부분으로 제공됐는데, 그 대표적인 프로그램으로 도스용 아래아 한글을 들 수 있다. 윈도우로 넘어오면서 MS는 이러한 입력 부분에서의 문제를 운영체제 차원에서 기본 기능으로 제공하면 응용 프로그램을 만드는 측에서 수월해질 것이라고 판단했고, 그 결과 IME(input method editor)라는 기능을 제공함으로써 윈도우에서의 입력 문제를 해결하려고 했다.

극동 지역(far east, 즉 한국, 중국, 대만, 일본) IME는 일본어 지원 IME를 바탕으로 해서 만들어졌다고 볼 수 있다. 일본어의 입력 방법은 다양하게 있지만 윈도우에서 제공하는 것은 발음을 영어로 입력하면 일본어 사전을 뒤져서 그 발음과 유사하거나 발음으로 시작하는 단어들을 제시(candidate)하고 선택하게 한다. 마치 한글 발음을 치고 한자 키를 눌러 한자를 선택하는 방법과 유사하지만 일본어 입력은 한 글자만 선택하는 것이 아니라 여러 단어를 입력할 수도 있다는 점에서 차이가 있다. 그렇기 때문에 일본어 IME는 응용 프로그램과 매우 밀접하게 메시지를 주고받으면서 동작한다. 또 중국어는 매우 다양한 입력 방식이 있지만 단어를 입력하기보다는 한자 한 개를 입력하는 방법이 주로 사용되며, 한글도 유사한 방식으로 입력이 가능하다. IME의 복잡도로 본다면 일본어, 중국어, 한국어의 순서가 될 것이다.

극동 지역 IME의 개발을 위해서는 일본어를 입력할 수 있는 것만 만들면 다른 것은 간단히 해결된다. 예를 들어 극동 지역 윈도우에 포함된 워드패드는 리치 에디트 컨트롤을 사용해 만든 프로그램인데, 리치 에디트 컨트롤은 극동 지역 윈도우 간의 바이너리가 동일하다. 즉 같은 프로그램으로 각국 윈도우에서 IME를 이용해 동작한다는 것이다. 예전에 베타 버전인 아웃룩 익스프레스(Outlook Express) 프로그램을 사용할 때 조합중인 글자에 점선으로 밑줄이 그어져 있고 색이 다르게 처리되는 것을 본 적이 있다. 이것은 일본어 IME에서 글자를 입력하는 방식으로, 최종 마무리가 각국별로 되기 전이었기 때문인데 이를 통해 일단 일본어 버전부터 만든다는 것을 짐작할 수 있었다. 좀 불안하기는 하지만 일본어 윈도우에 한글 IME 파일을 복사해 쓸 수도 있는 것을 보면 그 구조의 유사성이 얼마나 높은지를 알 수 있을 것이다.

일단 IME의 구조를 살펴보면 좀 답답하게 설계했다는 느낌이 든다(너무 많은 자원을 소모하는 것을 볼 수 있다). 극동 지역의 윈도우는 일단 응용 프로그램이 시작하면 IME를 사용하기 위해서 다음과 같은 5개의 팝업 윈도우를 생성한다.

① IME : 시스템에서 제공하는 표준 IME 윈도우, 다섯 개중 가장 최상 위에 있다.
② MSIME95 :
한글 입력을 위해 제공된 IME, 클래스 이름은 바뀔 수 있다
.
③ IMESTATE : IME
의 상태를 표시하는 윈도우, /, 전각/반각, 한자 상태를 표시한다
.
④ IMECOMP :
조합 중인 글자를 표시하는 윈도우, 프로그램에서 위치를 지정한다
.
⑤ IMECAND :
선택할 수 있는 한자 리스트를 표시한다.

MSIME95 IME 클래스로 만들어진 윈도우를 오너(Owner) 윈도우로 삼고, IMESTATE, IMECOMP, IMECAND 클래스로 만들어진 윈도우는 MSIME95를 오너 윈도우로 갖고 있다. IME 윈도우의 오너 윈도우는 해당 응용 프로그램에서 만든 팝업 윈도우로 지정돼 있다. 오너 윈도우에 대해 생소한 사람들을 위해 간단히 설명하면, 부모/자식 윈도우와 비슷한 관계로 팝업 윈도우를 만들 때 오너 윈도우를 지정함으로써 윈도우들의 종속 관계를 지정하는 방법이다.

앞에서 언급한 클래스로 만들어진 5개의 윈도우의 관계를 보면 제일 먼저 IME 클래스로 윈도우를 만들고(디폴트 IME라는 타이틀을 갖지만, 화면에 보이지는 않는다), 시스템에서 제공하는 클래스인 MSIME95 클래스를 이용해 윈도우를 만든다. 이 윈도우 생성 시에 IMESTATE, IMECOMP, IMECAND의 순서로 윈도우를 만들게 된다. 보통 화면에 IME로 보이는 것은 IMESTATE 클래스에서 만들어진 윈도우이다. 이것은 거의 사용하지도 않는 전각/반각 상태를 - 일본 IME 영향이라 생각한다 - 표시하고 있으며, 한글 윈도우 3.1에서는 수시로 화면에 나타나기 때문에 화면에 표시하지 않으려고 했던 프로그래머(필자라고 눈치챘겠지만)에게는 무척이나 고생스러운 윈도우였다.

응용 프로그램에서 IME를 사용하는 방법은 세 가지로 분류된다. 첫째는 IME를 고려하지 않고 만들어진 프로그램으로 대부분의 영문 프로그램들이 여기에 속한다. 이들 프로그램은 디폴트 IME 클래스가 키보드 메시지를 처리해서 영문의 경우에는 WM_CHAR 한 개를, 한글이나 한자의 경우에는 WM_CHAR 두 개를 연속으로 발생시켜 프로그램이 WM_CHAR를 처리할 때 입력되도록 한다. 경우에 따라서는 화면에 이상한 글자가 찍히면서 화면을 업데이트해야 글자가 정상으로 보인다. 외국의 유명 에디터가 이런 경우에 해당하는데 MS 제품들은 거의 이와는 다른 방법을 사용한다.

둘째는 IME를 전부 사용하기에는 너무 복잡해서 최소한의 노력만을 기울여 프로그램을 만든 경우로 IME의 기능을 일부만 지원하는(half aware) 프로그램들이다. IMECOMP 윈도우를 적당한 위치로 옮겨 놓고 WM_CHAR 대신 WM_IME_COMPOSITION이나 WM_IME_CHAR과 같은 메시지를 이용해 완성된 글자를 처리하는데 보기에는 깔끔하지 않아도 글자를 처리하는데 있어서는 문제가 발생하지 않는다.

셋째는 IME의 기능을 API를 이용해서 직접 사용하는 프로그램들(full aware) IMECOMP 윈도우 외에도 UI 윈도우를 컨트롤하며 제공되는 메시지를 제대로 처리하는 프로그램들이다. 주로 MS 제품들이나 워드프로세서들이 여기에 속한다. 해야 하는 일이 많아서 제대로 하려면 작업이 상당하다.

대부분의 프로그램들이 입력을 에디트 컨트롤에서 받아 처리하기 때문에 IME를 직접 다루지 않지만, 프로그램에서 에디트 컨트롤이 해주는 수준으로 프로그램을 만드는 것은 쉬운 일만은 아니다. 또한 예전부터 다양한 글자 입력을 위해서 디폴트 IME 이외에 다른 IME를 쉽게 만들 수 있도록 관련 자료의 공개를 요청했지만, 일본과는 달리 우리나라에서는 새로운 IME를 만들기 위한 샘플 소스 조차 공개하지 않고 있는 실정이다. 일본에서는 상품으로 팔리는 IME가 있을 정도로 다양한 입력 방식이 있다. 국내에서도 속기용 자판들이 개발돼 있지만 IME 방식으로 여러 응용 프로그램에서 공통으로 사용될 수 있다는 얘기는 들은 적이 없다. MS에서 방침이 바뀌었는지 모르겠지만 쉬운 방법은 없는 것 같다. 다음 호에서 IME를 컨트롤하는 방법에 대해 좀더 살펴보고 IME를 커스터마이즈하는 법을 찾아보도록 하겠다.

IME를 설계한 사람들을 보면 일본 사람들이 두 명, 한국 사람 한 명, 중국 사람 한 명이다. 윈도우 3.1에서 시스템에 글로벌하게 존재하던 IME가 윈도우 95로 넘어오면서 각각의 쓰레드마다 만들어지고, 매 응용 프로그램마다 5개의 윈도우를 만들면서 독립성은 유지되지만 자원의 낭비는 심해졌다. 윈도우에는 눈에 보이지 않는 프로그램들이 몇 개씩 동작하는데 이들 모두에게 5개의 윈도우가 생성돼 있다고 생각해 보기 바란다. 가뜩이나 메모리 소요량이 많은 윈도우에서 엄청난 낭비를 주도하는 주범이라고 할 만하다.

이보다 더 큰 문제는 활성 쓰레드가 변할 때, 즉 응용 프로그램이 전환될 때 IME는 자신의 상태를 새로 설정한다. 얼핏 생각하면 독립적이어서 좋다고 할지 모르겠지만, IME는 키보드와 연관돼 있으며 마우스처럼 많은 메시지를 생성시키지도 않는다. 즉 키보드는 한 개밖에 없으며 현재 프로그램을 사용하는 사람이 현재 어느 언어를 사용하는지도 확실히 알고 있다. 쓰레드 별로 처리하더라도 어차피 현재 조합중인 상태를 벗어나 강제로 완성시키기 때문에 쓰레드 별로 분리해 처리하는 것이 별 의미가 없다. 예전에 워드프로세서를 만들 때 윈도우 별로 현재 사용하는 언어를 지정할 수 있게 했는데 사용자에게 혼동만을 가져와서 곤란한 적이 있었다. 지금 새로 윈도우를 만든다면 IME 부분은 하나의 입력 디바이스에 붙어 있는 필터로서 동작하게 했으면 한다. 기존의 프로그램과의 호환성이 발목을 붙잡겠지만 IME를 하나의 독립된 프로그램으로 동작하게 하고 메시지로 통신하면 자원 공유도 높아지고 잦은 응용 프로그램 전환시 나타나는 IME의 오동작도 자연스럽게 사라질 것이라 생각한다.

 

, 포기할 수 없는 자유여!

더 이상 한글 처리가 외국 프로그램이 국내에 팔리는데 장벽이 되지 못한다는 말을 들은지 6년이 지났다. 우습게도 한글 처리는 운영체제를 개발하는 외국회사에서 주도권을 잡고 있으며, 대부분의 프로그램들은 에디트 컨트롤만 쓰면 되는 상황이 돼 버렸다. 시스템에서 제공하는 문자 코드가 아닌 다른 코드를 사용하려면 최소한 에디트 컨트롤을 만들어야 한다. 그러나 한 번이라도 에디트 컨트롤을 만들어 본 사람들은 알겠지만, 에디트 컨트롤 하나가 웬만한 에디터의 수준이기 때문에 배보다 배꼽이 더 커지는 상황이 돼 버린다. 결국 현재 시스템이 제공하는 코드나 IME를 사용해 획일화된 환경에서 사용해야 한다. 한글 윈도우에서는 기본 영문 키보드 드라이버가 아닌 다른 국가 키보드 드라이버를 사용하면 IME가 정상적으로 동작하지 않는다. 필자는 영문의 경우 쿼티(Qwerty) 자판이 아닌 드보락(Dvorak) 자판을, 한글은 3벌식을 사용한다. 수많은 키보드 레이아웃을 제공하며 다양한 선택을 가능하게 하지만 그것은 영어 문화권에서의 사치일 뿐이고, 우리에게는 선택권이 없다. 무엇이 우리에게서 자유를 빼앗아갔는지는 독자들이 스스로 생각해 볼 일이며, 마지막으로 능력있는 프로그래머들의 우리 문화 사랑을 기대해 본다.


필자 연락처 : freefish@netsgo.com, freefish@chollian.net, foolfish@hitel.net
정리 : 정주향(jhjung@infoage.co.kr)




 

AND