멀티바이트와 유니코드

개발을 하면서 문자열을 인코딩 한 방식과 다른 방식으로 디코딩을 하여 문자가 깨지는 현상이 발생합니다. 어떤 문자열 인코딩 방식이 있는지 왜 인코딩 된 방식과 다른 방식으로 디코딩하면 문자가 깨지는 지 확인해보겠습니다.


ASCII CODE

먼저 멀티바이트와 유니코드를 알아보기 전 ASCII CODE 에 대해서 잠깐 확인해보겠습니다. 컴퓨터 공학을 접해본 사람이라면 “ASCII” 라는 단어가 낯설지 않으실 겁니다.

ASCII CODE란 숫자만을 인식할 수 있는 컴퓨터에서 문자를 표현하기 위해 정해진 숫자에 정해진 문자를 맵핑 시켜놓은 것 입니다.

기본적인 ASCII 코드0~127 까지 128(7bit)개의 단어가 숫자와 맵핑되어있으며, 유럽어와 몇가지 특수문자가 포함된 확장 ASCII 코드128개의 단어가 추가적으로 맵핑되어 총 256(8ibt = 1byte) 의 단어를 표현할 수 있습니다.

문제는 이 단어라는 것들이 기본적으로 영어의 알파벳만을 고려하여 맵핑되어 있습니다. 지구상에는 많은 국가와 언어들이 존재하는데 알파벳만 사용해서 글로벌 서비스를 하기 위해서는 한계가 있습니다.

아무리 영어가 국제 표준처럼 사용된다 하더라도 영어를 사용하지 못하는 사람들이 굉장히 많기 때문이죠 이렇듯 글로벌 서비스를 하기 위해서는 알파벳이 아닌 언어로 표현을 해야하는데 ASCII 코드는 이게 불가능합니다.


멀티바이트

이 문제을 해결하기 위해 생겨난 것이 멀티바이트 입니다.

멀티바이트 라는 이름 부터가 ASCII 코드가 1Byte 크기로 단 하나의 바이트를 사용하는데, 멀티바이트 즉 1개 이상의 Byte라는 이름으로 ASCII 코드의 단점을 극복하기 위해 나왔다 라는 뉘앙스가 강해보입니다.

단순히 1Byte만으로는 사용하고 싶은 단어를 다 표현할 수 없으니 그 이상의 숫자를 사용하는 것 입니다. ASCII 가 영어 알파벳 사용을 위해 256개의 알파벳을 맵핑시켜 놓았다면, 한글이라는 문자를 사용하기 위해서 더 많은 단어를 1Byte 이상을 사용해서 숫자와 맵핑을 시켜놓는 것 입니다.

이런 방식으로, 한글을 위한 맵핑, 중국어를 위한 맵핑, 불어를 위한 맵핑, 기타 등등의 쓰임을 위한 맵핑등으로 사전에 맵핑 시켜 놓은 것을 코드 페이지 라고 합니다.

ASCII 또한 알파벳 사용을 위해 맵핑시켜놓을 것 으로 코드 페이지 라고 할 수 있습니다.

문제는 이미 많은 개발자들이 ASCII(1Byte)의 한계를 실감하고 있었고, 표준방식이 잡히기 전 불편함을 느끼고 있던 많은 곳에서 우후죽순 자신들만의 멀티바이트 코드 페이지 를 만들고 사용하기 시작합니다.

대부분 국가 단위로 사용되는 언어가 다르기에 국가 단위의 체계들이 생겨나기 시작하는데 이를 국가 코드 페이지 라고 합니다.

비유를 하자면 전세계적으로 멀티바이트 코드 페이지 춘추전국시대가 시작됩니다.

이 춘추전국시대가 발생하면서 또 다른 문제가 생겨납니다.

멀티바이트 말 그대로 1Byte 이상의 바이트를 사용하면 OK 이기 때문에, 같은 단어를 어디서는 1Byte로, 어디서는 2Byte로 어디서는 3Byte로 또 다른 어디서는 그 이상의 Byte로 제각각 표현하기 시작합니다.

이렇게 많은 코드 페이지 가 우후죽순 생겨나다 보니 ASCII가 아닌 멀티바이트를 사용했음에도, ‘가’ 라는 단어가 어디서는 ‘가’ 로 정상적으로 출력되는 한편, 또 다른 어디서는 한자로, 또는 특수 문자로, 또는 정말 알 수 없는 단어로 표현되는 현상이 발생하기 시작했습니다.

극단적으로 예를 들어봅시다, 여러분이 해외에 사는 친구에게 이번 방학에 놀러 오라고 이메일로 “wellcome” 이라는 단어를 적어서 보냈는데, 받는 친구는 여러분과는 다른 코드 페이지 를 사용하고 있어서 “wellcome” 이라는 단어가 “go to hell” 로 보일 수 있다는 것 입니다 (극단적 예시입니다.)


