Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

유니코드와 한글 - 유니코드와 닷넷을 이용한 한글 처리

유니코드에 대해 잘 정리된 글이 하나 있습니다. ^^

한글 인코딩의 이해 1편: 한글 인코딩의 역사와 유니코드 
; http://helloworld.naver.com/helloworld/19187

2012년 5월호 마이크로소프트웨어 잡지에 보면 위의 글을 쓴 분이 역시 비슷한 내용으로 "유니코드와 한글 - 유니코드와 JAVA를 이용한 한글 처리"라는 글을 쓰셨습니다. (2개의 내용이 다소 다르기 때문에 모두 읽어보셔도 좋습니다. 유니코드 문자셋과 유니코드 인코딩 방식의 차이를 모른다면 꼭 읽어보시길 바랍니다. ^^)

위의 글에 보면, JAVA와 한글의 관계가 나오는데요. 저도 ^^ 이와 빗대어서 "유니코드와 한글 - 유니코드와 닷넷을 이용한 한글 처리"를 정리해 보려고 이렇게 글을 쓰는 것입니다.





1. 닷넷의 기본 문자열 인코딩

일단 자바의 String 개체는 UTF-16 BE 인코딩을 사용한다고 하는데요. 닷넷은 어떨까요? 이에 대해서는 다음의 글에서 찾아볼 수 있습니다.

Character Encoding in the .NET Framework
; https://docs.microsoft.com/ko-kr/dotnet/standard/base-types/character-encoding

UTF-16 encoding is used by the common language runtime to represent Char and String values, and it is used by the Windows operating system to represent WCHAR values.


즉, 자바와 다른점이라면 UTF-16 BE 가 아닌 UTF-16 LE 정도!

그 외에 자바의 경우 "한글 인코딩의 이해 1편: 한글 인코딩의 역사와 유니코드"글을 읽다 보면 '변형된 UTF-8' 이라는 방식이 나오는데요.

(자바는) 문자열 전송/수신을 위해 직렬화가 필요할 때 변형된 UTF-8(Modified UTF-8)을 사용한다. ... 변형된 UTF-8에서 U+0000을 2바이트로 표시하는 이유는 인코딩된 결과에 널 문자(00)가 나타나지 않게 하고 ... (하는 이유로 U+0000 값이 0xc080 으로 표현된다.)


애석하게도 제가 자바에 대한 이해도가 낮아 "변형된 UTF-8"이 실제로 사용되는 지에 대한 테스트를 해볼 수는 없었습니다. 단지, String.getBytes 에 "UTF-8" 값을 전달하면 "변형된 UTF-8"이 아닌 정상적인 UTF-8 인코딩 된 값이 나오는 것으로 보아,

String string = "\0한글";

String charsetName = "UTF-8";
try 
{
    byte [] bytes = string.getBytes(charsetName);
        
    for (byte b : bytes)
    {
        System.out.print(String.format("0x%02X ", b));
    }
} catch (UnsupportedEncodingException ex)
{
}

// 출력 결과 - 0x00 값이 0xc080 으로 나오지 않기 때문에 '변형된 UTF-8' 인코딩은 아님.
0x00 0xED 0x95 0x9C 0xEA 0xB8 0x80 

자바의 내부에서만 특별하게 사용되는 것 같습니다. 닷넷의 경우는, 제가 알기로는 "변형된 UTF-8" 같은 것은 사용하지 않습니다.





2. 코드 포인트 값 구하기

자바는 유니코드의 코드 포인트 값을 확인하기 위해 String.codePointAt 메서드를 사용한다고 하는데요.

String string = "\0한글";
for (int i = 0; i < string.length(); i ++)
{
    System.out.print(String.format("U+%04X ", string.codePointAt(i)));
}

// 출력 결과
U+0000 U+D55C U+AE00

테스트를 해보니 charAt 메서드로 반환된 char 타입이 갖는 값과 동일했습니다. (잠시 후에 설명되지만, 둘의 분명한 차이점이 있습니다.)

