본문 바로가기

Books

개발자가 꼭 알아둬야 할 유니코드와 문자 집합에 대한 고찰

조엘 온 소프트웨어


신비스러운 Content-Type 태그에 대해 궁금했던 적이 있습니까?

 HTML 코드에 넣어야 한다는 사실은 알고 있을지 몰라도, 이 태그를 정확히 어떻게 정의해야 할지는 모를 것이다.

 외국에 사는 반가운 친구가 보낸 이메일의 제목이'???? ?????? ??? ????' 같은 깨진 글자여서 난감했던 적이 한번쯤 있을 것이다.

 나는 많은 소프트웨어 개발자가 문자집합, 인코딩, 유니코드와 같은 신비로운 세계를 재빨리 따라잡지 못하는 사실에 낙담하고 있다.

 몇년전 FogBUGZ를 테스트하던 베타 테스터는 시스템이 일본어 이메일을 처리할 수 있는지 궁금해지기 시작했다. 일본어로 된 이메일을 받는다는 것에 대하여 생각해 본적이 없었다. 하지만 우리가 MIME 이메일 메시지를 해석하기 위해 사용하고 있던 상용 ActiveX 컨트롤을 주의 깊게 살펴본 결과, 문자 집합을 완전히 잘못 처리하고 있음을 발견했다.

 이런 문제점을 해결하려면 잘못된 변환을 거꾸로 변환한 다음, 다시 한 번 재대로 변환하도록 모험적인 코드를 실제로 작성해야만 했다. 또 다른 상용 라이브러리를 살펴 보았으나 이 또한 문자코드 처리 부분을 완벽하게 엉터리로 구현해 놓았다. 

 유명한 웹 개발도구인 PHP가 문자 인코딩 관례를 거의 완전히 무시한 사실을 알게되었고, PHP는 8비트 문자열만을 사용하기 때문에 PHP로 범 세계적인 우수한 웹 애플리케이션을 개발하기란 거의 불가능에 가깝다.

 21세기를 살아가는 전문 개발자인 여러분이 문자, 문자집합, 인코딩, 유니코드를 모른다면 이 부분을 이해하기 전까지 다른 코드를 작성하는 것은 멈추기를 바란다.

 '일반 텍스트는 ASCII이며, 8비트 문자열이다'라는 허튼 소리는 잘못돼도 한참 잘못된 것이며, 여전히 이런 허튼 소리를 믿고 프로그래밍을 한다면, 세균의 존재를 믿지 않는 의사나 다름 없다.




역사적 관점

 가장 손쉽게 이 주제를 이해하는 방법은 타임머신을 이용하는 것이다.

 유닉스를 처음 고안하고 K&R Kernighan & Ritchie이 "The C Programming language"를 집필한 시절, 

특수기호가 붙을 필요가 없고 영문자만 처리해도 충분했으며, 모든 글자를 32에서 127사이의 숫자를 사용하여 표현할 수 있는 ASCII 라고 불리는 코드만 있었다. (예를 들어 공백은 32 글자 A는 65로 나타낸다.)

 ASCII는 편리하게 7비트로 글자를 저장할 수 있었다. 요즘 대다수 컴퓨터가 8비트인 바이트 단위를 사용하므로, ASCII 문자를 저장하고도 비트를 하나 더 확보할 수 있었기에, 마음만 먹으면 나머지 한 비트를 사용할 수 있었다.

 바이트는 8비트까지 여분이 있었기에, 많은 사람이 얼씨구나, 128에서 255사이에 위치한 코드를 우리 마음대로 요리할 수 있단 말이지?라고 생각한다. 하지만 너무나도 많은 사람들이 동시에 이런 생각을 하는 바람에 128에서 255에 이르는 공간에 무엇을 넣어야 할지 동상이몽을 한다는 문제점이 생겼다.

 일례로 IBM PC는 OEM 문자집합을 고안했는데, 이 문자집합은 유럽 언어를 위한 몇몇 강조 문자와 수평, 수직, 귀퉁이를 비롯한 각종 선 그리기 문자를 제공했기 때문에, 화면에 그럴싸한 상자를 그리는 데 이런 문자를 사용하였다. 

 그러나 미국 이외 지역에서 PC를 판매하기 시작하면서 온갖 OEM 문자 집합 형식이 나오기 시작했고, 각자 용도에 맞게 128글자를 정의 하였다. 