유니코드

위와 같은 문제들이 빈번하게 발생하기 시작하면서 많은 개발자들은 곤혹을 치루게 되고 표준이 될 코드 페이지 의 필요성을 절실하게 느끼게 됩니다.

그 때 등장한 것이 유니코드 입니다.

유니코드전세계의 모든 언어맵핑시킨 코드 페이지 입니다. ASCII코드 와 같은 코드 페이지 라는 말이죠

(실제 풀네임은 모르겠지만 유니코드 = 유니버셜 코드 페이지 라고 봐도 무방할 것 같습니다.)

유니코드 가 등장하면서 각 언어마다 자신의 언어를 쓰기위한 별도의 코드 페이지 를 만들 필요가 없어집니다.

드디어 수 많은 개발자들이 염원하던 코드 페이지 춘추전국시대 가 끝을 맞이하는 순간 입니다.

아, 물론 실제로 유니코드 가 등장했다고 춘추전국시대 가 100% 끝난 것은 아닙니다. 전세계적으로 영어가 표준화됬음에도 불과하고 영어가 통하지 않는 지역이 있듯이 이미 자신들의 코드 페이지를 이용해서 개발된 레거시 서비스들이 존재하기에 여전히 문제가 발생할 수 있습니다.

이 레거시 서비스들을 유지보수 하거나, 서비스간의 통신을 하게 될 경우가 충분히 존재하기에 우리는 코드 페이지 개념을 충분히 이해할 필요가 있습니다.

뒤에 나올 내용이지만 흔히 들어본 UTF-8, UTF-16 이라는 말이 유니코드와 동일한 것 이다 라고 착각할 수 있는데 엄연히 다른 의미 입니다. UTF-8, UTF-16유니버셜 코드 페이지, 즉 유니코드 를 이용해서 어떻게 인코딩 할 것인가 라는 인코딩 방식을 말하는 것 입니다. 자세한 것은 뒤에 설명드리겠습니다.


유니코드 인코딩

유니코드에서 고유 번호는 ‘U+번호’ 형태로 부여됩니다. 예를들어 글자 ‘가’는 유니코드 테이블에서 44032번으로 정의되기 때문에 U+AC00 으로 표현할 수 있습니다. 여기서 U+는 유니코드 테이블을 의미하고, AC00은 16진수 숫자임을 의미합니다.

유니코드는 단순 맵핑 테이블이기 때문에 저장 방법에 대하여 관여하지 않습니다. 그렇기 때문에 메모리나 디스크에 여러 가지 방법으로 저장할 수 있는데, 이런 저장 방식을 ‘인코딩’ 이라고 합니다.

이 인코딩 방식이 우리가 많이 들어본 ‘UTF-8’, ‘UTF-16’. ‘UCS-2’, ‘UTF-32’ 등등 입니다.

UCS-2

UCS-2는 유니코드 번호를 16비트 숫자로 저장하는 방식입니다. 16비트를 사용하기 때문에 0000부터 FFFF까지 총 6만5,536개의 글자를 저장할 수 있습니다. 그 외의 유니코드 글자는 표현 못하는 불완전한 방식입니다.

이 UCS-2로 인해 많은 개발자들이 지금도 유니코드는 16비트로 6만 5,536개의 글자만 저장할 수 있다고 오해하고 있습니다. 인코딩 방식이 UCS-2일때만 6만 5,536개로 제한되는 겁니다.

UCS-2의 단점

UCS-2는 16bit 저장방식으로 고정되어있기 때문에 ‘영문(알파벳)’ 을 저장할때에도 16bit를 사용합니다. 알파벳은 8bit만으로 충분히 표현할 수 있음에도 불과하고 말이죠. 그렇기 때문에 저장 공간의 대부분이 0으로 낭비되는 단점이 존재합니다. 이런 비효율성을 개선하고 기존 ASCII 코드와 호환되도록 고안된 것이 UTF-8입니다.

UTF-8

UTF-8유니코드를 1, 2, 3, 4바이트로 저장하기 때문에 모든 유니코드를 표현할 수 있을 뿐 아니라 ASCII 코드 체계와 100% 호환됩니다. 유니코드가 등장하기 이전에 작성된 문자열 함수와 호환되는 이점 때문에 사실상 표준처럼 이용되고 있습니다.

UTF-16