for (int i = 0; i < string.length(); i ++)
{
    char ch = string.charAt(i);
    int chNumber = ch;
    System.out.print(String.format("0x%02X ", chNumber));
}

// 출력 결과
0x00 0xD55C 0xAE00 

닷넷의 경우를 확인해 볼까요?

우선, string 의 메모리 포인터 자체를 반환해서 값을 출력해 보면 자바의 출력 결과와 동일한 코드 포인트 값을 얻게 됩니다.

string text = "\0한글";
fixed (char* ptr = text)
{
    char *pStart = ptr;

    for (int i = 0; i < text.Length; i++)
    {
        int code = *(pStart + i);
        string txt = string.Format("0x{0:x}", code);
        Console.WriteLine(txt);
    }
}

// 출력 결과
0x0
0xd55c
0xae00

물론, char 타입이 가지고 있는 값도 동일합니다.

foreach (char a in text)
{
    Console.WriteLine(((int)a).ToString("x"));
}

// 출력 결과
0x0
0xd55c
0xae00

사실, codePointAt 값과 char 값이 동일한 것은 16비트로 표현되는 유니코드 문자셋에 한해서입니다. 그 이상을 넘어가면 차이를 보입니다. 가령, 16비트를 넘어서는 문자의 예로 0x10000 과 0x10001 을 들어보겠습니다.

Unicode Character 'LINEAR B SYLLABLE B008 A' (U+10000)
; http://www.fileformat.info/info/unicode/char/10000/index.htm

Unicode Character 'LINEAR B SYLLABLE B038 E' (U+10001)
; http://www.fileformat.info/info/unicode/char/10001/index.htm

위의 글에 따라 0x10000 문자는 "unicode_in_net_1.png - LINEAR B SYLLABLE B008 A"라는 글자이며 0x10001 문자는 "unicode_in_net_2.png - LINEAR B SYLLABLE B038 E"라는 글자입니다.

이를 자바로 코딩해서 codePointAt을 구하면 다음과 같이 출력 됩니다.

String string = "\uD800\uDC00\uD800\uDC01";
for (int i = 0; i < string.length(); i ++)
{
    System.out.print(String.format("U+%04X ", string.codePointAt(i)));
}

// 출력 결과
U+10000 U+DC00 U+10001 U+DC01

이상하군요. 문자열의 크기는 2가 아닌 4로 구해지며 그에 따라 code point값을 출력하는데 0xdc00, 0xdc01 값이 꼭 따라붙게 됩니다. 혹시 0x10000, 0x10001 값만 출력되게 하는 방법이 있을지 모르지만... 어쨌든 그런데로 코드 포인트값이 정확하게 구해지고 있습니다.

반면 닷넷의 경우에는 UTF-16 인코딩 된 값만을 가지고 있으므로 다음과 같이 그대로 4바이트의 UTF-16 바이트 값이 출력됩니다.

string text = "\uD800\uDC00\uD800\uDC01";

foreach (char a in text)
{
    Console.Write("0x" + ((int)a).ToString("x") + " ");
}

// 출력 결과
0xd800 0xdc00 0xd800 0xdc01

굳이 2바이트를 넘어가는 유니코드에 대해 코드 포인트 값을 닷넷에서 알고 싶다면 이대로는 안되고, UTF-32로 변형한 후 바이트 배열을 4 바이트씩 끊어서 가져오는 방법을 사용해야 합니다.

string text = "\uD800\uDC00\uD800\uDC01";

byte[] textBytes = Encoding.UTF32.GetBytes(text);
for (int i = 0; i < textBytes.Length / 4; i++)
{
    int codePoint = BitConverter.ToInt32(textBytes, i * 4);
    Console.Write("0x" + codePoint.ToString("x") + " ");
}

// 출력 결과
0x10000 0x10001





3. UCS-2와 UTF-16의 차이점

"한글 인코딩의 이해 1편: 한글 인코딩의 역사와 유니코드"글에 보면 다음과 같은 말이 나오는데요.

