Section 11.2: 문자열을 올바른 방법으로 뒤집기 (reverse)

많은 경우 문자열을 뒤집는 (reverse) 동작이 필요로 할 때, 아래와 같은 방법을 사용하게 된다:

char[] a = s.ToCharArray(); System.Array.Reverse(a); string r = new string(a);

그러나, 많은 사람들이 이러한 구현 방식에서 간과하고 있는 사실은 이러한 접근법 자체가 올바른 방법이 아니라는 것이며, 이는 NULL 체크가 빠졌다거나 하는 이유에 기인한 것이 아니다.

위 방식이 올바르지 않은 이유는 바로 Glyph 혹은 Grapheme Cluster 가 여러개의 code point (character 라고도 불리는) 로 이루어져 있을 수 있기 때문이다.

왜 이것이 문제가 되는지를 이해하려면, 우선적으로 우리가 일반적으로 "문자 (character)" 라고 지칭하는 용어가 실제로 어떤 것을 의미하는지를 알아야 한다.

인용 문서:

Character 라는 것은 overload 된 용어로써 여러가지를 의미할 수가 있다.
Code point 는 정보를 표현하는 가장 작은 단위이며, 텍스트라는 것은 연속된 code point 의 집합이다. 각각의 code point 는 유니코드 표준에 의해 의미가 부여된 숫자를 나타내게 된다.
Grapheme 이라는 것은 하나 혹은 그 이상의 연속된 code point 의 집합으로써, 읽는 사람으로 하여금 기록 시스템상에 존재하는 단일 요소로 인식하게 만드는 하나의 기호화된 단위이다. 예를 들어, aä 는 모두 grapheme 이나, 이들은 복수개의 code point 로 이루어져 있을 수 있다 (예를 들어, ä 는 기반이 되는 글자 a 와 diaresis 라고 하는 상단 점 두개를 나타내는 기호 글자 두개의 code point 로 이루어져 있을 수 있다; 그러나 해당 grapheme 을 나타내는 대안의, legacy 인, 단일 code point 역시 존재한다). 일부 code point 들은 어떠한 grapheme 에도 속하지 않는 경우도 있다 (예: zero-width non-joiner, 혹은 directional override 들).
Glyph 는 일종의 이미지로써, 많은 경우 font (glyph 들의 모음인) 에 저장되어 있으며, grapheme 들 혹은 그 일부를 표현하기 위해 사용된다. 폰트들은 복수개의 glyph 를 조합하여 하나의 표현을 나타낼 수 있는데, 예를 들어 위에서 설명한 ä 가 만약 단일 code point 라고 할 때, 폰트가 두개의 공간적으로 중첩된 glyph 를 이용하여 렌더링을 하도록 선택할 수 있다. OTF 의 경우에는, 폰트 내의 GSUB 과 GPOS 테이블이 이러한 용도로 사용될 수 있는 치환/배치 정보를 가지고 있게 된다. 또한 폰트는 하나의 grapheme 에 대하여 복수개의 대체 가능한 glyph 들을 가지고 있을 수도 있다.

따라서 C# 에서는, 문자 (character) 는 실제적으로 Code Point 를 의미한다.

이것이 뜻하는 바는, 만약 사용자가 Les Misérables 와 같은 유효한 문자열을 아래에 나타난대로 문자들의 배열로 표현하여 뒤집으려고 할 경우,

string s = "Les Mise\u0301rables";

아래와 같은 결과를 얻게 될 것이다:

selbaŕesiM seL

위에서 볼 수 있듯이, 강세가 문자 e 가 아닌 r 에 가 있음을 확인할 수 있다.

비록 char 배열을 두번 뒤집는 string.reverse.reverse 가 원래의 문자열을 정상적으로 반환한다 할지라도, 이러한 방식의 뒤집기는 원본 문자열을 뒤집는 올바른 방법이라고 볼 수 없다.

실제 수행이 되어야 할 작업은 각 Grapheme Cluster 를 따로 뒤집는 것이다. 따라서, 제대로 된 방법이라고 한다면 다음과 같이 뒤집을 수 있을 것이다:

private static System.Collections.Generic.List < string > GraphemeClusters(string s) { System.Collections.Generic.List < string > ls = new System.Collections.Generic.List < string > (); System.Globalization.TextElementEnumerator enumerator = System.Globalization.StringInfo.GetTextElementEnumerator(s); while (enumerator.MoveNext()) { ls.Add((string) enumerator.Current); } return ls; } private static string ReverseGraphemeClusters(string s) { if (string.IsNullOrEmpty(s) || s.Length == 1) return s; System.Collections.Generic.List < string > ls = GraphemeClusters(s); ls.Reverse(); return string.Join("", ls.ToArray()); } public static void TestMe() { string s = "Les Mise\u0301rables"; // s = "noël"; string r = ReverseGraphemeClusters(s); // 다음은 잘못된 방식을 나타낸다: // char[] a = s.ToCharArray(); // System.Array.Reverse(a); // string r = new string(a); System.Console.WriteLine(r); }

이러한 방식으로 올바른 뒤집기를 구현하였다면,이는 아시아 / 동남아시아 언어들을 비롯한 불어 / 스웨덴어 / 노르웨이어 등등 다양한 언어에 대해서도 정상 동작함을 확인할 수 있을 것이다.

본 문서는 C# Notes for Professionals (라이센스:CC-BY-SA) 를 한글로 번역한 문서입니다. 번역상 오류가 있을 수 있으므로 정확한 내용은 원본 문서를 참고하세요.

[출처] https://books.goalkicker.com/CSharpBook/

반응형

+ Recent posts