Joel on Software painless software management
The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)
By Joel Spolsky Wednesday, October 08, 2003
불가사의한 Content-Type 태그에 대해 궁금한 적 없으셨는가? 아시다시피, HTML 코드이다. 이 코드가 대체 무슨 역할을 할까?
혹시 불가리아의 친구로부터 "???? ?????? ??? ????"라는 제목이 적힌 이메일을 받은 적 있는가?
최근 필자는 너무나 많은 개발자들이 불가사의한 캐릭터의 세계, 인코딩, 유니코드의 발전 속도를 따라오지 못한다는 사실에 우울해 했었다. 몇 년 전, 한 FogBUGZ베타 테스터가 일본어로 된 이메일을 어떻게 다룰 지 궁금해 한 적이 있었다. 일본어? 일본어로 편지를 썼다고? 전혀 몰랐었다. 당시 MIME 이메일 메시지를 분석하는 데에 사용했던 상용 ActiveX 컨트롤을 자세히 보았는 데, 이 컨트롤이 캐릭터 셋을 완전히 틀리게 다루고 있었다. 그때문에 해당 잘못된 코드를 완전히 재작성해야했다. 또다른 상용 라이브러리를 알아 보았더니 그 역시 깨진 캐릭터 코드 구현을 갖고 있었다. 패키지 개발자들에게 연락을 해 보았더니, 자신은 전혀 손댈 수 없더랜다. 다른 프로그래머들처럼 그 역시 그냥 좋게 좋게 넘어가 주기를 원했다.
그러나 그럴 수 없었다. 유명 웹 개발 툴 PHP는 대부분 8비트를 생각 없이 사용하면서, 캐릭터 인코딩 문제를 완전히 무시하고 있었다. 따라서 좋은 국제적인 웹 애플리케이션을 개발하기란 거의 불가능이었다. 좋은 게 좋은 건가, 과연?
따라서 한 마디 안 할 수 없다. 당신이 만약 2003년 지금 프로그래머이고, 문자와 캐릭터 셋, 인코딩, 그리고 유니코드의 기본 개념에 대해 모른다면, 폭로해 버리겠다. 고발해 버리겠다. 잠수함에서 여섯 달 동안 양파나 까게 만들어 버리겠다. 맹세코.
또 한 가지 있다.
IT'S NOT THAT HARD.
본 기사에서 필자는 모든 프로그래머가 알아야할 부분에 대해 알려주겠다. 사실 이거 하나다. "plain text = ascii = characters are 8 bits"는 틀릴 뿐만 아니라(정말 틀리다), 그런 식으로 계속 프로그래밍 하고 있었다면, 병원균을 믿지 않는 의사보다 나을 건덕지가 없다. 본 기사를 읽기 전에는 제발 코드 한 줄 작성하지 않기 바란다.
시작하기 전에, 다국어 작업에 대해 아는 흔치 않은 프로그래머라면, 필자의 총론이 터무니 없이 단순하다는 사실을 알게 될 것이다. 필자는 그저, 모두가 이해해야할 최소한의 한도를 알려줄 뿐이다. 강세 표시가 없는 영어 이외의 어떠한 언어로 작업을 해야 한다면 도움이 꼭 될 것이다. 캐릭터 핸들링은 다국어 작업에 있어서 사소한 부분임이 사실이지만, 오늘은 그 캐릭터 셋부터 쓰겠다.
A Historical Perspective
제일 쉽게 이해하려면 역시 역사를 알아야 한다.
EBDIC과 같은 정말 오래된 캐릭터에 대해 얘기하리라고 생각하실 지도 모르겠지만 그러지는 않겠다. EBDIC은 여러분의 삶과 아무런 관련이 없다. 그렇게까지 오래된 캐릭터부터 이야기하지는 않겠다.
별로 오래되지 않은 옛날, 유닉스가 처음 발명되고, K&R이 C 프로그래밍 언어를 작성하던 시절이다. 당시는 모두가 단순했다. EBDIC은 막 나온 상태였으며, 강세 표시가 없는 영어만 다루는 캐릭터였다. 우리에게는 32부터 127까지의 숫자를 이용하는 ASCII 코드가 있었다. 공간이 32 개였고 "A"는 65 번이었다. 여기에서는 문자가 7비트로 저장된다. 당시 대부분의 컴퓨터는 8-비트 바이트를 이용하고 있었기에, 가능한 모든 아스키 캐릭터를 저장할 수 있었을 뿐만 아니라 남는 부분도 있었다. 따라서 영악하다면 WordStar가 영문 전용임을 골리기 위해 WordStar의 전구가 마지막 문자에서 실제로 빛나도록 할 수도 있었다. 32 아래에 있는 코드는 unprintable 로 불렸으며, 욕할 때... 농담이다. 32 아래의 코드는 컨트롤 캐릭터다. 이를테면 7은 컴퓨터 경고음이며 12는 현재의 페이지를 프린터로 보낸다던지 하는 일들을 한다.
여러분이 영어만 사용한다면야 충분한 일이다.
바이트는 8비트로 이뤄졌기 때문에, 대부분은 "128~255 사이는 우리 멋대로 써도 되겠군"이라고들 생각한다. 문제는 같은 시기에 너무나 많은 이들이 저런 식으로 생각하고 128부터 255까지의 공간을 저마다 점유해버렸다는 데에 있다. IBM-PC는 OEM 캐릭터 셋으로 알려진 캐릭터 셋을 가졌는 데, 이 캐릭터 셋은 유럽 언어의 강세 붙은 알파벳과 함께, 여러가지 line drawing characters를 지원했다. 수평선이나 수직선, 여러 다른 형태의 수평선 등, 이들 선으로 화면상에 선이나 상자를 만들 수 있었고, 창고에 처박혀 있을 8088 컴퓨터에서 지금도 돌릴 수 있다. 사실 미국 바깥에서 PC를 구입하기 시작하면서 여러가지 별다른 OEM 캐릭터 셋이 나타나기 시작하고, 저마다 128 개의 캐릭터를 사용해 버렸다. 가령 어떤 PC의 캐릭터 코드 130은 e를 표시하지만, 이스라엘에서 팔리는 다른 컴퓨터에서는 히브리 어, Gimel ( )을 표시할 때도 있었다. 따라서 미국인이 이스라엘로 resumes를 보내면, 이스라엘에서는 r sum s로 나타난다. 위에 있는 128 개의 공간을 갖고 쓴다는 데 러시아 어라면 어떻겠는가? 당시는 노어 문서 교환을 신뢰하기가 힘들었다.
마침내 이런 자유 분방한 OEM에 대해 ANSI 표준이 등장한다. ANSI 표준으로 일단 128 아래는 모두가 동의한다. 원래가 아스키였기 때문에 여기에는 이론이 없었는 데, 129 위로부터는 어디에 사는가에 따라서 여전히 여러가지 다른 방법이 존재했다. 이 별다른 시스템들은 code pages으로 불린다. 가령 이스라엘의 DOS는 862라 불리는 코드 페이지를 사용했지만, 그리스 사용자들은 737을 사용했다. 이들 모두 128 아래까지만 같고 그 위로는 제각기 달랐다. 각국의 MS-DOS는 여러가지 코드 페이지로 영어에서 아이슬란드 어에 이르기까지 다양한 언어를 다루었으며, 심지어는 같은 컴퓨터에 에스페란토나 아람어까지 쓰는 코드 페이지도 있었다! 하지만, 같은 컴퓨터에서 히브리어와 희랍어를 쓰기는 완전히 불가능했다. 그럴려면 완전히 사제화된 프로그램을 따로 작성해야했다. 희랍어와 히브리어는 제각기 다른 코드 페이지를 필요로 했기 때문이다.
게다가 아시아에서의 상황은 더욱 혼란스러웠다. 아시아 문자들은 갯수만 수만 가지가 넘기 때문에 8비트로 도저히 처리할 수가 없었다. 아시아 문자에 대해서는 DBCS(double byte character set)라는 복잡한 시스템이 해결을 한다. 1 바이트에 저장하는 문자도 있지만 2 바이트에 저장하는 문자도 있었다. 일률적으로 표시하기는 쉬웠지만 뒤로는 거의 불가능했다. 프로그래머들은 s++와 s--를 사용하지 말아야 했지만, 그대신 윈도우즈의 이런 복잡한 상황을 어떻게 처리할 지 알았던 AnsiNext and AnsiPrev와 같은 함수를 호출하였다.
하지만 아직도 1 바이트가 캐릭터이며 캐릭터는 8비트라고 생각한다. 한 가지 언어만을 하고 컴퓨터 한 대만을 고집한다면 모르지만, 인터넷 시대에 어떻게 그럴 수 있는가. 문자열 바뀜이 빈번해지고, 혼란감이 가중됐다. 이때 다행히도 유니코드가 탄생한다.
Unicode
유니코드는 지구상의 모든 입력 시스템을 포함하는 단일 문자 셋을 만들자는 야심한 노력의 일환이었다. 유니코드가 단순히 16비트 코드로서 각 캐릭터가 16비트를 차지하기에 65536 개의 문자가 가능해진다고 잘못 알고 있는 이들이 있는 데, 그런 생각은 실질적으로 옳지 않다. 유니코드에 대해 제일 많이 퍼진 미신이 바로 이점이니 너무 자책하지는 말라.
사실 유니코드는 캐릭터에 대한 개념의 전환이며, 유니코드 식으로 이해할 필요가 있다. 그렇지 않으면 이해 자체가 어렵기 때문이다.
지금까지 우리는 메모리나 디스크에 저장할 수 있는 비트로 이뤄진 문자표를 가정하였다.
A -> 0100 0001
유니코드에서, 문자표는 여전히 이론적인 개념으로서 코드 포인트라 불린다. 코드 포인트가 메모리나 디스크 상에서 어떻게 나타나는 가는 완전히 다른 이야기다.
유니코드에서 A는 관념화 되어있다. A는 자유롭다.
A
관념화 되어있는 A는 B나 a와는 다르지만, A와 A, A와는 같다. Times New Roman 서체에서 A는 Helvetica 서체의 A와 같지만, 소문자 "a"와는 다르다. 헷갈리지 않은가? 독일 문자, ß는 진짜 문자인가, 아니면 ss를 표시하기 위한 예쁜 방법인가? 문자 모양이 단어 끝에서 바뀐다면, 그것은 다른 문자인가? 히브리어에서는 예스이지만, 아랍어에서는 노우다. 여하간 유니코드 컨소시엄의 똑똑한 사람들이 지난 십여 년간 이점을 알아내고, 고도의 정치적인 논쟁을 벌였으니 여러분은 걱정할 일이 없다. 게다가 이미 정리 되어 있다.
유니코드 컨소시엄은 모든 문자의 관념화 문자를 매직 넘버와 매치시켰다. 가령 이러하다. U+0645. 이 매직 넘버가 코드 포인트라고 불린다. U+는 "유니코드"를 의미하고, 수치는 16진법 숫자다. U+FEC9는 아랍어의 아인(? )이다. 영어의 A는 U+0041이다. 모든 코드는 윈도우즈 2000/XP에서 charmap 에서 보거나, 유니코드 웹 사이트를 방문하면 알 수 있다.
유니코드가 지정할 수 있는 문자의 수에는 실질적인 한계가 없으며, 사실 65536개도 넘어설 수 있기 때문에, 모든 유니코드 문자를 두 바이트로 압축시킬 수는 없건만, 그런 미신은 계속 떠돌고 있다.
OK, 문자열이 하나 있다고 하자.
Hello
유니코드에서 위 글자는 다섯 개의 코드 포인트로 나타난다.
U+0048 U+0065 U+006C U+006C U+006F.
코드 포인트 묶음. 사실 숫자들일 뿐이다. 아직 이메일 메시지에 어떻게 나타난다거나 메모리에 어떻게 저장시키는 지에 대해서는 아무런 이야기도 하지 않은 상태다.
Encodings
여기가 바로 인코딩이 들어설 곳이다.
유니코드 인코딩에 대한 맨 처음 아이디어는 2 바이트 미신과 관련이 있다. 각각 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를 모든 유니코드 스트링 시작 부분에 저장시키는 괴상한 습관이 생겨났다. 이 현상은 Unicode Byte Order Mark이라고 불리며, 높은 자리와 낮은 자리의 바이트를 바꾸면 FF FE처럼 보인다. 그러면 모든 바이트를 스와프시켜야한다는 사실을 알려준다. 휴. 모든 유니코드 문자열이 시작부터 바이트 오더 마크를 갖진 않는다.