유니코드의 인코딩 방식으로는 코드 포인트를 코드화한 UCS-2와 UCS-4, 변환 인코딩 형식(UTF, UCS Transformation Format)인 UTF-7, UTF-8, UTF-16, UTF-32 인코딩 등이 있다.


그럼, UCS-2와 UTF-16에는 과연 어떤 차이가 있는 걸까요?

이에 대해서 검색해 보니, 다음의 글이 나오는데요.

유니코드의 UCS와 UTF
; http://sweeper.egloos.com/165361

UTF-16
; http://ko.wikipedia.org/wiki/UTF-16

국제 문자 세트
; http://ko.wikipedia.org/wiki/UCS

위의 글을 정리해 보면, UCS-2는 16비트(2바이트)정수를 사용하므로 유니코드 31비트 문자셋 중에서 16비트 이하의 부분만을 표현하도록 되어 있는 반면, UTF-16 은 UCS-2를 포함하면서 유니코드 문자셋을 21비트까지 표현할 수 있다는 차이가 있습니다.

간단한 예를 들면, 유니코드로 0x1d11e 의 값은 UCS-2로는 인코딩이 불가능하지만, UTF-16으로는 0xd834, 0xdd1e 로 인코딩하는 것이 가능합니다.





4. 닷넷과 한글 인코딩

그 다음, 자바의 String.getBytes 출력 결과를 비교해야 할 필요가 있을 것 같습니다.

위에서도 한번 예를 들었지만, String.getBytes 에 인코딩 방식을 나타내는 문자열을 전달해 주었는데... 그렇다면 전달하지 않은 경우에는 어떻게 될까요? 영문 윈도우에 한글 설정이 추가된 환경에서,

개발자 PC 환경 - 유니코드(Unicode)를 위한 설정
; https://www.sysnet.pe.kr/2/0/762

다음의 코드를 테스트 해보았습니다.

String string = "\0한글";

byte [] bytes = string.getBytes();
        
for (byte b : bytes)
{
    System.out.print(String.format("0x%02X ", b));
}

// 출력 결과
0x00 0xC7 0xD1 0xB1 0xDB 

자바의 문서에 보면 "Encodes this {@code String} into a sequence of bytes using the platform's default charset, storing the result into a new byte array." 라고 씌여 있으므로 (영문 윈도우의 기본 인코딩 값이며 닷넷에서는 "Windows-1252"로 알려진) CP1252 인코딩으로 나와야 할텐데, 위의 출력 결과는 그렇지 않습니다.

동일한 운영체제에서 닷넷으로 Default 인코딩으로 변환하면 다음과 같은 결과가 나옵니다.

string text = "\u0000한글";
foreach (byte aByte in Encoding.Default.GetBytes(text))
{
    Console.Write("0x" + aByte.ToString("x") + " ");
}

// 출력 결과
0x0 0x3f 0x3f 

0x3f 는 '?' 문자인데, 인코딩 시에 해당 문자값이 없는 경우 대체되는 값으로 일반적인 상황에서 이를 '깨졌다'고 표현합니다. 그리고 이는 Encoding.GetEncoding("Windows-1252").GetBytes 를 호출했을 때와 동일한 결과입니다. 그렇다면 (영문 윈도우에 한글이 설치된 경우) 자바가 판단한 "platform's default charset"은 무엇일까요?

"한글 인코딩의 이해 1편: 한글 인코딩의 역사와 유니코드"글에 보면 다음과 같은 설명이 있는데,

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로 지정해야 한다.


여기서 euc-kr 과 MS949 방식을 테스트 해볼 필요가 있을 것 같습니다.

일단, euc-kr 로 테스트 하면 다음과 같이 자바의 기본 getBytes 와 동일한 값을 얻을 수 있습니다.

string text = "\u0000한글";
foreach (byte aByte in Encoding.GetEncoding("EUC-KR").GetBytes(text))
{
    Console.Write("0x" + aByte.ToString("x") + " ");
}

// 출력 결과
0x0 0xc7 0xd1 0xb1 0xdb 