예를 들어 몇몇 PC에서 문자코드 130은 é를 표시하지만, 이스라엘에서 팔린 컴퓨터는 히브리 글자일 지멀(λ)로 표시하기에, 미국 사람이 이력서(résumé)라는 단어를 이스라엘로 보내면 rλsumλ로 도착할 것이다.

따라서 128글자를 다루기 위한 여러 규약이 존재하기에 같은 언어로 작성된 문서조차도 안정적으로 교환할 수 없는 상황이 생기게 되었다.


 이런 OEM 난투극은 ANSI 표준 위원회에서 ANSI 표준을 체계적으로 정리함에 따라 모든 사람이 128 미만 문자에 대해 어떻게 다룰지에 동의 하면서 끝났다. 이는 ASCII와 거의 유사하다. 그러나 여전히 지역에 따라 128 이상 문자를 다루는 여러가지 방법이 존재한다. 이런 각기 다른 시스템을 '코드페이지'라고 부른다.

 예를들어 보자. 이스라엘 DOS는 862로 불리는 코드페이지를 사용하며, 그리스 사용자는 737로 불리는 코드페이지를 사용한다. 각 코드페이지는 128 미만에 위치한 글자는 모두 동일하게 처리하지만, 128 상위에 위치한 글자는 국가별로 사용하는 재미있는 기호들로 채워져 있다.

 국가별로 다양한 MS-DOS 버전은 영어부터 아이슬란드어까지 각기 다른 코드페이지를 할당받아 다양한 언어를 처리한다. 심지어 몇가지 '여러 나라 말을 지원하는' 코드페이지는 에스페란토어나 갈리시아어를 같은 컴퓨터에서 처리할 수도 있다. 하지만 히브리어와 그리스어를 컴퓨터에서 동시에 사용할 수는 없다. 히브리어와 그리스어는 상위 숫자를 해석하는 방법이 다른 상이한 코드페이지를 요구하기 때문에 이런 현상이 발생한다.

 반면에 아시아에서는 사람을 환장하게 만드는 여러가지 요소를 고려해야만 한다. 즉 아시아 문자집합이 결코 8비트에 들어갈 수 없는 수천가지 글자로 이뤄져 있다는 사실이다. 이런 문제점은 일반적으로 DBCS (Double Bytes Character Set)로 부르는 두 바이트 문자 집합 시스템으로 해결한다.

몇몇 글자는 1바이트에 저장하고, 다른 글자는 2바이트에 저장하는 골 때리는 방식이다. 문자열을 따라 쉽게 앞으로 나갈 수는 있지만, 뒤로 돌아오기에는 무척 어려운 구조이다. 문자열에서 앞뒤로 왔다 갔다 하기 위해 s++ 이나  s--를 사용하는 대신에 이런 혼란을 처리할 수 있게 하는, 윈도우에서 제공하는 AnsiNext와 AnsiPrev와 같은 함수를 호출하도록 프로그래머를 독려하고 있다.

 하지만 여전히 대다수 사람들은 단순하게도 바이트가 글자이며, 글자는 8비트라고 가정했다. 물론 문자열을 다른 컴퓨터로 옮기거나 컴퓨터 하나에서 언어를 둘 이상 처리하지 않는 경우에는 이런 가정이 통했을지 모르나, 인터넷 세상이 열리면서 특정 컴퓨터에서 다른 컴퓨터로 문자열이 이동하는일이 너무나도 자연스러워지면서, 모든 가정에 금이 가기 시작했다. 그래도 불행 중 다행으로 우리에게는 유니코드가 있다.


유니코드

 유니코드는 지구상에서 존재하는 모든 이성적인 쓰기 시트템은 물론이고 스타트렉에 나오는 클링온과 같이 공상에서나 나올 법한 언어까지 모두 포함하는 단일 문자 집합을 만들기 위한 용감한 노력 끝에 나온 결과물이다. 몇몇 사람은 유니코드가 각 글자마다 16비트를 차지하기에 65,536개라는 가능한 문자만을 사용할 수 있는 단순한 16비트 코드시스템이라고 오해하고 있다.