당분간은 충분할 테지만, 프로그래머들의 불평 소리가 벌써 들려온다. "저 제로들을 보라!" 당신들은 이미 U+00FF 이상의 코드 포인트를 사용하지 않는 미국인들이니 그럴만도 하다. 아니면 보존을 원하는 칼리포니아 히피던가. 텍사스 인들이라면, 바이트가 두 배로 늘어나도 상관하지 않으리라. 저 칼리포니아 밴댕이들은 문자열 저장 용량 두 배의 아이디어조차 못참는다. 게다가 이미 여러가지 ANSI와 DBCS로 가득찬 케케묵은 문서들을 모조리 변환시켜야 한다면 과연 누가 그걸 맡을까? 무아? 이 이유만으로도 유니코드를 그동안 무시해왔었다. 그러는 동안 상황은 악화되었다.
그때문에 굉장히 뛰어난 개념이 나왔다. UTF-8이다. UTF-8은 유니코드 코드 포인트 문자열을 저장하는 또다른 시스템이다. 위의 매직 U+숫자를 8비트 바이트를 이용하는 메모리로 저장하는 식이다. UTF-8에서는, 0에서 127에 이르는 모든 코드 포인트가 단일 바이트에 저장된다. 128을 넘는 코드 포인트만이 2나 3, 혹은 6까지 올라가는 바이트로 저장된다.