그렇다면 MS949 는 어떻게 지정해야 할까요?

제가 예전에 닷넷에서 지원되는 인코딩 방식을 나열했었는데,

닷넷에서 지원되는 문자열 인코딩 이름 목록
; https://www.sysnet.pe.kr/2/0/1147

여기 보면, 한글 관련해서 euc-kr 과 ks_c_5601-1987 이 있는 것을 볼 수 있습니다. 바로 그 "ks_c_5601-1987" 이 MS949 에 해당합니다. 사실 "ks_c_5601-1987" 이름에 대해서는 논란이 많습니다. 아래의 글에 보면,

한글 표현에 대한 고찰
; http://tenny.egloos.com/2598689

MS949 라고 마이크로소프트에서 그냥 이름지었으면 되었을 텐데, 이를 euc-kr == "ks_c_5601-1987" 로 기존에 사용되던 인코딩 이름을 가져다가 쓴 것이어서 혼란이 왔다는 것입니다.

어쨌든, 그렇다면 자바에서 String.getBytes에서 기본 사용한 인코딩 방식이 무엇인지 확인하려면 euc-kr 에서는 표현이 안되는 "똠"이라는 글자를 넣어보면 됩니다. 만약 표현이 안되면 euc-kr 이고 표현이 되면 MS949 방식인 거죠.

String string = "\0한글똠";

byte [] bytes = string.getBytes();
        
for (byte b : bytes)
{
    System.out.print(String.format("0x%02X ", b));
}

// 출력 결과
0x00 0xC7 0xD1 0xB1 0xDB 0x8C 0x63 

보시는 것처럼, 제대로 0x8c, 0x63으로 얻어온 것을 보면 자바의 getBytes에서 "MS949" 방식으로 인코딩 한 것이 맞는 것 같습니다. 그리고, 이와 동일한 결과를 닷넷에서 얻으려면 Microsoft가 차용한 "ks_c_5601-1987"을 "MS949" 대신 지정하면 됩니다.

foreach (byte aByte in Encoding.GetEncoding("ks_c_5601-1987").GetBytes(text))
{
    Console.Write("0x" + aByte.ToString("x") + " ");
}

// 출력 결과
0x0 0xc7 0xd1 0xb1 0xdb 0x8c 0x63 

정리가 좀 되시나요?

euc-kr 은 2,350 자의 한글 표현이 지원되는 인코딩이고, MS949(윈도우 운영체제에서는 ks_c_5601-1987)는 11,172자의 한글 표현이 가능합니다. 그리고 IBM이 만든 CP949는 닷넷의 문자열 인코딩에는 기본적으로 지원되지 않습니다. euc-kr 과 MS949는 코드 체계가 유사하지만 유니코드와는 완전히 다릅니다. 가령 "똠" 이라는 글자는 MS949 에서는 "0x8c,0x63" 이지만 유니코드에서는 "U+b620"에 해당합니다.





5. 유니 코드 정규화

자바에서 지원되는 Normalizer.normalize 메서드는 닷넷에서도 지원됩니다.

자바의 아래와 같은 예제는,

String han = "한";

String nfd = Normalizer.normalize(han, Normalizer.Form.NFD);
String nfc = Normalizer.normalize(nfd, Normalizer.Form.NFC);

닷넷에서 다음과 같이 바뀔 수 있습니다.

string han = "한";

string nfd = han.Normalize(NormalizationForm.FormD); // nfd == ㅎ ㅏ ㄴ
string nfc = nfd.Normalize(NormalizationForm.FormC); // nfc == 한

NFD (정준 분해) - NormalizationForm.FormD
NFC (정준 분해한 뒤 다시 정준 결합) - NormalizationForm.FormC
NFKD (호환 분해) - NormalizationForm.FormKD
NFKC (호환 분해한 뒤 다시 정준 결합) - NormalizationForm.FormKC