실제 이런 생각은 착각이며 유니코드에 대한 가장흔한 미신이므로, 이렇게 생각했더라도 부끄러워할 필요는 없다. 실제로 유니코드는 글자를 다루는 일종의 철학이므로, 사물을 생각하는 유니코드 방식을 이해할 필요가 있다. 그렇지 않으면 유니코드가 어디에 쓰는 물건인지 감이 오지 않을 것이다.

지금까지 우리는 글자를 디스크나 메모리에 저장할 수 있는 비트 몇개로 사상한다고 가정했었다. 다음을 보자.

A -> 0100 0001

유니코드에서는 글자는 코드 포인트라는 단순히 이론적인 개념으로 사상한다. 메모리나 디스크에서 어떻게 코드 포인트를 표현하는지는 완전히 다른 이야기이다. 유니코드에서 글자 A는 관념적인 이상이며, 공중에 둥둥 떠다닌다.


A

 이 관념적인  AB와 다르며 , a와도 다르다. 하지만 A, A, A 와는 동일하다. Times New Roman 폰트로 적은 A는 Helvetica 폰트로 적은 A와 동일한 문자이지만, 소문자로 쓴 'a'와는 다르다는 개념은 별다른 논쟁거리가 아닌 듯이 보이지만, 몇몇 언어에서는 무슨 글자인지 추측하는 작업 자체가 논란을 일으킬 수 있다.

독일 글자인 B가 진짜 글짜일까? 아니면 단순이 ss를 쓰기 쉽게 만든 글자일까? 글자 모양이 단어 끝에서 바뀌면 다른 글자가 돼야 할까? 히브리어는 그렇고, 아랍어는 그렇지 않다. 사정이 어떻게 됐든지 유니코드 컨소시엄에 속한 똑똑한 사람들은 이런 사실을 몇 세기 전부터 알고 있었으며, 격렬한 정치적인 논쟁을 벌였기에 여기에 대해 걱정할 필요가 없다. 이미 유니코드 컨소시엄에서 다 알아서 처리했기 때문이다.

 유니코드 컨소시엄은 각 알파벳에 존재하는 관념적인 철자마다 고유 번호를 붙여놓았고, U+0645라는 식으로 표현한다. 이 고유 번호를 코드 포인트라고 부른다. U+는 '유니코드'를 의미하며, 숫자는 16진수로 표현 한다. U+0639는 아라비아 글자인 Ain을 나타낸다. 영어 글자 A는 U+0041이다.

유니코드를 보려면, 윈도우 2000/XP에서 charmap 유틸리티를 사용하거나 유니코드 웹사이트를 방문하여 각 글자마다 할당된 유니코드 코드 포인트를 확인할 수 있다.

 실질적으로 유니코드가 정의할 수 있는 글자 개수에는 제한이 없다. 사실 이미 코드 숫자가 65,536을 넘었기 때문에, 모든 유니코드 글자는 실제로 2바이트로 압축할 수 조차 없다. 아무튼 16비트 유니코드는 미신이다.


좋다. 이제 문자열을 다뤄보자.

Hello를 유니코드로 표현할 경우 다음과 같은 코드 포인트 5개로 대응시킬 수 있다.

U+0048 U+0065 U+006C U+006C U+006F

단순히 코드 포인트 묶음이다. 아직 이런 문자열을 어떻게 이메일 메시지에 표현하거나 메모리에 올리는지 이야기 하지 않았다.


인코딩

이제 인코딩이 등장할 시간이다. 2바이트라는 미신을 이끌어낸 유니코드 인코딩에 대한 초기 아이디어에 따르면, '그래, 단순히 코드 포인트 숫자를 각각 두 바이트로 저장하자'는 것이었기에. 'Hello'는 다음과 같이 표현될 수 있다.

00 48 00 65 00 6C 00 6C 00 6F

맞는가? 하지만 너무 서두르지 말고 잠시만 기다려보라. 다음과 같은 표현법은 어떠할까?