UTF-16UCS-2 방식이 6만 5,536개의 글자만을 표현 가능한 단점을 극복하기 위해 고안됐습니다. 기본적으로는 UCS-2와 같이 2바이트(16bit)이지만 UCS-2가 저장하지 못하는 10000부터 10FFFF까지의 유니코드도 4바이트(32bit)로 처리할 수 있습니다. 2Byte(16bit) 또는 4Byte(32bit)로 유니코드 문자를 저장하는 방식입니다.

UCS-4 / UTF-32

모든 유니코드를 4Byte(32bit)로 저장합니다.


바이트 순서 표식 (Byte Order Mark, BOM)

글자 에 해당하는 U+AC00을 UCS-2로 표현할 경우 빅 엔디안 방식으로 AC 00이고, 이를 리틀 엔디안으로 표현하면 00 AC입니다. 이렇듯 같은 UCS-2 인코딩 방식을 이용해도 바이트를 저장하는 순서에 따라 형태가 달라집니다.

이러한 문제를 해결하기 위해서 바이트 순서 표식(Byte Order Mark, BOM)이 등장합니다. 보통 BOM 이라고 부르는데 이를 위해 U+FEFF 문자가 사용됩니다.

UCS-2 / UTF-16

UCS-2 나 UTF-16으로 저장된 데이터의 맨 앞에 FE FF로 시작한다면 해당 데이터는 빅 엔디안 방식, FF FE 일 경우 리틀 엔디안 입니다.

UCS-4 / UTF-32

UCS-4와 UTF-32 의 경우 00 00 FE FF 이면 빅 엔디안, FF FE 00 00 이면 리틀 엔디안 입니다.

UTF-8

UTF-8은 바이트 순서가 고정되어 있어 BOM이 필요 없습니다. 하지만 관례상 붙이기도 하는데 그 경우 EF BB BF 형태로 표현합니다.

바이트 순서로 인해 문제가 될 소지가 있는 UCS-2, UTF-16, UCS-4(UTF-32)의 경우 인코딩 이름에 빅 엔디안BE 리틀 엔디안LE를 붙이기도 합니다. 예를들어 UCS-2BE, UCS-2LE, UTF-32LE 등이 있습니다.


부록 1 : wchart_t 타입

C나 C++ 언어에서 1Byte 이상의 저장 크기가 필요한 문자 코드를 지원하기 위한 타입이 wchar_t입니다. 1Byte 이상의 저장 크기가 필요한 문자라는 뜻은 쉽게 말해 ASCII 영역을 벗어나는 문자들, 멀티바이트 코드 집합에 포함되는 문자열들을 말합니다. 즉 유니코드를 지원하기 위해 있는 타입입니다.

wchart_t 타입의 문자열 리터럴을 사용하기 위해서는 문자열 앞에 L을 붙입니다. “Hello”는 char 타입의 문자열 리터럴이지만, L”Hello”는 wchar_t 타입의 문자열인 것입니다. 참고로 wchar_t타입의 크기와 문자열 리터럴의 인코딩 방식은 표준으로 정해지지 않아 컴파일러 마다 다를 수 있습니다.

Visual C++의 경우 wchar_t 타입의 크기는 2Byte 이며, UTF-16으로 인코딩합니다. gcc의 경우 wchar_t 타입의 크기를 4byte로, 인코딩은 UCS-4 방식으로 처리합니다.

윈도우 2000 이후의 커널 컴포넌트는 모두 UTF-16을 표준 저장 방식으로 사용합니다(UTF-8을 왜 안썼을까..) 그 이전의 윈도우NT 커널은 UCS-2가 표준입니다.

윈도우NT 커널은 유니코드 기반으로 설계됐기 때문에 윈도우 환경에서는 별도의 라이브러리 없이 Win32Api 를 이용하면 wchar_t와 char 간 문자열을 손쉽게 변환할 수 있습니다. 하지만! 개발은 항상 멀티플랫폼을 염두해야하기에 Wind32Api에 종속되는 코드는 사용하지 않는게 좋습니다. 되도록이면 std::string을 씁시다!

윈도우NT 커널이란? // 쉽게말해 최신 윈도우 커널 Windows NT란 처음에 MS-DOS 위에서 돌아가던 Windows 3.1의 성공으로 Windows 3.1의 GUI 환경을 그대로 가져와 기업과 서버용으로 제작한 커널과 그 커널을 기반으로 만든 운영 체제의 일종이다. 이 때 작성된 커널은 MS-DOS를 기반으로 잡는 종전의 Windows보다 뛰어난 안정성으로 지금까지도 기업, 서버뿐만 아니라 가정용까지 MS-DOS를 버리고 이 커널을 사용하고 있다. 참고로 NT 제품군의 버전은 1.0부터가 아니라 3.1부터 시작한다. 이유는 3.1 문서에 자세히 서술되어 있다. Windows NT의 정확한 의미는 Windows New Technology이다.


참고 자료