유니코드의 역사, 인코딩, 프로그래밍
Study/C/C++ 2008. 10. 1. 16:53 |본 게시글은 필자가 유니코드에 대해 그동안 궁금해했던 점들을 인터넷에서 찾아서 정리한 내용이다. 자료 조사에 반나절 이상이 소요되었고, 내용 정리에 또 반나절이 사용되었다. 나름 잘 정리하려고 애썼음을 밝히고 싶다. 쩝...
■ ASCII -> ANSI -> Unicode
C 언어를 배우면 나오는 용어 중에 ASCII 코드(아스키 코드) 라는 것이 있다. 영문자를 포함한 문자들을 32에서 127 사이의 숫자들을 사용하여 표현하는 방식이다. ASCII 코드의 32 미만에는 특수 코드(예를 들어, PC 스피커에서 '삑'소리를 나게 한다던가 탭, 줄바꿈 등)가 저장되었다. 즉, 7비트만 있으면 영문 + 숫자 + 특수 문자 등을 화면에 출력할 수 있다.
보통의 컴퓨터에서 8비트(=1바이트) 단위를 사용하기 때문에 1비트를 더 사용하여 추가적인 문자를 표현하기도 하였다. 예를 들어, 유럽식 특수 문자를 넣기도 하고, 또는 화면에 선을 그릴 수 있는 그래픽 문자를 넣어두기도 하였다.
그러나, 이 추가 비트를 이용하여 생긴 128~255 사이의 숫자에 매핍된 문자는 나라마다 또는 컴퓨터 회사마다 서로 다르게 되어있어서 혼란이 생기게 되었다. 그리하여 ANSI 표준 위원회에서 ANSI 표준을 정하였는데 128 아래의 숫자에는 모든 사람들이 ASCII 코드를 그대로 사용하는 것에 동의하였다. 128 이상의 숫자에서는 코드 페이지(code page)라는 것을 만들어서 각 나라마다 고유의 문자셋을 정하여 사용하도록 설정하였다. 다국어로 판매되던 MS-DOS 는 이 코드 페이지를 이용하여 영어부터 아이슬란드어까지의 다양한 나라의 언어를 처리할 수 있었다. 그러나, 하나의 컴퓨터에서 히브리어와 그리스어를 동시에 처리하는 것을 불가능했고, 아시아권의 문자를 표현하기에는 8비트는 턱없이 부족했다.
아시아권의 문자는 두 개의 바이트를 사용하여 표시하는 DBCS(Double Bytes Character Set)를 사용해야만 했다. 이 경우 16비트를 사용하고, 65536개의 서로 다른 문자의 표현이 가능해지는 것이다. (그러나, 실제로 6만여개의 코드를 모두 사용하는 것은 아니다.)
유니코드(unicode)는 전세계의 모든 문자를 컴퓨터에서 일관되게 표현하기 위해 만들어진 표준이다. 기존의 ASCII, ANSI 등의 인코딩 방식이 다국어 환경에서 서로 호환되지 않는 단점들이 있었기 때문에 이러한 문자 인코딩 방식을 모두 유일한 유니코드로 교체하고자 하는 것이 이 표준의 목적이다. 애플컴퓨터, IBM, 마이크로소프트 등이 컨소시엄으로 설립한 유니코드(Unicode)가 1990년에 첫 버전을 발표하였고, ISO/IEC JTC1에서 1995년 9월 국제표준으로 제정하였다. 유니코드의 공식 명칭은 ISO/IEC 10646-1(Universal Multiple-Octet Coded Character Set)이다. 2008년 현재 유니코드 5.1까지 발표되었다. 유니코드는 컴퓨터 소프트웨어의 국제화와 지역화에 널리 사용되게 되었으며, 최근의 기술인 XML, 자바, 그리고 Microsoft Windows 최신 운영 체제 등에서도 지원하고 있다.
■ 유니코드의 인코딩 방식
우리가 일반적으로 메모장 등의 텍스트 편집기를 이용하여 그 내용을 확인할 수 있는 문서를 '일반 텍스트'라고 말을 하는데 이는 올바른 표현이 아니다. ZIP, RAR 등의 압축 프로그램이 고유의 포맷 또는 인코딩 방식이 있는 것처럼 일반 문자열도 나름대로의 인코딩 방식을 가질 수 있다. 그 중, 가장 대중적이면서 널리 사용되던 인코딩 방식으로 볼 수 있었던 텍스트 파일을 '일반 텍스트'라고 표현을 한 것이라고 생각해야 한다. 이는 유니코드에 대한 설명을 하면서 보다 명확해질 수 있다.
다음과 같은 문자열이 있다고 하자.
이 문자열을 유니코드를 이용하여 표현하면 다음과 같이 5개의 코드 포인트를 사용하여 나타낼 수 있다.
자, 그렇다면 이 코드를 실제로 어떻게 저장을 할까?? 메모장을 이용하여 실제 Hello 라는 문자열을 타이핑하고, 유니코드 방식으로 저장하여 그 결과를 살펴보자.
[그림 2]는 빅 인디언 방식으로 문자열을 저장한 결과를 보여준다. 처음에 나타나는 FE FF 의 두 바이트가 이 문서가 빅 인디언 방식으로 저장되어 있음을 나타낸다. 그 후에 나타나는 문자열의 바이트 순서는 위에 나타난 코드 포인트의 순서와 동일하다.
[그림 3]은 리틀 인디언 방식으로 문자열을 저장한 결과이다. 빅 인디언 방식과 달리 FF FE 순서로 처음 두 바이트가 나타나고, 그 후에 5개의 코드 포인트는 바이트 순서가 서로 바뀌어서 저장되는 것을 볼 수 있다.
마지막으로 요새 널리 사용되는 UTF-8 방식의 인코딩 방식을 알아보자. 위의 Hello 문자열에서 각 문자는 상위 바이트에 00 이라는 비어있는 바이트를 가지고 있다. 즉, UTF-8에서는 0~127 사이에 존재하는 ASCII 코드에 대해서는 오직 하나의 바이트만을 사용하여 인코딩을 하는 방식이다. 128 이상의 코드 포인트는 2~6 바이트까지 확장하여 저장을 한다. (http://www.utf-8.com/ 참조) UTF-8에서 한글은 보통 3바이트를 차지한다. [그림 4]는 Hello 문자열을 UTF-8 방식으로 저장한 결과를 보여준다. 이 때, 처음 EF BB BF 는 UTF-8 인코딩의 시작을 의미한다. 참고로, 빅 인디언의 FE FF, 리틀 인디언의 FF FE, UTF-8의 EF BB BF 와 같이 시작되는 문자 코드를 BOM(Byte Order Mark)이라고 한다.
■ Microsoft Windows 프로그래밍과 유니코드
Windows 2000은 유니코드 사용을 기본으로 하여 만들어졌다. 사용자로부터 ANSI 문자열이 넘어오면 이를 유니코드로 바꾸어 내부적으로 처리한다. 물론 이런 변환을 수행하기 때문에 시간과 메모리면에서의 소비가 존재한다. 그럼에도 유니코드를 사용하는 이유는 추후에 지속적으로 유니코드를 사용하는 컴퓨팅 환경에 미리 맞추어 나가려는 노력이라고 볼 수 있다.
참고로, Windows 2000은 유니코드와 ANSI를 지원한다. 즉 둘 중 하나로 개발할 수 있다. Windows 3.1 또는 Windows 98은 내부적으로 ANSI 문자열을 사용한다. Windows CE는 유니코드만을 지원하고, 유니코드로만 개발해야한다.
Visual C++ 2005를 이용하여 프로그램을 개발할 경우, 기본적으로 프로젝트의 문자집합 속성이 유니코드로 지정되어있다. 그러므로 WIN32 API 함수들도 유니코드 환경에 맞게 제공이 되는데, 하위호환성(win9x)을 위해 멀티바이트 입력도 받을 수 있도록 API 가 제공이 된다. 예를 들어, 기존의 SendMessage 함수는 SendMessageW(Wide, 유니코드) 와 SendMessageA(Ansi, 멀티바이트) 함수로 나누어져서 다음과 같은 전처리기에 의해 개발 환경에 맞게끔 자동 스위칭되어 호출된다.
#ifdef UNICODE
#define SendMessage SendMessageW
#else
#define SendMessage SendMessageA
#endif // !UNICODE
Visual C++ 2005 프로젝트 속성에서 문자집합 속성이 유니코드로 설정되어있으면 UNICODE 라는 것이 정의되는 것으로 인지하면 된다. 그러므로, WIN32 API를 사용할 때에는 대표적인 함수 이름을 사용하여 예전과 큰 변화없이 프로그램을 작성하면 된다.
프로그램 내부에서 유니코드로 문자열 상수를 사용하기 위해서는 문자열 앞에 대문자 L을 붙여서 사용해야 한다. 즉, "Hello" 라고 쓰면 이는 ANSI 형태의 문자열이고, L"Hello" 라고 써야 유니코드 문자열이 된다. 그러나, 실제 Visual C++ 2005 에서 프로그래밍을 할 때에는 L 매크로 대신에 _T() 매크로를 사용하는 것이 유리하다. 즉, _T("Hello") 형태로 작성하도록 한다. 이는 _T() 매크로가 유니코드일 때에는 L"Hello" 로, 멀티바이트 환경에서는 "Hello" 로 자동으로 변환시켜주기 때문이다. (내부적으로 UNICODE 가 정의되었는지를 판단하여 결정된다.)
유니코드는 2바이트를 차지하기 때문에 기존의 char 타입으로 문자열을 저장할 수 없고, wchar_t 타입을 사용해야 한다. wchar_t 타입은 실제로는 다음과 같이 unsigned short 타입과 동일하다.
typedef unsigned short wchar_t;
wchar_t szBuffer[100];
위 코드에서 szBuffer에는 100개의 유니코드형 문자를 저장할 수 있다. 그러나, 실제적으로 Visual C++ 2005에서 문자열 데이터를 저장하기 위해서는 TCHAR 타입을 사용하는 것이 바람직하다. TCHAR은 프로젝트 설정이 UNICODE로 되어있으면 wchar_t 로 변환되고, 멀티바이트 설정이면 char로 변환된다.
typedef unsigned short wchar_t;
typedef wchar_t WCHAR;
#ifdef UNICODE
typedef WCHAR TCHAR;
#else
typedef char TCHAR;
문자열 버퍼를 다루기 위해서는 기존의 strcpy, strcat 같은 표준 C 런타임 문자열 함수도 유니코드에서는 사용할 수 없다. 대신 이에 대응되는 유니코드 문자열 함수를 사용해야 한다.
char* strcat(char*, const char*);
wchar_t* wcscat(schar_t*, const wchar_t*);
이처럼 유니코드 문자열 함수는 wcs(wide character string)로 시작한다. 그러므로 기존 문자열 함수 이름에서 str을 wcs로 변경하면 된다. 그러나, (위와 마찬가지로) Visual C++ 2005에서는 _tcscmp 함수를 사용하는 것이 바람직하다. TCHAR와 마찬가지로 _tcscmp 함수는 UNICODE 가 정의되었는지를 확인하여 strcmp 또는 wcscmp 함수를 알아서 변환해주기 때문이다. 아래는 MSDN에 나와있는 변환 방식을 보여준다.
참고문헌: 조엘 온 소프트웨어 - 유쾌한 오프라인 블로그, 여러 블로그, MSDN, etc
■ ASCII -> ANSI -> Unicode
C 언어를 배우면 나오는 용어 중에 ASCII 코드(아스키 코드) 라는 것이 있다. 영문자를 포함한 문자들을 32에서 127 사이의 숫자들을 사용하여 표현하는 방식이다. ASCII 코드의 32 미만에는 특수 코드(예를 들어, PC 스피커에서 '삑'소리를 나게 한다던가 탭, 줄바꿈 등)가 저장되었다. 즉, 7비트만 있으면 영문 + 숫자 + 특수 문자 등을 화면에 출력할 수 있다.
보통의 컴퓨터에서 8비트(=1바이트) 단위를 사용하기 때문에 1비트를 더 사용하여 추가적인 문자를 표현하기도 하였다. 예를 들어, 유럽식 특수 문자를 넣기도 하고, 또는 화면에 선을 그릴 수 있는 그래픽 문자를 넣어두기도 하였다.
[그림 1] ASCII 코드를 포함하는 IBM PC의 OEM 코드
그러나, 이 추가 비트를 이용하여 생긴 128~255 사이의 숫자에 매핍된 문자는 나라마다 또는 컴퓨터 회사마다 서로 다르게 되어있어서 혼란이 생기게 되었다. 그리하여 ANSI 표준 위원회에서 ANSI 표준을 정하였는데 128 아래의 숫자에는 모든 사람들이 ASCII 코드를 그대로 사용하는 것에 동의하였다. 128 이상의 숫자에서는 코드 페이지(code page)라는 것을 만들어서 각 나라마다 고유의 문자셋을 정하여 사용하도록 설정하였다. 다국어로 판매되던 MS-DOS 는 이 코드 페이지를 이용하여 영어부터 아이슬란드어까지의 다양한 나라의 언어를 처리할 수 있었다. 그러나, 하나의 컴퓨터에서 히브리어와 그리스어를 동시에 처리하는 것을 불가능했고, 아시아권의 문자를 표현하기에는 8비트는 턱없이 부족했다.
아시아권의 문자는 두 개의 바이트를 사용하여 표시하는 DBCS(Double Bytes Character Set)를 사용해야만 했다. 이 경우 16비트를 사용하고, 65536개의 서로 다른 문자의 표현이 가능해지는 것이다. (그러나, 실제로 6만여개의 코드를 모두 사용하는 것은 아니다.)
유니코드(unicode)는 전세계의 모든 문자를 컴퓨터에서 일관되게 표현하기 위해 만들어진 표준이다. 기존의 ASCII, ANSI 등의 인코딩 방식이 다국어 환경에서 서로 호환되지 않는 단점들이 있었기 때문에 이러한 문자 인코딩 방식을 모두 유일한 유니코드로 교체하고자 하는 것이 이 표준의 목적이다. 애플컴퓨터, IBM, 마이크로소프트 등이 컨소시엄으로 설립한 유니코드(Unicode)가 1990년에 첫 버전을 발표하였고, ISO/IEC JTC1에서 1995년 9월 국제표준으로 제정하였다. 유니코드의 공식 명칭은 ISO/IEC 10646-1(Universal Multiple-Octet Coded Character Set)이다. 2008년 현재 유니코드 5.1까지 발표되었다. 유니코드는 컴퓨터 소프트웨어의 국제화와 지역화에 널리 사용되게 되었으며, 최근의 기술인 XML, 자바, 그리고 Microsoft Windows 최신 운영 체제 등에서도 지원하고 있다.
■ 유니코드의 인코딩 방식
'일반 텍스트' 라는 개념은 존재하지 않습니다.
우리가 일반적으로 메모장 등의 텍스트 편집기를 이용하여 그 내용을 확인할 수 있는 문서를 '일반 텍스트'라고 말을 하는데 이는 올바른 표현이 아니다. ZIP, RAR 등의 압축 프로그램이 고유의 포맷 또는 인코딩 방식이 있는 것처럼 일반 문자열도 나름대로의 인코딩 방식을 가질 수 있다. 그 중, 가장 대중적이면서 널리 사용되던 인코딩 방식으로 볼 수 있었던 텍스트 파일을 '일반 텍스트'라고 표현을 한 것이라고 생각해야 한다. 이는 유니코드에 대한 설명을 하면서 보다 명확해질 수 있다.
다음과 같은 문자열이 있다고 하자.
Hello
이 문자열을 유니코드를 이용하여 표현하면 다음과 같이 5개의 코드 포인트를 사용하여 나타낼 수 있다.
U+0048 U+0065 U+006C U+006C U+006F
자, 그렇다면 이 코드를 실제로 어떻게 저장을 할까?? 메모장을 이용하여 실제 Hello 라는 문자열을 타이핑하고, 유니코드 방식으로 저장하여 그 결과를 살펴보자.
[그림 2]는 빅 인디언 방식으로 문자열을 저장한 결과를 보여준다. 처음에 나타나는 FE FF 의 두 바이트가 이 문서가 빅 인디언 방식으로 저장되어 있음을 나타낸다. 그 후에 나타나는 문자열의 바이트 순서는 위에 나타난 코드 포인트의 순서와 동일하다.
[그림 2] 빅 인디언 방식
[그림 3]은 리틀 인디언 방식으로 문자열을 저장한 결과이다. 빅 인디언 방식과 달리 FF FE 순서로 처음 두 바이트가 나타나고, 그 후에 5개의 코드 포인트는 바이트 순서가 서로 바뀌어서 저장되는 것을 볼 수 있다.
[그림 3] 리틀 인디언 방식
마지막으로 요새 널리 사용되는 UTF-8 방식의 인코딩 방식을 알아보자. 위의 Hello 문자열에서 각 문자는 상위 바이트에 00 이라는 비어있는 바이트를 가지고 있다. 즉, UTF-8에서는 0~127 사이에 존재하는 ASCII 코드에 대해서는 오직 하나의 바이트만을 사용하여 인코딩을 하는 방식이다. 128 이상의 코드 포인트는 2~6 바이트까지 확장하여 저장을 한다. (http://www.utf-8.com/ 참조) UTF-8에서 한글은 보통 3바이트를 차지한다. [그림 4]는 Hello 문자열을 UTF-8 방식으로 저장한 결과를 보여준다. 이 때, 처음 EF BB BF 는 UTF-8 인코딩의 시작을 의미한다. 참고로, 빅 인디언의 FE FF, 리틀 인디언의 FF FE, UTF-8의 EF BB BF 와 같이 시작되는 문자 코드를 BOM(Byte Order Mark)이라고 한다.
[그림 4] UTF-8 방식
■ Microsoft Windows 프로그래밍과 유니코드
Windows 2000은 유니코드 사용을 기본으로 하여 만들어졌다. 사용자로부터 ANSI 문자열이 넘어오면 이를 유니코드로 바꾸어 내부적으로 처리한다. 물론 이런 변환을 수행하기 때문에 시간과 메모리면에서의 소비가 존재한다. 그럼에도 유니코드를 사용하는 이유는 추후에 지속적으로 유니코드를 사용하는 컴퓨팅 환경에 미리 맞추어 나가려는 노력이라고 볼 수 있다.
참고로, Windows 2000은 유니코드와 ANSI를 지원한다. 즉 둘 중 하나로 개발할 수 있다. Windows 3.1 또는 Windows 98은 내부적으로 ANSI 문자열을 사용한다. Windows CE는 유니코드만을 지원하고, 유니코드로만 개발해야한다.
Visual C++ 2005를 이용하여 프로그램을 개발할 경우, 기본적으로 프로젝트의 문자집합 속성이 유니코드로 지정되어있다. 그러므로 WIN32 API 함수들도 유니코드 환경에 맞게 제공이 되는데, 하위호환성(win9x)을 위해 멀티바이트 입력도 받을 수 있도록 API 가 제공이 된다. 예를 들어, 기존의 SendMessage 함수는 SendMessageW(Wide, 유니코드) 와 SendMessageA(Ansi, 멀티바이트) 함수로 나누어져서 다음과 같은 전처리기에 의해 개발 환경에 맞게끔 자동 스위칭되어 호출된다.
#ifdef UNICODE
#define SendMessage SendMessageW
#else
#define SendMessage SendMessageA
#endif // !UNICODE
Visual C++ 2005 프로젝트 속성에서 문자집합 속성이 유니코드로 설정되어있으면 UNICODE 라는 것이 정의되는 것으로 인지하면 된다. 그러므로, WIN32 API를 사용할 때에는 대표적인 함수 이름을 사용하여 예전과 큰 변화없이 프로그램을 작성하면 된다.
프로그램 내부에서 유니코드로 문자열 상수를 사용하기 위해서는 문자열 앞에 대문자 L을 붙여서 사용해야 한다. 즉, "Hello" 라고 쓰면 이는 ANSI 형태의 문자열이고, L"Hello" 라고 써야 유니코드 문자열이 된다. 그러나, 실제 Visual C++ 2005 에서 프로그래밍을 할 때에는 L 매크로 대신에 _T() 매크로를 사용하는 것이 유리하다. 즉, _T("Hello") 형태로 작성하도록 한다. 이는 _T() 매크로가 유니코드일 때에는 L"Hello" 로, 멀티바이트 환경에서는 "Hello" 로 자동으로 변환시켜주기 때문이다. (내부적으로 UNICODE 가 정의되었는지를 판단하여 결정된다.)
유니코드는 2바이트를 차지하기 때문에 기존의 char 타입으로 문자열을 저장할 수 없고, wchar_t 타입을 사용해야 한다. wchar_t 타입은 실제로는 다음과 같이 unsigned short 타입과 동일하다.
typedef unsigned short wchar_t;
wchar_t szBuffer[100];
위 코드에서 szBuffer에는 100개의 유니코드형 문자를 저장할 수 있다. 그러나, 실제적으로 Visual C++ 2005에서 문자열 데이터를 저장하기 위해서는 TCHAR 타입을 사용하는 것이 바람직하다. TCHAR은 프로젝트 설정이 UNICODE로 되어있으면 wchar_t 로 변환되고, 멀티바이트 설정이면 char로 변환된다.
typedef unsigned short wchar_t;
typedef wchar_t WCHAR;
#ifdef UNICODE
typedef WCHAR TCHAR;
#else
typedef char TCHAR;
문자열 버퍼를 다루기 위해서는 기존의 strcpy, strcat 같은 표준 C 런타임 문자열 함수도 유니코드에서는 사용할 수 없다. 대신 이에 대응되는 유니코드 문자열 함수를 사용해야 한다.
char* strcat(char*, const char*);
wchar_t* wcscat(schar_t*, const wchar_t*);
이처럼 유니코드 문자열 함수는 wcs(wide character string)로 시작한다. 그러므로 기존 문자열 함수 이름에서 str을 wcs로 변경하면 된다. 그러나, (위와 마찬가지로) Visual C++ 2005에서는 _tcscmp 함수를 사용하는 것이 바람직하다. TCHAR와 마찬가지로 _tcscmp 함수는 UNICODE 가 정의되었는지를 확인하여 strcmp 또는 wcscmp 함수를 알아서 변환해주기 때문이다. 아래는 MSDN에 나와있는 변환 방식을 보여준다.
TCHAR.H routine | _UNICODE & _MBCS not defined | _MBCS defined | _UNICODE defined |
_tcscmp | strcmp |
_mbscmp | wcscmp |
참고문헌: 조엘 온 소프트웨어 - 유쾌한 오프라인 블로그, 여러 블로그, MSDN, etc
'Study > C/C++' 카테고리의 다른 글
공용체(union)을 이용한 다중 멤버 변수 이름 지정 (1) | 2009.01.24 |
---|---|
double atan2(double y, double x) 사용법 (0) | 2008.11.30 |
공백 클래스(empty class)와 바이트 패딩(byte padding) (0) | 2008.08.28 |
상수 객체 참조자에 의한 전달(Pass-by-reference-to-const) (0) | 2008.06.27 |
vector 에서 동적 할당한 데이터 처리하기 (0) | 2008.02.20 |