48 00 65 00 6C 00 6C 00 6F 00

기술적으로는 두 가지 모두 가능하다. 믿기 어려운 이야기지만, 실제로 초기 구현가는 특정 CPU가 가장 빨리 동작할 수 있는 모드에 맞춰 유니코드 코드 포인트를 빅엔디안이나 리틀 엔디안 모드로 저장할 수 있기를 원했다. 이러다 보니 하루 아침에 유니코드를 저장하는 방식은 두 가지가 돼버렸다. 따라서 유니코드 문자열 시작부분에 FE FF를 저장하는 희한한 관례가 생기고 말았다. 이를 유니코드 바이트 순서 표시라고 부르며, 상위와 하위 바이트 순서를 바꿀 경우 FF FE로 보이므로, 이런 문자열을 읽는 경우에 다른 모든 바이트 순서를 반드시 뒤집을 필요가 있음을 알 수 있다. 저런 이상과는 달리 현실에서는 모든 유니코드 문자열의 시작 부분에 바이트 순서를 표시하지는 않는다.

 초기에는 이런 방식만으로도 충반한 것 같았다. 하지만 프로그래머는 불평을 늘어놓기 시작했다. 미국 국적 프로그래머는 실제로 U+00FF 상위에 존재하는 코드 포인트를 거의 사용하지 않은 일반 영문 텍스트를 바라보며, "저 많은 0을 보라!"고 외쳤다. 물론 이런 불평은 냉소적이면서 자유 분방한 캘리포니아 프로그래머 사이에서 터져 나왔다. 원리원칙을 따지는 텍사스 사람이라면, 게걸스럽게 두 바이트씩 먹어 버리는 상황에 대해 그렇게 개의치 않을 것이다. 하지만 캘리포니아 꽁생원들은 문자열을 저장하기 위해 사용하는 공간을 두 배로 낭비한다는 사상은 결코 용납하지 않았다.

또한 다양한 ANSI와 DBCS 문자 집합을 사용한 수많은 빌어먹을 문서 전부를 누가 다 변환해야 한다는 이유로 대다수 사람들은 유니코드를 여러해 동안 무시해왔으며, 그 동안 상황은 더욱 나빠졌다.

 이 때에 맞춰 UTF-8로 부르는 기가 막힌 개념이 등장했다. UTF-8은 유니코드 코드 포인트를 따르는 문자열을 저장하기 위한 또 다른 시스템으로, 8비트 바이트를 사용해서 매직 U+넘버를 기억 공간에 저장한다.

UTF-8에서는 0에서 127 사이에 존재하는 모든 코드 포인트를 단일 바이트로 저장한다. 128 이상인 코드 포인트만 2,3 바이트에서 시작해서 최대 한계인 6바이트까지 확장해서 저장한다.


 이런 방식을 적용할 경우 UTF-8으로 표현한 영어 텍스트가 ASCII와 완전히 똑같이 맞아 떨어진다는 부수적인 효과를 거둘 수 있었다. 결론적으로 미국 사람들은 뒤에서 뭐가 어떻게 돌아가는지 전혀 인식하지 못할 것이다. 다른 나라 사람들만 온갖 시련을 겪어야 할 것이다.

아까 살펴본 U+0048 U+0065 U+006C U+006C U+006F 으로 표현한 Hello는 

48 65 6C 6C 6F로 저장할 것이며, (주목하세요!) 이런 표현방식은 ASCII, ANSI, 지구상에서 통용되는 모든 OEM 문자 집합으로 저장하는 방식과 동일하다. 이제 특수 기호가 붙은 그리스 기호나 클링온 글자를 사용할 만큼 배짱이 있다면, 단일 코드 포인트를 저장하기 위해  여러 바이트를 사용해야만 할 것이다. 하지만 미국 사람은 결코 이런 사실을 인식 하지 못할 것이다.