이렇게 하면 영어 텍스트는 아스키와 UTF-8이 동일해지는 효과가 있다. 따라서 미국인들로서는 별다를 일이 없지만, 나머지 세계에서는 대환영일 수 밖에 없다. 특히, U+0048 U+0065 U+006C U+006C U+006F를 갖는 Hello가 48 65 6C 6C 6F로 저장된다. 보시라! 아스키나 ANSI, 모든 지구상의 OEM 캐릭터 셋에서도 동일하다. 강세가 있는 문자나 희랍어, 혹은 외계어라도 이제 몇 개의 바이트만 사용해서 단일 코드 포인트에 저장해야 한다. 하지만 미국인들이라면 별다른 변화가 없다. (UTF-8은 단일 0바이트를 null-terminator로 사용하기를 원하는 예전 스트링-프로세싱 코드가 문자열을 자르지 못하도록 하는 멋진 특징도 갖고 있다)
지금까지 필자는 유니코드 인코딩의 세 가지 방법에 대해 말하였다. 전통적인 store-it-in-two-byte 방법은 UCS-2(두 바이트이기 때문이다)나, UTF-16(16비트이기 때문이다)로 불리며, 하이-엔디안 UCS-2인지, 로우-엔디안 UCS-2인지를 구분해야한다. 유명해진 새 UTF-8 표준은 영어 문자와 아스키 이외에 뭔가 있다는 것도 완전히 모르는 무식한 프로그램을 우연히 같이 사용하는 운좋은 경우 멋지게 사용할 수 있다.
유니코드 인코딩에는 다른 방법도 많다. UTF-7로 불리는 방법은 UTF-8과 여러모로 같지만, 하이 비트가 언제나 제로임을 보장하기 때문에, 7비트로 충분하다고 무조건 판단하는 엄격한 검열 이메일 시스템을 통과해야할 때에는 그런식으로 압축을 손실없이 할 수 있다. UCS-4는 각 코드 포인트를 4 바이트로 저장한다. 각 단일 코드 포인트가 같은 바이트 수치로 저장된다면 멋지겠지만, 텍사스인들이라 하더라도 그런 식으로 메모리 낭비를 반기지는 않을 것이다.
사실 유니코드 코드 포인트가 나타내는 관념화된 문자를 생각해 보면, 유니코드 코드 포인트는 어떠한 이전 인코딩 스킴으로도 인코딩이 가능해지기도 하다! 가령 Hello (U+0048 U+0065 U+006C U+006C U+006F) 유니코드 문자열을 아스키나 오래된 OEM 희랍어 인코딩, 히브리어 ANSI 인코딩, 혹은 그 외 지금까지 나온 어떠한 인코딩으로도 할 수 있다. 단 한 가지 문제가 있다. 문자 몇 개가 안 나타날 수도 있다! 나타내려는 인코딩에서 표현하고자 하는 유니코드 코드 포인트에 대응시킬만한 것이 없다면, ?과 같은 마크나 빈 상자가 나타난다. 이건 도대체 어디서 구했을까? -> ?
일부 코드 포인트만 제대로 나타내고, 다른 코드 포인트는 질문 기호로 바꿔버리는 수천 가지의 전통적인 인코딩이 존재한다. 영어에서 개중 유명한 인코딩은 Windows-1252 (서유럽 언어용 Windows 9x 표준이다)과 라틴-1로 알려진 ISO-8859-1이 있다(역시 서유럽 언어에서 유용하다). 하지만 노어나 히브리어 문자를 이들 인코딩에서 저장하려 한다면 질문 기호만 나타날 뿐이다. UTF 7,8,16,32는 모두 어떠한 코드포인트라도 올바르게 나타낼 수 있다.
The Single Most Important Fact About Encodings
필자의 설명을 깡그리 잊었다면, 한 가지 사실만이라도 기억해달라. 사용하는 인코딩에 대한 지식 없이 문자열을 가져서는 안된다. "순수" 텍스트가 아스키라면서 모래 속에 머리를 처박고만 있을텐가?
There Ain't No Such Thing As Plain Text.
메모리나 파일, 이메일 메시지에 문자열이 하나 있다면 어떤 인코딩인 지 알아야 한다. 그렇지 않으면 잘못 나타난 표시로 인해 해석할 수가 없게 되어 버린다.
"내 웹사이트가 알 수 없는 말로 밖에 안 보여요."나 "그여자, 강세만 집어 넣으면 이메일을 못읽더만"과 같은 어리석은 문제들 대부분은 특정 문자열이 UTF-8인지 아스키인지, ISO 8859-1(라틴 1)인지, 윈도우즈 1252(서유럽 언어)인 지를 먼저 말해주지 않으면 도대체 무슨 말인 지 알 수 없게 나타난다는 단순한 사실조차 이해하지 못한 순진한 프로그래머때문이다. 코드 포인트 127 위로 인코딩 수 백 개는 존재한다.
그렇다면 문자열이 어떤 인코딩을 쓰는 지에 대한 정보를 어떻게 알 수 있는가? 표준화된 방법들이 존재한다. 이메일 메시지에서는 각 폼의 헤더에 다음과 같은 문자열이 있다.
Content-Type: text/plain;charset="UTF-8"
원래 이 아이디어는 웹 서버가 웹 페이지 자체에 대해 HTML이 아니라 헤더의 답변 형태로 HTML을 보내기 이전에 Content-Type http 헤더를 보낸다는 개념에서 나왔다.
하지만 문제가 있다. 수많은 사이트와 여러가지 언어를 사용하는 수천 개의 페이지, 제각기 마이크로소프트 프론트 페이지로 각자의 언어대로 만든 페이지를 가진 거대 웹 서버를 운영한다고 해 보자. 웹 서버 자신은 각 파일 작성 인코딩이 무엇인 지 일일이 모른다. 따라서 저런 헤더를 보낼 수 없다.
Content-Type처럼 특정한 태그를 집어 넣은 HTML 파일 자신이라면 좋을 것이다. 물론 이것만으로도 순수주의자들은 돌아 버린다... 인코딩을 알기 전에 어떻게 HTML 파일을 읽는다는 말인가?! 다행히도, 일반적으로 사용하는 인코딩 대부분은 32에서 127 사이에 있는 캐릭터와 일치하기에, 웃긴 문자로 시작하지 않더라도 HTML 페이지를 시작할 수 있다.
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
하지만 그런 메타 태그는 <head>의 첫 번째로 있어야 한다. 웹 브라우저가 이 태그를 보자마자 페이지 분석을 멈추고 해당 인코딩으로 다시 페이지를 읽을 것 아니겠는가.
웹 브라우저가 메타 태그나 헤더에서 어떠한 Content-Type도 못찾는 다면 어떻게 될까? 인터넷 익스플로러는 이경우 상당히 흥미롭게 작동한다. 추측이다. 여러가지 언어 인코딩에서 어떤 식으로 텍스트가 나타나는가를 토대로 어떤 언어와 인코딩인 지를 추측하는 것이다. 예전의 여러가지 8 바이트 코드 페이지는 128과 255 폭 사이에서 고유 문자를 집어 넣고, 인간의 언어가 각기 다른 문자 사용법을 갖고 있기 때문에, 이런 추측은 그런대로 잘 작동한다. 정말 신기하지만, 웹 브라우저에서 Content-Type 헤더가 필요한 지조차 모르는 웹-페이지 저작자들에게는 충분하다. 하지만 자기 언어의 문자-빈번도-분포와 정확히 일치 하지 않는 무언가를 작성한다면? 아마도 인터넷 익스플로러는 이게 한국어라 생각하여 그에 맞게 표시할 지도 모른다. 래리 월(Larry Wall)의 말인데, "내놓는 것에는 엄격하고, 받아들이는 데에는 후해야 한다"는 말이 있다. 엔지니어링 개념으로서는 좋은 말이 아니다. 하여간 이 웹사이트를 잘못 읽은 독자는 어떻게 될까? 불가리아어로 쓴 페이지인데 한국어로 나타나고, 그 한국어조차 바르게 나타나지 않는다면? 그는 아마도 View|Encoding 메뉴로 간 다음, 제대로 나올 때까지 여러가지 언어를 해볼 것이다. (동유럽 언어로 적어도 열 몇 개는 있다) 그런데 대부분의 사용자들은 그럴 줄도 모른다.

