유니코드 문자열 처리

C#은 string 객체를 char 컬렉션으로 사용할 수 있게 해주는 편리한 장치를 제공합니다.

var msg = "안녕하세요";

char first = msg[0];
ReadonlySpan<char> firstTwo = msg[..2];

char key = '안';
bool contains = msg.Contains(key);

// 또는

foreach(char c in msg) 
{ 
   if (c == key )
   {   
      contains = true; break;
   }
 }

닷넷은 이러한 장치들의 성능을 개선하는 방향으로 버전업 되고 있습니다.

Ordinal value

그런데, 이런 성능을 제대로 향유하기 위해서는 그 문자열을 구성하는 모든 문자가 단수 코드 유닛(single code unit)인 지 확인해야 합니다. (예를 들면, 비밀 번호 문자열).

왜냐하면, 닷넷 char 객체는 단수 코드를 갖는 유니코드 값을 나타내기 때문입니다.

char AGrave = '\u00C0';

문자열에 있는 모든 문자가 "단수 코드 값"을 가진 것이 확실하다면, 문자열을 char 컬렉션으로 처리하는 것이 아무런 문제가 없고 효율적입니다.

Character value

문제는 Unicode 문자들 중에는 복수의 코드 유닛을 갖는 것도 있는데, 이들은 char 로 표현할 수 없고, string 으로 표현해야 합니다.

string AGraveCombining = "\u0041\u0300";

AGraveCombining 은 싱글 코드 유닛 문자 두 개를 합쳐 놓은 것인데, 문화권(CultureInfo)에 따라 서로 다른 하나의 문자를 가리킬 수 있습니다.

이 경우, string 객체를 char 컬렉션으로 처리하면 하나의 문자가 두 개의 문자가 되는 문제가 발생합니다.

AGraveCombining : Á  // 키보드에서 \u0030 이 입력되지 않아, \u0193 을 사용.
AGraveCombining[0] : A
AGraveCombining[1] : `

한글은?

다행히 한글은 이러한 문제가 발생하지 않습니다.

왜냐하면, 한글의 자음과 모음,

그리고 자모를 초/중/종성으로 조합한 모든 음절 마다 하나의 코드를 부여했기 때문입니다.

참고로, 한글은 완성형 방식으로 유니코드에 포함되었다는 점을 알 수 있습니다. 이 방식의 단점은 'ㄱ’으로 시작하는 이름 찾기, 받침이 'ㅇ’인 단어 찾기 등과 같이 한글스러운 검색이 용이하지 않거나 불가능하게 된 것입니다.

이모지는?

이모지는 문제가 될 수 있습니다.

image

 string text = "😘";

 foreach (char c in text)
 {
     Console.WriteLine(c);
 }

결과

결과적으로, 우리가 일반적으로 사용하는 ANSI 알파벳과 특수 문자, 한/중/일 문자로만 구성된 문자열의 각 문자를 Ordinal Value 로 처리하는데 아무런 문제가 없습니다.

그러나, 문자열에 이모지가 포함될 것으로 예상되는 경우에도 Ordinal value 로 처리하면, 하나의 문자가 두 개의 코드로 쪼개지고, 쪼개진 각 코드가 유니코드 테이블에 없는 경우, 이상한 문자로 표현되는 현상이 발생합니다.

이러한 현상을 막으려면, 문자열을 Character value 의 집합으로 처리해야 할 필요가 있습니다.

StringInfo 클래스

비단 이모지 뿐만 아니라, 복수의 코드 유닛 문자를 사용하는 언어의 문자들은 string 을 char 집합이 아닌, 단일 문자를 나타내는 string의 집합으로 처리해야 합니다.

이를 위해, 닷넷은 문자열을 유니코드 문자의 집합으로 취급하게 해주는 도구를 제공합니다.

StringInfo Class (System.Globalization) | Microsoft Learn

사용법은 위 문서에 잘 나와 있어 별도로 설명하지는 않지만, 문서 가장 하단에 나타난 주의 사항 - 닷넷 프레임워크 버전 별로, 지원하는 Unicode 표준이 다르다는 점은 주의할 필요가 있을 것 같습니다.

8 Likes

Rune 사용하는 방법도 있지 않나요?

2 Likes

닷넷의 Rune 은 유니코드의 "코드 포인트(숫자 중 유니코드에서 문자 값으로 사용되는 것들)"를 나타내기 위한 것입니다.

유니코드 문자표에서 문자에 할당된 숫자(Scalar value), 혹은 서로게이트 페어로 쓰이는 숫자인가를 판별할 때, 혹은 반대로 문자를 숫자로 표현할 때 유용합니다.

이에 반해, StringInfo 는 문자열을 음절(Grapheme cluster, 닷넷에서는 TextElement) 단위로 취급하게 해줍니다.

유니코드에서는, 비주류 "음절 문자"로 분류되는 각 문자(음절)는 하나 이상의 음소 문자의 조합으로 나타내는 방식을 사용합니다.

음소는 Rune으로 표현되기에, 음절은 Rune의 조합(Combining), 다시 말하면 복수의 Rune 으로 구성됩니다.

이모지는 서로게이트 페어로 나타나기에, 하나의 Rune 객체로 표현이 가능해서, 본문의 예제에 써도 무방하지만, 이모지와 음절 문자가 섞인 문자열에는 적합하지 않습니다.

참고로, 한글은 음절 기반의 문자이지만, 기본 다국어로 분류되어, 모든 음절이 scalar value 를 할당 받았습니다.
그래서, char, Rune, TextElement 를 구분하지 않고 쓸 수 있습니다.

2 Likes

한글도 조합형으로는 낱자마다 character value가 나눠져 있지 않나요?

1 Like

본문에 나와 있듯이 음소 마다 코드도 할당되어 있고, 이들 중 현대 한국어에 쓰이는 것들로만 조합한 음절 (완성형) 11,172자에도 코드가 할당되어 있습니다.

참고로, 옛 고어 음소도 포함하여, 모든 조합 가능한 한글 음절 수는 160만 개가 넘고, 이들 모두에 코드를 할당한다면, 유니코드 전체(16 pane * 65,536/pane)를 써도 턱도 없이 모자란다고 합니다.

물론 유니코드 엔코딩이 문자 조합(Combining)을 지원하기 때문에 조합형으로 엔코딩하는 것도 가능합니다만, 데이터 크기가 완성형 보다 3~4배 많아지므로 효율성 측면에서 잘 사용하지 않는 것이라 합니다.