추가적으로 UTF-8에는 단일 0 바이트를 널 문자로 사용하길 원하는 단순 무식한 예전 문자열 처리 코드가 문자열을 잘라먹지 않는다는 좋은 특성이 있다.

 지금까지 유니코드를 인코딩하는 세 가지 방식에 대해 설명했다. 두 바이트에 글자를 저장하는 전통적인 방식을 UCS-2(두 바이트를 사용하니까)나 UTF-16(16비트로 저장하니까)로 부른다. 하지만 여전히 특정 문자가 빅 엔디안 UCS-2인지 리틀 앤디안 UCS-2인지를 구분할 필요가 있다. 또한 새로 등장항 인기있는 UTF-8 표준이 있는데, 이 방식은 영어 문서를 완벽하게 기존처럼 다룰 수 있으며, ASCII 이외에 다른 어떤 문자도 인식하지 못하는 뇌사상태에 빠진 프로그램에도 적용할 수 있는 멋진 특성이 있다.

이제 실제 유니코드 코드 포인트로 표시하는 이상적인 글자 관점에서 사물을 바라보게 되었으니, 이런 유니코드 코드 포인트는 오래 전에 학교에서 배운 어떤 인코딩 기법으로도 인코딩할 수 있다.

예를 들어 Hello(U+0048 U+0065 U+006C U+006C U+006F)를 위한 유니코드 문자열을, ASCII로 인코딩하거나 지금까지 만들어왔던 낡은 OEM 그리스 인코딩으로도 인코딩할 수 있다. 한 가지 문제점이 있는데, 일부 글자는 화면에 안 나타날 수도 있다. 글자 표현을 위해 적용하는 인코딩이 해당 유니코드 코드 포인트를 위한 대응 글자를 지원하지 않는다면, 흔히 물음표 기호(?)로 유니코드 글자를 대체한다. 운이 좋으면 상자 모양이 나타날 수도 있다.

 몇 가지 코드 포인트만을 올바르게 저장하고 나머지 모든 코드 포인트를 물음표로 바꿔 버리는 수백 개가 넘은 언어별 인코딩 기법이 존재한다. 가장 인기 있는 영문 텍스트 인코딩은 윈도우 1252(서 유럽 언어를 위한 윈도우 9X 표준)나 Latin-1로 불리는 ISO-8859-1 이다. 하지만 이런 인코딩을 사용해서 러시아나 히브리어 글자를 저장하려고 시동할 경우 물음표 기호만 나올 뿐이다. 반면에 UTF 7, 8, 15, 32 모두 어떤 코드 포인트도 올바르게 저장할 수 있는 멋진 특성을 제공한다. 


인코딩에 대해 가장 중요한 사실 한 가지

지금까지 설명한 내용은 모두 까맣게 잊어버린다고 해도, 가장 중요한 사실 하나는 기억하자. 인코딩 방식을 모르는 문자열은 아무 의미가 없다는 사실이다. 더 이상 타조처럼 모래에 머리를 파묻고서 '일반' 텍스트는 ASCII라고 가정할 수 는 없다.


'일반 텍스트'라는 개념은 존재하지 않는다.

메모리나 파일이나 이메일 메시지 내부에 문자열이 있으면 이 문자열 인코딩이 무엇인지 알아야만 하며, 그렇지 못하면 이 문자열을 해석할 수도 없으며 사용자에게 올바르게 보여줄 수도 없다.

"제 웹사이트가 깨져 보여요"나 "강조 표시를 쓰면 제 여자친구가 이메일을 읽을 수 없어요"와 같은 어리석은 문제 대부분은 문자열을 인코딩이 UTF-8, ASCII, ISO 8559-1(라틴 1), 윈도우 1252(서유럽) 방식 중 무엇인지 알려주지 않을 경우, 올바르게 문서를 표시하지 못하거나 심지어 어디서 끝날지 추측할 수 조차 없다는 단순한 사실도 모르는 햇병아리 프로그래머들 때문이다. 코드 포인트 127을 초과하는 인코딩이 수백 가지가 넘기에 모든 가정은 수포로 돌아간다.

 문자열 인코딩 방식에 대한 정보는 어떻게 저장할까?

이런 작업을 수행하는 표준적인 방법이 존재한다. 이 메일 메시지라면 헤더에 

Content-Type: text/plain; charset="UTF-8"와 같은 문자열 형식을 넣는다.

