한글 인코딩의 이해 1편: 한글 인코딩의 역사와 유니코드
2012.02.29 16:41
NHN Business Platform 쇼핑서비스개발팀 오영은
분명 제대로 보이는 한글 이름의 파일을 내려받았는데 읽을 수 없는 이상한 이름으로 저장된 파일을 받아본 경험이 있을 것입니다. 보통 '인코딩이 깨졌다.'라고 말하는 이런 상황은 왜 발생하는 것일까요? 그 이유는 컴퓨터에서 한글을 표현하는 다양한 방식이 있는데 해당 방식이 서로 맞지 않기 때문입니다.
최초로 컴퓨터가 발명되고 오랜 기간 동안 발전되어 온 지역이 미국이기에 해당 지역에서 사용하는 언어의 문자 집합인 영어 알파벳과 이와 비슷한 문자 체계를 지닌 유럽어 알파벳 처리에 대한 연구가 가장 먼저 시작되었습니다. 이 외의 다른 문자 집합(character set)은 기존에 수립된 인코딩(영어 및 유럽어 문자 집합용)으로 표현하기에는 한계가 있었기 때문에 이들을 처리하기 위한 연구가 추가로 진행되었습니다.
요즘은 어느 전자 기기에서나 한글을 제대로 입력할 수 있고 일부 소형 기기에서는 한글을 더 빠르게 입력할 수도 있어 컴퓨터에서 한글을 처리하는 작업이 너무나 쉽고 당연하게 받아들여지고 있습니다. 하지만 한글을 제대로 표현하기 위한 한글 인코딩 체계가 수립되기까지는 수십 년의 세월이 걸렸습니다.
현재 우리나라에서 주로 사용하고 있는 CP949 또는 EUC-KR(둘은 엄밀히 다릅니다) 인코딩과 유니코드를 제대로 이해하기 위해서는 한글을 표현하기 위한 그간의 역사를 알 필요가 있습니다. 2편 연작으로 기획된 이 기사의 1편에서는 한글 인코딩의 역사를 다루고, 2편에서는 'Java 언어를 기준으로 한글을 처리하는 방법'을 다루도록 하겠습니다.
문자 집합과 인코딩
컴퓨터는 수치 연산을 위해 설계되었다. 컴퓨터 발명 초기에는 문자를 표현해야 하는 요구가 없었다. 영어 단어 'compute'는 단순히 '계산하다'라는 뜻이고, 초창기의 컴퓨터와 '전자 계산기'는 동의어이기도 했다. 그러나 (너무나 당연하지만) 문자를 표현해야 하는 요구가 발생했다. 컴퓨터 간에 문자 데이터를 교환해야 할 일이 생기기도 했다. 이기종 컴퓨터끼리 문자 데이터를 교환하기 위해서는 표준이 필요하다. 이런 이유로 ASCII(American Standard Code for Information Interchange)와 같은 표준 문자 인코딩이 만들어졌다.
문자를 표현하기 위해서는 가장 먼저 '문자 집합'을 정의해야 한다. 문자 집합은 표현해야 할 문자를 정하고 순서를 지정한 것이다. 영어라면 'A', 'B', 'C'에서 'Z'까지(소문자 a에서 z), 한글이라면 '가', '각', '간'에서 '힣'까지다. 물론 숫자나 특수 문자뿐만 아니라 인쇄와 통신을 제어하기 위한 제어 문자도 문자 집합에 포함되어야 한다. 이러한 문자 집합을 코드 형태(일반적으로 행렬)로 표기한 것을 코드화된 문자 집합(CCS, coded character set)이라고 한다. 예를 들어 '가'에는 10001, '각'에는 10002와 같이 코드를 할당하는 방식 말이다. 그리고 문자 집합을 컴퓨터에 저장하기 위해서 옥텟(octet, 8비트 단위) 형태로 표현한 것을 인코딩 방식(CES, character encoding scheme)이라고 한다.
영어의 문자 집합과 인코딩
최초의 컴퓨터인 ENIAC(Electronic Numerical Integrator And Computer)이 만들어진 이후 약 15년이 지나서야 문자 집합이라는 개념이 생겼다. 그 시초는 ASCII이다. ASCII는 0x00부터 0x7F까지의 총 127개 문자(제어 문자, 특수 문자, 숫자, 알파벳 등)로 이루어져 있다. 이는 미국에서 제정된 표준이니 영어 알파벳을 표현하기에는 큰 문제가 없었겠지만, 'Ü'와 같은 문자를 표현할 수 없어 유럽어에는 사용할 수 없었다. 이를 해결하기 위해 확장 ASCII(Extended ASCII)를 제정하여 기존의 ASCII로 정의하지 못했던 128번부터 255번까지의 새로운 문자를 정의할 수 있게 되었다. 즉, 새로 추가된 128개의 코드(0x80 ~ 0xFF)로 프랑스어, 독일어 등의 유럽어를 표현할 수 있게 된 것이다. 이와 같이 다양한 유럽어를 표현할 수 있는 확장 ASCII는 ISO-8859 유럽 통일 표준안으로 제정되었다. (표준안의 ISO 버전은 언어에 따라 약간씩 다르다. 예를 들어 ISO 8859-1은 서유럽 언어를, ISO 8859-2는 동유럽 언어를 표현하기 위한 표준안이다.)
한글의 문자 집합과 인코딩
영어나 유럽어는 알파벳을 기초로 사용하므로 256개의 코드를 이용하여 문자 집합을 표현하는 것이 가능하다. 하지만 중국, 일본, 한국(CJK, Chinese-Japanese-Korean)에서 사용하는 문자 집합인 한글, 한자, 가나, 병음, 주음 부호 등은 그 개수가 많아 확장 ASCII 코드로도 이를 모두 처리할 수 없다. 따라서 CJK 문자를 처리하기 위한 별도의 방안이 필요하다.
그렇다면 컴퓨터에서 한글을 표현하는 방법에는 무엇이 있을까? 한글 표현 방법은 크게 조합형과 완성형으로 나눌 수 있으며, 이를 좀 더 세분화하면 N바이트 조합형, 3바이트 조합형, 7비트 완성형, 2바이트 조합형, 2바이트 완성형으로 나눌 수 있다. 조합형이란 한글의 제자 원리에 기반하여 초성, 중성, 종성에 각각 코드를 할당하는 방식이고, 완성형이란 '가', '각', '간'과 같은 완성된 문자에 코드를 할당하는 방식이다. 이 중 완성형이 한글 표준안으로 채택되었고, 따라서 유니코드의 한글 표현 방식에도 완성형이 먼저 채택되었다.
Microsoft Windows 95 이전, 본격적으로 한글 문자 집합과 한글 글꼴이 운영체제에 포함되기 전인 DOS(Disk Operating System) 시절에는, 결과를 출력하기 위해 BIOS(Basic Input/Output System)를 직접 제어하거나 '한글 카드'라 불리는 ISA(Industry Standard Architecture) 인터페이스와 같은 별도의 하드웨어를 사용해야 했다.
N바이트 조합형
유럽어만 표시할 수 있었던 컴퓨터에서 한글을 표시하기 위한 고육지책으로 고안된 최초의 한글 표현 방식이다. 이 방식은 대형 컴퓨터에서 단말기를 이용하여 한글을 표현하는 데 사용되었다. N바이트 조합형은 각각의 개별적인 한글 자모를 영문자 하나하나에 대응시키고, 시작과 끝에 SI(Shift In)와 SO(Shift Out)을 추가하여 한글과 영어를 구분하는 방식이다. 예를 들어, '한글'은 ^N^bDAzI^O로 표현할 수 있다(^N은 SI이고 ^O는 SO임).
다음 표는 N바이트 조합형의 한글/알파벳 매핑 규칙을 보여준다.
자음 | 모음 | ||
ㄱ | A | ㅏ | b |
ㄲ | B | ㅐ | c |
ㄴ | D | ㅑ | d |
ㄷ | G | ㅒ | e |
ㄸ | H | ㅓ | f |
ㄹ | I | ㅔ | g |
ㅁ | Q | ㅕ | j |
ㅂ | R | ㅖ | k |
ㅃ | S | ㅗ | l |
ㅅ | U | ㅛ | r |
ㅆ | V | ㅜ | s |
ㅇ | W | ㅠ | w |
ㅈ | X | ㅡ | z |
ㅉ | Y | ㅣ | | |
ㅊ | Z | ||
ㅋ | [ | ||
ㅌ | \ | ||
ㅍ | ] | ||
ㅎ | ^ |
3바이트 조합형
초성, 중성, 종성에 1바이트씩 할당하여 사용하는 방식이다. 3바이트 조합형은 80년대 개인용 컴퓨터(PC)에서 사용되었다. 단지 중성과 종성이 없는 글자를 위해 채움 문자(fill code)를 정의하고 있다. N바이트 조합형이 한글을 2바이트에서부터 5바이트로 표현하는 데 반해, 3바이트 조합형은 3바이트로 표현하며 이는 KS X 1001 표준안의 한글 채움 문자 방식과 유사하다.
7비트 완성형
세운상가에서 만들었다고 하여 청계천 한글이라고도 한다. 소문자 뒤에 대문자가 오는 경우가 거의 없고, 특수 문자 뒤에 영문자가 오는 경우가 거의 없다는 점에 착안하여 고안한 방식이다. 즉, 처음 1바이트가 소문자이거나 특수 문자이고, 그 다음 1바이트가 대문자이면 한글로 표현한다. 글자 표현이 1,300여자 정도로 제한된다는 점과 일부 영어 단어가 한글로 표시(1990년대 초반 자주 사용되던 'dBASE'라는 프로그램 이름이 '늦ASE'로 표시)되는 문제가 있다.
표현 글자 수가 적었지만, 7비트 완성형은 당시 많이 사용했던 허큘리스 그래픽 카드를 확장하여 만들어진 기능이기 때문에 과거 80~90년대의 컴퓨터에서 작업 처리 속도를 지연하지 않는 유일한 방식이었다. 이런 이유로 당시에는 어느 정도의 수요가 있었다.
2바이트 조합형
초성, 중성, 종성에 각각 5비트씩 할당하고, 처음 1비트(MSB, most significant bit)는 1로 설정하여 한글임을 표시하는 방식이다. 대우, 삼보 등의 여러 회사에서 각각의 조합형 방식을 제안하여 사용하였으며, 나중에는 삼보 컴퓨터가 주도한 상용 조합형(KSSM)이 표준처럼 사용되었다. 예를 들어 '한글'은 1 10100(ㅎ) 00011(ㅏ) 00101(ㄴ) 1 00010(ㄱ) 11011(ㅡ) 01001(ㄹ) (0xD0, 0x65, 0x8B, 0x69)로 표현된다.
다음 표는 2바이트 조합형의 한글 매핑 규칙을 보여준다.
비트 조합 | 초성 | 중성 | 종성 |
00001 | 채움 | 채움 | |
00010 | ㄱ | 채움 | ㄱ |
00011 | ㄲ | ㅏ | ㄲ |
00100 | ㄴ | ㅐ | ㄳ |
00101 | ㄷ | ㅑ | ㄴ |
00110 | ㄸ | ㅒ | ㄵ |
00111 | ㄹ | ㅓ | ㄶ |
01000 | ㅁ | ㄷ | |
01001 | ㅂ | ㄹ | |
01010 | ㅃ | ㅔ | ㄺ |
01011 | ㅅ | ㅕ | ㄻ |
01100 | ㅆ | ㅖ | ㄼ |
01101 | ㅇ | ㅗ | ㄽ |
01110 | ㅈ | ㅘ | ㄾ |
01111 | ㅉ | ㅙ | ㄿ |
10000 | ㅊ | ㅀ | |
10001 | ㅋ | ㅁ | |
10010 | ㅌ | ㅚ | |
10011 | ㅍ | ㅛ | ㅂ |
10100 | ㅎ | ㅜ | ㅄ |
10101 | ㅝ | ㅅ | |
10110 | ㅞ | ㅆ | |
10111 | ㅟ | ㅇ | |
11000 | ㅈ | ||
11001 | ㅊ | ||
11010 | ㅠ | ㅋ | |
11011 | ㅡ | ㅌ | |
11100 | ㅢ | ㅍ | |
11101 | ㅣ | ㅎ |
2바이트 완성형
완성된 음절을 코드와 일대일 대응시키는 방식이다. 예를 들어, '가'는 0xB0A1, '각'은 0xB0A2로 코드화한다. ISO 2022 표준을 기준으로 하였으며, KS C 5601:1987 표준안으로 채택되었다. ISO 2022 표준은 ASCII 영역과 겹치지 않도록 첫 번째 비트 값을 1로 규정하였으므로, KS C 5601 표준안은 0xA1A1부터 0xFEFE까지의 영역(94x94, 8,836글자)만을 사용하였다. 더군다나 8,836개의 글자 중에서 부호 및 일본, 러시아 글자에 1,598자를, 한자에 4,888자를 할당하여 한글에는 2,350자밖에 사용할 수 없었다. 2바이트 완성형의 문제는 1990년에 방영된 MBC 드라마 제목 '똠방각하'를 표현하지 못하면서 불거졌는데, 완성형으로는 '똠'을 표현할 수 없어 한글의 제자 원리를 무시한 방식이라는 비난을 피할 수 없게 되었다.
이러한 문제를 해결하기 위해 1992년에 KS C 5601 표준안에 완성형/조합형을 모두 표준으로 지정하였으나, 표준 조합형은 기존에 사용되던 상용 조합형(KSSM)과 코드가 맞지 않아 거의 사용되지 않았다. 1990년대 초반까지 조합형과 완성형이 모두 사용되었지만, 국가 주도의 프로그램, 운영체제에서 완성형을 기본으로 지원하였고, Microsoft Windows 95에서 확장 완성형이 사용됨으로써 조합형은 사장되었다. 현재는 KS C 5601은 KS X 1001로, KS C 5636은 KS X 1003으로 변경되었다. KS X 1003 표준안은 ASCII와 동일하나, 역슬래시(\)가 원화(\)로 표기되는 것만 다르다.
확장 완성형
Microsoft가 독자적으로 제정한 문자 집합으로 완성형 코드에서 표현할 수 없던 8,822자가 추가되었다. 통합형 한글 코드(UHC, Unified Hangul Code)라고도 하며, 현대 한글을 모두 표현할 수 있다. 그러나 완성형 영역의 2,350자는 자음, 모음 순서대로 배열되어 정렬에 문제가 없었으나, 확장 완성형의 문자 정렬에는 문제가 있었다.
한글의 인코딩 방식
EUC-KR은 KS X 1001과 KS X 1003 표준안의 인코딩 방식이며, CP949(MS949, x-windows-949)는 확장 완성형의 인코딩 방식이다. 그러므로 EUC-KR은 2,350자의 한글, CP949는 11,172자의 한글을 표현할 수 있다. 그러나 Java에서는 CP949와 MS949를 다르게 취급한다. CP949는 IBM에서 처음 지정한 코드 페이지(sun.nio.cs.ext.IBM949)가 기준이고 Microsoft가 제정한 확장 완성형은 MS949(sun.nio.cs.ext.MS949)를 기준이다. 그러므로 Java에서는 CP949와 EUC-KR이 사실상 같으며, 확장 완성형을 사용하기 위해서는 MS949로 지정해야 한다.
유니코드
한글뿐 아니라 일본어와 중국어에도 컴퓨터에서 해당 언어를 표현할 수 있는 독자적인 문자 집합이 있다(KPS-9566, EUC-CN, EUC-TW, EUC-JP, Shift JIS, Big5, GB, HZ 등). 문제는 '어떻게 동시에 한국어, 중국어, 일본어를 표현하느냐'이다. 하나의 문자 집합을 사용하는 문서에서는 이를 동시에 표현할 수 없다(escape sequence를 이용하여 여러 문자 집합을 표현할 수 있으나 이는 널리 쓰이지 않았다).
이런 문제는 유럽어의 문자 집합에도 있었다. 유로화를 나타내는 '€' 기호에는 ISO 8859-15(Latin 9)의 코드 값 중 0xA4이 할당되었으나 ISO 8859-1(Latin 1)의 0xA4 코드에 할당된 문자는 '¤'다. 이 문제를 해결하기 위해 전 세계적으로 사용되는 모든 문자 집합을 하나로 모아 탄생시킨 것이 유니코드이다. ISO 10646 표준에서 UCS(Universal Character Set)을 정의하고 있다. 유니코드 1.0.0은 1991년 8월 제정되었으며, 그 후 약 5년이 지나서야 유니코드 2.0.0에 한글 11,172자가 모두 포함되었다. 현재 버전은 2010년 10월 11일 제정된 6.0이다.
유니코드 값을 나타내기 위해서는 코드 포인트(code point)를 사용하는데, 보통 U+를 붙여 표시한다. 예를 들어, 'A'의 유니코드 값은 U+0041로 표현한다(\u0041로 표기하기도 함). 유니코드는 공식적으로 31비트 문자 집합이지만 현재까지는 21비트 이내로 모두 표현이 가능하다. 유니코드는 논리적으로 평면(plane)이라는 개념을 이용하여 구획을 나누며, 평면 개수는 0번 평면인 기본 다국어 평면(BMP; Basic Multilingual Plane)에서 16번 평면까지 모두 17개이다. 대부분의 문자는 U+0000~U+FFFF 범위에 있는 기본 다국어 평면에 속하며, 일부 한자는 보조 다국어 평면(SMP, Supplementary Multilingual Plane)인 U+10000~U+1FFFF 범위에 속한다. 이 중 한글은 U+1100~U+11FF 사이에 한글 자모 영역, U+AC00~U+D7AF 사이의 한글 소리 마디 영역에 포함된다(자세한 내용은 위키백과의 '유니코드 목록' 내용 참조).
유니코드 인코딩 방식
유니코드의 인코딩 방식으로는 코드 포인트를 코드화한 UCS-2와 UCS-4, 변환 인코딩 형식(UTF, UCS Transformation Format)인 UTF-7, UTF-8, UTF-16, UTF-32 인코딩 등이 있다. 이 중 ASCII와 호환이 가능하면서 유니코드를 표현할 수 있는 UTF-8 인코딩이 가장 많이 사용된다. UTF-8은 코드 포인트 범위에 따라 다음 표에서 보는 바와 같이 인코딩 방식이 다르다.
다음 표는 코드 포인트 범위에 따른 UTF-8 인코딩 방식을 보여준다.
코드 포인트 범위 | 비트 수 | 인코딩 |
U+0000~U+007F | 7 | 그대로 인코딩 |
U+0080~U+07FF | 11 | 110xxxxx 10xxxxxx |
U+0800~U+FFFF | 16 | 1110xxxx 10xxxxxx 10xxxxxx |
U+10000~U+1FFFFF | 21 | 11110xxx 10xxxxxx 10xxxxxx10xxxxxx |
위의 표에서 xxxx로 표시된 부분에는 원래의 비트 값을 순서대로 적는다. 즉, U+0080을 비트 값으로 표현하면 000 1000 0000인데, 인코딩 방식에 의해 11000010 10000000으로 변환되어, 0xC2 0x80으로 저장된다. 예를 들어, '한글'을 코드 포인트로 표현하면 U+D55C U+AE00인데, 이를 UTF-8 인코딩하면, 0xED 0x95 0x9C 0xEA 0xB8 0x80이 된다.
U+D55C U+AE00
1101 0101 0101 1100 1010 1110 0000 0000 2진수 표현
1110 1101 1001 0101 1001 1100 1110 1010 1011 1000 1000 0000 인코딩 방식에 따라 인코딩
한글 완성형의 코드 포인트 범위는 U+AC00~U+D7AF이므로, UTF-8 인코딩에서 한글은 무조건 3바이트 인코딩이다. 그래서 URL에 파라미터 값이 %ED%95%9C%EA%B8%80과 같이 표시된다면 UTF-8 인코딩일 확률이 높다(ISO8859, EUC-KR, UTF-8 인코딩 중 하나라면 말이다).
이번 기사에서는 프로그램 언어에서 한글을 제대로 처리하기 위해 알아야 할 기본 지식을 알아 보았다. 다음 편 기사에서는 유니코드로 한글을 표현하는 방법과 Java에서 한글을 처리하는 방법에 대해서 다루고자 한다.
출처 - http://helloworld.naver.com/helloworld/19187
한글 인코딩의 이해 2편: 유니코드와 Java를 이용한 한글 처리
2012.06.07 19:04
참고
이 글은 "한글 인코딩의 이해 1편: 한글 인코딩의 역사와 유니코드"에 이어지는 글로, 월간 "마이크로소프트웨어" 2012년 5월호에 "유니코드와 JAVA를 이용한 한글 처리"라는 제목으로 실렸습니다.
프로그래밍에서 문자열을 다루다 보면, 여러 인코딩 방식(CES, Character Encoding Scheme)을 마주하게 된다. 특히 한글은 문자집합(CCS, Coded Character Set)의 구성에 따라 조합형, 완성형, 유니코드 등으로 나누어지는데, 본 문서에서는 유니코드에서 한글을 어떻게 표현하는지, 그리고 Java와 같은 언어에서 애플리케이션을 개발할 때 어떻게 한글을 처리하면 좋은지 알아보려 한다.
유니코드란
유니코드란 전 세계적으로 사용하는 모든 문자 집합을 하나로 모은 것이다. 유니코드 1.0.0은 1991년 8월 제정되었으며, 그 후 약 5년이 지나서야 유니코드 2.0.0에 한글 11,172자가 모두 포함되었다. 현재 버전은 2010년 10월 11일 제정된 6.0이다.
유니코드 값을 나타내기 위해서는 코드 포인트(code point)를 사용하는데, 보통 U+를 붙여 표시한다. 예를 들어, 'A'의 유니코드 값은 U+0041로 표현한다(\u0041로 표기하기도 함). 유니코드는 공식적으로 31비트 문자 집합이지만 현재까지는 21비트 이내로 모두 표현이 가능하다. 유니코드는 논리적으로 평면(plane)이라는 개념을 이용하여 구획을 나누며, 평면 개수는 0번 평면인 기본 다국어 평면(BMP; Basic Multilingual Plane)에서 16번 평면까지 모두 17개이다. 대부분의 문자는 U+0000~U+FFFF 범위에 있는 기본 다국어 평면에 속하며, 일부 한자는 보조 다국어 평면(SMP, Supplementary Multilingual Plane)인 U+10000~U+1FFFF 범위에 속한다. 이 중 한글은 U+1100~U+11FF 사이에 한글 자모 영역, U+AC00~U+D7AF 사이의 한글 소리 마디 영역에 포함된다.
유니코드의 인코딩 방식
유니코드의 인코딩 방식으로는 코드 포인트를 코드화한 UCS-2와 UCS-4, 변환 인코딩 형식(UTF, UCS Transformation Format)인 UTF-7, UTF-8, UTF-16, UTF-32 인코딩 등이 있다. 이 중 ASCII와 호환이 가능하면서 유니코드를 표현할 수 있는 UTF-8 인코딩이 가장 많이 사용된다. UTF-8은 코드 포인트 범위에 따라 다음 표에서 보는 바와 같이 인코딩 방식이 다르다. 다음 표는 코드 포인트 범위에 따른 UTF-8 인코딩 방식을 보여준다.
표1. 유니코드 범위 목록에서의 한글 관련 범위
코드 포인트 범위 | 비트 수 | 인코딩 |
U+0000 ~ U+007F | 7 | 그대로 인코딩 |
U+0080 ~ U+07FF | 11 | 110xxxxx 10xxxxxx |
U+0800 ~ U+FFFF | 16 | 1110xxxx 10xxxxxx 10xxxxxx |
U+10000 ~ U+1FFFFF | 21 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
위의 표에서 xxxx로 표시된 부분에는 원래의 비트 값을 순서대로 적는다. 즉, U+0080을 비트 값으로 표현하면 000 1000 0000인데, 인코딩 방식에 의해 11000010 10000000으로 변환되어, 0xC2 0x80으로 저장된다. 예를 들어, '한글'을 코드 포인트로 표현하면 U+D55C U+AE00인데, 이를 UTF-8 인코딩하면, 0xED 0x95 0x9C 0xEA 0xB8 0x80이 된다.
유니코드에서 한글을 표현하는 방법
유니코드 범위 목록(Mapping of Unit characters)을 살펴보면, 한글 표현을 위한 코드 영역 개수는 다른 언어 글자를 위한 코드 영역 개수보다 대체로 많다는 것을 알 수 있다. 유니코드에서 한글을 표현하기 위한 코드 영역은 다음과 같다.
표2 유니코드 범위 목록에서의 한글 관련 범위
이름 | 처음 | 끝 | 개수 |
한글 자모 (Hangul Jamo) | 1100 | 11FF | 256 |
호환용 한글 자모 (Hangul Compatibility Jamo) | 3130 | 318F | 96 |
한글 자모 확장 A (Hangul Jamo Extended A) | A960 | A97F | 32 |
한글 소리 마디 (Hangul Syllables) | AC00 | D7AF | 11184 |
한글 자모 확장 B (Hangul Jamo Extended B) | D7B0 | D7FF | 80 |
한글 소리 마디
초성, 중성, 종성으로 이루어진 한글을 표현하기 위한 영역이다. 현대 한글에서 표현 가능한 11,172자를 모두 포함하고 있다. 완성형 한글처럼 각 음절(소리 마디)마다 코드를 매핑하는 방식이다. 범위는 '가'(U+AC00)부터 '힣'(U+D7A3)까지이다. 그림1에서 볼 수 있듯이, 각 음절은 초성, 중성, 종성 순으로 정렬되어 있으며, 확장 완성형과 다른 점은, 초성, 중성, 종성의 분리가 가능하다는 점이다.
그림 1 유니코드에서의 한글 소리 마디
그림 1에 나와있듯이 초성은 19개, 중성은 21개, 종성은 28개로 이루어져 있다. 초성, 중성, 종성에 대한 값은 다음 표3에서 알 수 있다.
표 3 한글 소리마디에서 초성/중성/종성에 대한 순서 값
값 | 초성 | 중성 | 종성 | 값 | 초성 | 중성 | 종성 |
0 | ㄱ | ㅏ | 채움 | 14 | ㅊ | ㅜ | ㄿ |
1 | ㄲ | ㅐ | ㄱ | 15 | ㅋ | ㅝ | ㅀ |
2 | ㄴ | ㅑ | ㄲ | 16 | ㅌ | ㅞ | ㅁ |
3 | ㄷ | ㅒ | ㄳ | 17 | ㅍ | ㅟ | ㅂ |
4 | ㄸ | ㅓ | ㄴ | 18 | ㅎ | ㅡ | ㅄ |
5 | ㄹ | ㅔ | ㄵ | 19 | ㅢ | ㅅ | |
6 | ㅁ | ㅕ | ㄶ | 20 | ㅣ | ㅆ | |
7 | ㅂ | ㅖ | ㄷ | 21 | ㅇ | ||
8 | ㅃ | ㅗ | ㄹ | 22 | ㅈ | ||
9 | ㅅ | ㅠ | ㄺ | 23 | ㅊ | ||
10 | ㅆ | ㅘ | ㄻ | 24 | ㅋ | ||
11 | ㅇ | ㅛ | ㄼ | 25 | ㅌ | ||
12 | ㅈ | ㅙ | ㄽ | 26 | ㅍ | ||
13 | ㅉ | ㅚ | ㄾ | 27 | ㅎ |
한글 음절의 코드 포인트 값은 시작 값인 U+AC00에 ((초성 값 x 21) + 중성 값) x 28 + 종성 값을 더하면 된다. 예를 들어, '한'이라는 글자는 'ㅎ', 'ㅏ', 'ㄴ'으로 구성되어 있으며, 각각 18, 0, 4 값을 가지고 있으므로, '한'의 코드 포인트 값은 U+AC00 + ((18 x 21) + 0) x 28 + 4 = U+AC00 + U+295C = U+D55C가 된다. 이를 역으로 생각해 보면, 한글 음절에 대해 초성, 중성, 종성의 분리가 가능하다. 즉 한글 음절의 코드 포인트 값에서 U+AC00을 뺀 값을 ①이라 한다면, 다음과 같이 정리할 수 있다.
- ①의 값을 (21 x 28)로 나눈 몫은 초성
- ①의 값을 (21 x 28)로 나눈 나머지를, 28로 나눈 몫은 중성
- ①의 값을 28로 나눈 나머지는 종성
호환용 한글 자모
닿소리와 홀소리 같은 한글 자모를 표현하기 위한 영역이다. 이 영역은 현대 한글에서 사용하는 자음(U+3130 ~ U+314E), 모음(U+314F ~ U+3163), 채움 코드(U+3164), 옛한글 자모(U+3165 ~ U+318E)로 구성되어 있다.
한글 자모, 한글 자모 확장
첫가끝 코드라고도 부른다. 한글 자모 확장은 유니코드 버전 조합형 한글로도 이해할 수 있다.
한글 소리 마디에서는 완성형 한글을, 호환형 한글 자모에서는 초성/중성/종성 구별 없는 자음과 모음을 표현하는 것과 달리, 한글 자모, 한글 자모 확장에서는 초성/종성을 구별하는 자음과 모음으로 구성되어 있다. 이를 이용하여 조합형 한글을 표현할 수 있다. 또한, 한글 자모 영역에는 옛한글에서만 사용된 초성, 중성, 종성이 있으므로, 옛한글을 표현하는 데 전혀 문제가 없다.
그림 3 한글 자모 코드 영역
U+1100부터 U+115E까지는 초성, U+1161부터 U+11A7까지는 중성, U+11A8부터 U+11FF까지는 종성이다.
유니코드 정규화(Unicode equivalence)
한글 소리 마디와 한글자모, 한글 자모 확장 이렇게 두 개의 코드 영역이 있다는 것은 같은 글자를 표현하는 서로 다른 두 개의 방법이 있다는 것을 말한다. 이것은 한글뿐만 아니라 다른 언어에서도 나타나는 현상이다. 가령 "ñ"을 표현할 때 U+00F1을 사용할 수도 있고, U+006E (라틴 소문자 "n") 과 U+0303( 결합 틸데 "◌̃")을 연이어 사용하여 표현할 수도 있다. 유니코드 정규화(Unicode equivalence)란 이렇게 연속적인 코드를 사용하여 표현한 어떤 글자를 처리하는 방법을 다루는 명세이다. 유니코드 정규화에는 다음과 같은 네 가지 방법이 있다.
표 4 유니코드 정규화 방법과 예
정규화 방법 | 예 |
NFD (정준 분해) Normalization Form Canonical Decomposition | À (U+00C0) → A (U+0041) + ̀ (U+0300) 위 (U+C704) → ᄋ (U+110B) + ᅱ (U+1171) |
NFC (정준 분해한 뒤 다시 정준 결합) Normalization Form Canonical Composition | A (U+0041) + ̀ (U+0300) → À (U+00C0) ᄋ (U+110B) + ᅱ (U+1171) → 위 (U+C704) |
NFKD (호환 분해) Normalization Form Compatibility Decomposition | fi (U+FB01) → f (U+0066) + i (U+0069) |
NFKC (호환 분해한 뒤 다시 정준 결합) Normalization Form Compatibility Composition | 樂 (U+F914), 樂 (U+F95C), 樂 (U+F9BF) → 樂 (U+6A02) |
이중 한글 처리와 관련된 것은 NFD(소리 마디를 첫가끝 코드로 분해)와 NFC(첫가끝 코드를 소리 마디로 결합)이다.
Java는 유니코드 정규화 기능을 지원하고 있다. 아래 코드는 그 예제이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import java.text.Normalizer; public class NormalizerTest { private void printIt(String string) { System.out.println(string); for ( int i = 0 ; i < string.length(); i++) { System.out.print(String.format( "U+%04X " , string.codePointAt(i))); } System.out.println(); } @Test public void test() { String han = "한" ; printIt(han); String nfd = Normalizer.normalize(han, Normalizer.Form.NFD); printIt(nfd); String nfc = Normalizer.normalize(nfd, Normalizer.Form.NFC); printIt(nfc); } } |
아래는 위의 코드를 실행한 결과이다.
만약, 아래아 한()을 NFC로 만들고자 한다면, 'ㅎ'(U+1112), 'ㆍ'(U+119E), 'ㄴ'(U+11AB)를 결합하면 된다. 옛한글의 경우 글꼴에 따라 출력이 되지 않을 수 있으므로, 옛한글 글꼴을 지원하는 은글꼴 또는 함초롬체를 사용하여야 한다.
한양 PUA(Private Use Area) 코드
윈도 XP까지 시스템 글꼴을 제작해 온 한양정보통신에서 옛한글을 표현하기 위해 사용한 유니코드의 사용자 정의 영역 코드(U+E0BC ~ U+F77C)를 말한다. 옛한글에서 많이 쓰이는 5천여개의 완성형 음절을 정의하였으며, 비표준이나 현재까지도 많이 사용하고 있다.
Java와 한글
Java는 String에서 사용하는 인코딩은 UTF-16 BE(Big Endian)이다. 문자열 전송/수신을 위해서 직렬화가 필요할 때에는 변형된 UTF-8(Modified UTF-8)을 사용한다. Java의 DataInput, DataOutput 인터페이스 구현체에서는 문자열을 기록하거나 읽어들일 때 이 변형된 UTF-8을 사용한다. 변형된 UTF-8의 인코딩 규칙은 표5에서 볼 수 있다.
표 5 변형된 UTF-8 인코딩 규칙
코드 범위 | 인코딩 규칙 |
U+0000 | 11000000 10000000 (0xC080) |
U+0001 ~ U+FFFF | UTF-8 인코딩과 동일 |
U+010000 ~ U+1FFFFF | UTF-16 인코딩한 값을, UTF-8 인코딩함 (CESU-8) |
변형된 UTF-8에서 U+0000을 2바이트로 표시하는 이유는 인코딩된 결과에 널 문자(00)가 나타나지 않도록 하기 위해서이다. C언어와 같이 NULL 문자를 문자열의 끝으로 처리하는 언어에서 U+0000을 읽을 때, 문자열의 끝으로 잘못 처리하는 일이 없도록 하기 위해서이다. 그리고 U+010000 이상의 코드를 표현하기 위한 CESU-8(Compatibility Encoding Scheme for UTF-16:8-bit)은 UTF-8의 변형인데 코드 포인트 U+010000 이상의 글자를 표현하기 위한 방법이다. Java에서 글자를 표현하기 위해서 2바이트 크기를 가지는 char를 사용하는데, 전체 유니코드 글자를 2바이트로 표현할 수 없기 때문에 이러한 방식을 사용한다. Java의 변형된 UTF-8은 CESU-8에 NULL 문자 처리(U+0000)을 추가한 것이다.
한글의 표현과 인코딩
Java에서는 유니코드의 코드 포인트 값을 String.codePointAt(int); 메서드를 이용하여 확인할 수 있다. 다음은 '한글'(U+D55C U+AE00)에 대한 코드 포인트 값을 출력한 예이다.
1 2 3 4 5 | String string = "한글" ; for ( int i = 0 ; i < string.length(); i++) { System.out.print(String.format( "U+%04X " , string.codePointAt(i))); } System.out.println(); |
코드 포인트에 대한 개념을 이해하고 있다면, 한글/영어 개수를 세거나, 바이트 수에 맞추어 한글/영어 문자열 자르기 등은 어렵지 않을 것이다.
Java에서 인코딩된 값을 알아보려면, getBytes() 메서드를 이용하여 확인할 수 있다. 다음은 '한글'에 대한 인코딩 값을 출력한 예이다.
1 2 3 4 5 6 | String string = "한글" ; byte [] bytes = string.getBytes(); for ( byte b : bytes) { System.out.print(String.format( "0x%02X " , b)); } System.out.println(); |
여기에서 염두에 둘 점은 Java에서 문자열은 항상 UTF-16 BE 인코딩으로 저장되며, file.encoding시스템 프로퍼티에 의해 인코딩 값이 결정된다는 점이다. 특히 C언어를 많이 다루어 본 개발자라면 문자열을 C의 1바이트 char 배열로 여기는 경향이 강하기 때문에 이 차이점을 잘 이해해야 한다. 언어 차원에서 유니코드와 같은 캐릭터 인코딩을 지원하지 않는 C와 달리 Java에서는 언어 차원에서 유니코드와 여러 코드 페이지를 지원한다.
Java는 String 객체 내부(메모리 상에서) UTF-16 BE 인코딩으로 문자열을 저장하고, 문자열을 입/출력할 때에만 사용자가 지정한 인코딩 값 또는 운영체제의 기본 인코딩 값으로 문자열을 인코딩한다. JVM 기본 인코딩은 JVM 로딩 시에만 초기화되므로, 코드 중간에서 file.encoding 프로퍼티를 바꾸는 것은 아무 의미가 없다. 만약 file.encoding이 지정되어 있지 않다면, OS 환경 변수(예: LANG) 값을 따른다. Java에서 글자를 깨뜨리지 않으려면, 문자 집합의 이름을 지정해야 한다. 예를 들어, 문자열 객체의 getBytes() 메서드를 이용하여 바이트 배열을 얻고자 할 때, getBytes() 대신 getBytes(String charsetName) 메서드를 사용하고, 반대로 바이트 배열에서 문자열 객체를 얻고자 할 때, new String(byte[] b) 대신 new String(byte[] bs, String charsetName) 메서드를 사용한다.
웹과 한글
한글 처리, 특히 웹에서의 한글 처리는 무척 까다롭다. 그 이유는 사용자의 환경이 매우 다르다는 데 있다. 웹 프로그래밍을 하려면, 운영체제의 기본 인코딩, Java 소스 코드의 인코딩, JSP 파일의 인코딩, HTTP 요청의 인코딩, HTTP 응답의 인코딩, 데이터베이스의 인코딩, 파일의 인코딩 - 이렇게 많은 인코딩과 마주하게 된다.
웹에서 한글이 왜 깨지는가? 브라우저 인코딩 값과 서버 인코딩 값이 다르기 때문이다. Tomcat에서는 파라미터 인코딩 및 키와 값을 설정하기 위해 org.apache.catalina.connector.Request.parseParameters 메서드와 org.apache.tomcat.util.http.Parameters.processParameters 메서드를 이용하여 처리하고 있다.
org.apache.catalina.connector.Request.parseParameters 메서드
1 2 3 4 5 6 7 8 9 10 11 | protected void parseParameters() { ... String enc = getCharacterEncoding(); ... if (enc != null ) { parameters.setEncoding(enc); } else { parameters.setEncoding( "ISO-8859-1" ); } ... } |
org.apache.tomcat.util.http.Parameters.processParameters 메서드
1 2 3 4 5 6 7 8 | public void processParameters( byte bytes[], int start, int len, String enc) { ... tmpName.setBytes(bytes, nameStart, nameEnd – nameStart); tmpValue.setBytes(bytes, valStart, valEnd – valStart); ... addParam(urlDecode(tmpName, enc), urlDecode(tmpValue, enc)); ... } |
위 코드를 보면 알 수 있듯이, 인코딩이 올바르지 않게 설정되면 파라미터에 잘못된 값이 들어감을 알 수 있다. 다음 코드는 URL 디코딩이 잘못되면 어떤 결과가 초래되는지 쉽게 살펴볼 수 있는 예이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | String hangul = "한글" ; String[] encodings = new String[] { "EUC-KR" , "UTF-8" , "ISO8859-1" }; for (String encoding1 : encodings) { String encoded = URLEncoder.encode(hangul, encoding1); System.out.println(encoded); System.out.print( "\t" ); for (String encoding2 : encodings) { String decoded = URLDecoder.decode(encoded, encoding2); System.out.print(decoded + "\t\t" ); } System.out.println( "\n" ); } |
웹에서 여러 인코딩을 지원하려면, 인코딩된 URL 문자열과 사용한 인코딩 정보를 파라미터로 전달 해야 한다. 예를 들어, "/search.nhn?query=%C7%D1%B1%DB&ie=EUC-KR" 과 같이 URL이 설정되어 있다면, ie 파라미터 값을 이용하여 query의 파라미터 값을 URL 디코딩하면 된다. 그리고 가능하다면 Javascript의 encodeURI 메서드 (또는 encodeURIComponent 메서드)를 사용하는 것이 좋다.
Javascript에서의 URL 인코딩
Javascript는 escape, encodeURI, encodeURIComponent 메서드를 이용하여 URL을 인코딩할 수 있다. 이 중 escape 메서드는 A~Z, a~z, 0~9, @*-_+./ 문자가 아니면 유니코드 형식으로 인코딩하는데, ASCII 문자는 %XX, 그 외는 %uXXXX 형태로 인코딩된다. 예를 들어, '한글'을 escape 메서드로 인코딩하면, %uD55C%uAE00으로 인코딩되므로, Tomcat에서 URL 디코딩 시에 문제가 발생하게 된다. 일반적으로 문자열을 URL 인코딩하기 위해서 encodeURI 메서드를 많이 사용하며, :;=?& 문자는 인코딩하지 않는다.
Java의 URLEncoder.encode 메서드와 Javascript의 encodeURI 메서드는 공백(whitespace)을 '%20'으로 인코딩하느냐, '+'로 인코딩하느냐만 다르다. 마지막으로 encodeURIComponent 메서드는 encodeURI 메서드와 유사하지만, :;/=?&도 인코딩한다.
브라우저에서의 EUC-KR 인코딩
EUC-KR 인코딩은 2,350자의 한글만 사용할 수 있다. 그러면 EUC-KR 인코딩으로 이루어진 웹 페이지에서 '똠방각하'와 같은 문자열은 어떻게 처리될까? 브라우저 별로 다국어가 포함된 URL 인코딩 처리하는 방법이 다르다. EUC-KR 인코딩으로 표현 가능한 '한글'을 웹 URL에 넣어 브라우저 인코딩 테스트를 해보면 Internet Explorer, Firefox, Chrome 모두 한글을 '%C7%D1%B1%DB' 로 인코딩한다. 그러나 EUC-KR로 인코딩할 수 없는 '똠방각하' 를 처리할 때는 브라우저마다 결과가 다르다. 브라우저 별 EUC-KR 인코딩 방법을 테스트하기 위해 EUC-KR 인코딩을 사용하는 검색 시스템인 알타비스트를 이용해보기로 한다.
브라우저 검색 URL의 p 파리미터 값을 살펴보자.
표 5 브라우저 별 EUC-KR 인코딩 결과
브라우저 | 인코딩된 값 | 화면에 표시되는 문자열 |
Internet Explorer | %26%2346624%3B%B9%E6%B0%A2%C7%CF | 똠방각하 |
Firefox | %A4%D4%A4%A8%A4%C7%A4%B1%B9%E6%B0%A2%C7%CF | ㄸㅗㅁ방각하 |
Chome | %8Cc%B9%E6%B0%A2%C7%CF | c방각하 |
Chrome은 EUC-KR에 있는 확장 완성형의 문자를 지원하지 않기 때문에 '똠'을 인코딩할 수 없다. Internet Explorer는 EUC-KR에 없는 문자의 경우 유니코드 포인트 값으로 표현한다. 즉 '똠'의 유니코드 코드 포인트 값인 46624(U+B620)으로 URL 을 인코딩 한다. Firefox는 한글 채움 문자를 이용하여 음절을 표시하고 있다. 한글 채움 문자는 KS X 1001 표준안에 정의되어 있으며, (채움) 초성 중성 종성의 형태로 표시하고, 초성, 중성, 종성의 값이 없는 경우 (채움)으로 표시한다. EUC-KR 인코딩에서는 (채움) 값이 0xA4 0xD4이므로, 다음과 같이 인코딩된다.
표6 EUC-KR로 똠방각하를 인코딩할 때
인코딩 | 값 |
%A4%D4 | (채움) |
%A4%A8 | ㄸ |
%A4%C7 | ㅗ |
%A4%B1 | ㅁ |
%B9%E6 | 방 |
%B0%A2 | 각 |
%C7%CF | 하 |
알아두면 좋은 것들
영문 MS Windows는 CP1252, 한글 MS Windows는 MS949가 기본 인코딩이다. 리눅스에서는 LANG 환경 변수에 따라 다르지만, ko, ko_KR, ko_KR.eucKR은 모두 EUC-KR 인코딩이며, ko_KR.UTF-8만 UTF-8 인코딩이다. CentOS의 경우 /etc/sysconfig/i18n에서 시스템 기본 인코딩을 설정할 수 있다. 참고로 i18n은 국제화(internationalization)를 의미하며, l10n은 지역화(localization)을 의미한다. 18과 10이라는 숫자는 i와 n 사이, 또는 l과 n 사이의 글자 수를 의미한다. 요즘 편집기는 여러 인코딩을 처리할 수 있으므로, 보통 문서의 처음에 BOM(Byte Order Mark)이라는 값을 지정하여 인코딩 정보를 저장한다. UTF-8은 0xEF 0xBB 0xBF이며, 나머지 인코딩에 대한 BOM 값은 위키백과(http://en.wikipedia.org/wiki/Byte_order_mark)를 참고하면 좋다.
[1] 유니코드, http://ko.wikipedia.org/wiki/%EC%9C%A0%EB%8B%88%EC%BD%94%EB%93%9C
[2] UTF-8, http://ko.wikipedia.org/wiki/UTF-8
[3] 유니코드 정규화,http://ko.wikipedia.org/wiki/%EC%9C%A0%EB%8B%88%EC%BD%94%EB%93%9C_%EC%A0%95%EA%B7%9C%ED%99%94
[4] 옛한글, http://ko.wikipedia.org/wiki/%EC%98%9B%ED%95%9C%EA%B8%80
[5] 바이트 순서 표식,http://ko.wikipedia.org/wiki/%EB%B0%94%EC%9D%B4%ED%8A%B8_%EC%88%9C%EC%84%9C_%ED%91%9C%EC%8B%9D
출처 - http://helloworld.naver.com/helloworld/76650
'Development > Java' 카테고리의 다른 글
java - apache POI 소개 (0) | 2013.09.26 |
---|---|
java - 한글 초성 검색 (0) | 2013.08.30 |
java - 객체 직렬화(Object Serialization) (0) | 2013.08.10 |
rmi - Hessian Web Service Protocol – Hello World Example (0) | 2013.08.09 |
java - 이미지 사이즈 변경(image resize) (0) | 2013.06.22 |