필자 회사에서 만드는 웹 사이트 관리 소프트웨어, CityDesk 최신 버전에서 우리는 내부적으로 비쥬얼 베이직과 COM, 윈도우즈 NT/2000/XP가 기본 문자열로 사용하게 될 코드로 UCS-2(2 바이트) 유니코드 작성을 결정했다. C++ 코드에서는 char 대신, wchar_t ("wide char")를 선언해주면 된다. 그리고 str 함수보다는 wcs 함수를 사용하면 된다. (가령, strcat과 strlen 대신, wcscat과 wcslen를 쓴다.) C 코드에서 문자 그대로의 UCS0-2 문자열을 만들려면, 문자열 이전에 L만 집어 넣으면 된다. 가령 이러하다. L"Hello".
CityDesk는 웹 페이지를 수 년동안 웹브라우저가 잘 지원해 온 UTF-8 인코딩으로 전환시킨다. 덕분에 필자 웹 페이지도 29개 언어로 인코딩 되어 있으며, 이 페이지들을 보는 데 문제가 있다는 독자는 없었다.
본 기사는 다소 길다. 하지만 캐릭터 인코딩과 유니코드에 대해 알아야할 모든 것을 다룰 수는 없었다. 그래도 여기까지 읽으셨다면, 이제 프로그래밍을 할 때는 유니코드라는 항생제를 받아들이기 바란다. 물론 여러분에게 달린 일이다.
My company, Fog Creek Software, has just released FogBUGZ 3.0 , the latest version of our innovative system for managing the software development process. Check it out now!
http://www.joelonsoftware.com/articles/Unicode.html
위민복님의 글입니다. |
|