웹 페이지인 경우에는 웹서버가 Content-Type과 유사한 http헤더를 웹페이지 자체와 함께 반환하는 방식이 초기 아이디어이다. 물론 이는 HTML 페이지에 앞서 나오는 별로 응답 헤더 중 하나가 돼야 한다.

하지만 이러한 방식에는 중대한 결함이 있다. 다양한 사이틀 지원하는 대형 웹서버를 상상해보자.

- 사이트는 수많은 사람이 온갖 언어로 올린 페이지로 구성돼 있다. 

- 모든 페이지는 지역화된 MS 프론트페이지가 해당 언어에 맞춰 적절하게 생성해낸 인코딩을 사용한다. 

이럴 경우 웹서버 자체는 각 파일이 어떤 인코딩을 따르는지 알아챌 방법이 없기 때문에 일괄적으로 시스템 전체에 할당한 Content-Type 헤더를 보낼 수 없다.

HTML 파일 자체에 특별한 태그를 사용해서 HTML 파일의 Content-Type을 넣을 수 있다면 정말 편리할 것이다. 물론 이런 방법은 순수 혈통주의자를 미치게 만든다. 어떻게 HTML 파일 내부에서 사용하는  인코딩 규칙을 알기도 전에 HTML 파일을 읽어들일 수 있단 말인가? 다행이도 흔히 사용하는 모든 인코딩은 32부터 127까지 문자를 동일하게 알파벳과 숫자로 사용하기에, 정상적인 글자로 시작하는 다음과 같은 HTML 페이지 앞 부분을 활용할 수 있다.

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

하지만 이런 메타 태그는 실제로 <head> 부분에서 가장 처음에 등장해야만 한다. 웹 브라우저가 이 태그를 보자마자, 페이지 파싱을 중단하고 지정한 인코딩을 활용해서 전체 페이지를 다시 해석한 다음에 새로 시작해야 하기 때문이다.

http 헤더나 메타 태그 어느 쪽에서도 Content-Type을 찾이 못한다면 웹브라우저는 어떻게 해야 수행할까?

인터넷 익스플로러는 실제로 무척 흥미로운 작업을 수행한다. 익스플로러는 특정 언어와 특정 인코딩을 조합했을때 나타나는 바이트 빈도를 토대로 문서에 사용한 언어와 인코딩 방식을 추측해낸다. 다양한 예전 8비트 코드페이지는 128과 255사이에 위치한 각기 다른 법위에 국가별 글자를 배열하는 경향이 있으며, 사람이 사용하는 모든 언어는 글자 사용 빈도 히스토그램에서 특징적인 패턴을 보이기에, 이런 편법이 동작할 가능성은 매우 높다.

어쨋든 이런방식은 Content-Type 헤더가 필요하다는 사실을 전혀 모르는 순진한 웹페이지 작성자가 자신의 웹브라우저에서 페이지를 읽고 "이제 됐군"이라고 만족하게 만들지도 모른다. 그런데 이런 방식은 모국어와 글자 빈도 분포에 정확히 맞아 떨어지지 않는 몇 가지 글자를 사용해서 페이지를 만드는 순간, 인터넷 익스플로러가 이를 엉뚱하게도 한국어라고 인식해서 여기에 맞춰 화면을 표시함으로써 엉망진창이 되어버는 상황을 만들어 버릴지도 모른다. 따라서 이러한 상황은 "여유있게 받아들이고, 쫀쫀하게 보내라"는 잔 포스텔의 경구가 지적하는 핵심이 너무나도 솔직하지만 그다지 훌륭한 공학적인 원칙은 아님을 증명한다고 생각한다.

문자 인코딩과 유니코드에 대해 알아야 할 모든 사항까지 추가로 다루기는 어렵겠으나, 여기까지 이해했다면 주제를 프로그래밍으로 되돌려도 될 정도로 충분한 지식을 습득했으리라 믿는다.


반응형

'Books' 카테고리의 다른 글

나와 마주서는 용기  (0) 2019.10.01
3장 더 나은 코드를 위한 12 단계  (0) 2016.02.05
2장 - 기본으로 돌아가기  (0) 2015.11.05