이 정도면, 닷넷과 유니코드의 관계를 거의 파악한 것 같습니다. 위의 글을 잘 이해하셨다면, 지금쯤 유니코드 및 기타 문자셋들이 눈에 보이실 것입니다. ^^

그 외에 닷넷 String 과 관련해서 좀더 자세한 정보는 다음의 문서를 참조하시면 도움이 됩니다.

String Class
; http://msdn.microsoft.com/en-us/library/system.string.aspx

특히 위의 글에 포함된 다음의 토픽들이 유니코드와 연관이 있습니다.

Char Objects and Unicode Characters
; http://msdn.microsoft.com/en-us/library/system.string.aspx#Characters

Normalization
; http://msdn.microsoft.com/en-us/library/system.string.aspx#Normalization

(첨부된 파일은 본문의 내용을 테스트한 예제입니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]





[최초 등록일: ]
[최종 수정일: 10/14/2020 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer@outlook.com

비밀번호

댓글 쓴 사람
 



2013-01-15 07시24분
[steve] 좋은글 감사합니다.
[손님]
2013-06-10 01시07분
한글 초성검색 + 기타 확장기능 라이브러리 만들기 (C#)
; http://blog.daum.net/modamoda/15692730
정성태
2014-03-30 01시16분
[좋은 글 잘 보았습니다..]
지금까지 본 한글 관련 컴퓨터 글 중에서 "가장" 좋은 글인 것 같습니다.

이제 C# 공부한지 한 달 되었고..
엇그제 C#에서 한글 자모 해체 연습을 완료했습니다.

오늘.. (그러니까, 방금 전) 이 글을 보니까..
오,
정규화(정중분해)라는 방법이 있다는 걸 처음으로 알았습니다.

오,
정말로.. 놀랍네요..
  
화라락 해체하네요..

근데, 자모가 자동으로 달라붙습니다.
자모 사이에 공백을 하나씩 주어야 하네요.. ^^
전문 용어가 뭔지는 모르지만,
유니코드 중에서 "Hangul jamo"로 변환된 것이군요..

제가 수작업한 것은 "Hangul compatibility jamo"입니다.
분해한 자모가 자동으로 달라붙지 않습니다. ^^.

좋은 글 잘 보았습니다.
감사합니다.



(참고로 제 코드 남겨도 실례는 아니겠지요..
 시간 나시면 좋은 조언 해주세요..)


string 내용 = "한글 자모 해체";
int 유니코드 = 0;
int 초성임시 = 0;
int 종성임시 = 0;
string 초성 = "";
string 중성 = "";
string 종성 = "";
StringBuilder 누적 = new StringBuilder();    
foreach ( char 한글자 in 내용 )
    if ( Regex.IsMatch( 한글자.ToString(), "[가-힣]" ) )    
    {
        유니코드 = Convert.ToInt32( 한글자 );
        초성임시 = ( 유니코드 - 44032 ) / 588;
        종성임시 = ( 유니코드 - 44032 ) % 28;
        초성 = Convert.ToChar( 12593 + 초성임시 + ( 초성임시 < 2 ? 0 : 초성임시 < 3 ? 1 : 초성임시 < 6 ? 3 : 초성임시 < 9 ? 10 : 11 ) ).ToString();
        중성 = Convert.ToChar( ( ( 유니코드 - 44032 ) % 588 ) / 28 + 12623 ).ToString();
        종성 = 종성임시 == 0 ? "" : Convert.ToChar( ( 12592 + 종성임시 + ( 종성임시 < 8 ? 0 : 종성임시 < 18 ? 1 : 종성임시 < 23 ? 2 : 3 ) ) ).ToString();
        누적.Append( 초성 + 중성 + 종성 );
    }
    else
        누적.Append( 한글자 );    
MessageBox.Show( 내용 + "\n\n" + 누적.ToString() );







[손님]
2017-07-17 08시50분
[이동우] 좋은글 잘 읽고 갑니다.

자바쪽 변형된 UTF-8.... 예전에 이것때문에 자바쪽이랑 통신하다가 잘안되었었던 기억이 갑자기 생각났네요.ㅎㅎ
[손님]
2017-08-21 05시42분
[개발자] 도로명 주소 DB 불러오는데 MS949으로 인코딩 되어 있어서 한참을 삽질하고 있었는데 덕분에 해결되었습니다. 갑사합니다~~
[손님]
2017-09-18 09시38분
[손님] 좋은 글 감사합니다.
[손님]
2018-11-20 09시46분
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12379정성태10/21/202058.NET Framework: 955. .NET 메서드의 Signature 바이트 코드 분석파일 다운로드2
12378정성태10/20/2020132.NET Framework: 954. C# - x86/x64 환경에 따라 달라지는 P/Invoke 함수의 export 이름파일 다운로드1
12377정성태10/15/2020141디버깅 기술: 172. windbg - 파일 열기 시점에 bp를 걸어 파일명 알아내는 방법(Managed/Unmanaged)
12376정성태10/15/202050오류 유형: 669. windbg - sos의 name2ee 명령어 실행 시 "Failed to request module list." 오류
12375정성태10/15/2020174Windows: 177. 윈도우 탐색기에서 띄우는 cmd.exe 창의 디렉터리 구분 문자가 'Yen(&#0165;)' 기호로 나오는 경우 [1]
12374정성태10/14/2020144.NET Framework: 953. C# 9.0 - (6) Function pointers파일 다운로드2
12373정성태10/14/202074.NET Framework: 952. OpCodes.Box와 관련해 IL 형식으로 직접 코딩 시 유의할 점
12372정성태10/14/2020148.NET Framework: 951. C# 9.0 - (5) Attributes on local functions파일 다운로드1
12371정성태10/13/202055개발 환경 구성: 519. Visual Studio의 Ctrl+Shift+U (Edit.MakeUppercase) 단축키가 동작하지 않는 경우
12370정성태10/13/202055Linux: 33. Linux - nmcli를 이용한 고정 IP 설정
12369정성태10/21/2020842Windows: 176. Raymond Chen이 한글날에 밝히는 윈도우의 한글 자모 분리 현상 [1]
12368정성태10/12/202050오류 유형: 668. VSIX 확장 빌드 - The "GetDeploymentPathFromVsixManifest" task failed unexpectedly.
12367정성태10/12/202052오류 유형: 667. Ubuntu - Temporary failure resolving 'kr.archive.ubuntu.com'
12366정성태10/13/2020135.NET Framework: 950. C# 9.0 - (4) Native ints파일 다운로드1
12365정성태10/12/2020129.NET Framework: 949. C# 9.0 - (3) Lambda discard parameters파일 다운로드1
12364정성태10/11/2020171.NET Framework: 948. C# 9.0 - (2) Skip locals init파일 다운로드1
12363정성태10/11/2020181.NET Framework: 947. C# 9.0 - (1) Target-typed new파일 다운로드1
12362정성태10/11/2020161VS.NET IDE: 151. Visual Studio 2019에 .NET 5 rc/preview 적용하는 방법
12361정성태10/19/2020241.NET Framework: 946. C# 9.0을 위한 개발 환경 구성
12360정성태10/8/202069오류 유형: 666. The type or namespace name '...' does not exist in the namespace 'Microsoft.VisualStudio.TestTools' (are you missing an assembly reference?)
12359정성태10/7/202064오류 유형: 665. Windows - 재부팅 후 iSCSI 연결이 끊기는 문제
12358정성태10/7/202053오류 유형: 664. Web Deploy 설치 시 "A newer version of Microsoft Web Deploy 3.6 was found on this machine." 오류
12357정성태10/7/202052오류 유형: 663. 이벤트 로그 - The storage optimizer couldn't complete retrim on New Volume
12356정성태10/7/202077오류 유형: 662. ASP.NET Core와 500.19, 500.21 오류 (0x8007000d)
12355정성태10/3/2020102오류 유형: 661. Hyper-V Linux VM의 Internal 유형의 가상 Switch에 대한 IP 연결이 되지 않는 경우
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...