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/
'번역 > C# Notes for Professionals' 카테고리의 다른 글
11.4: 문자열의 좌/우/중간으로부터 x 개의 문자를 가져오기 (0) | 2021.01.27 |
---|---|
11.3: 문자열을 고정 길이로 표시하기 위해 여백 문자 채워넣기 (padding) (0) | 2021.01.26 |
11.1: 문자열 서식 (format) 사용하기 (0) | 2021.01.20 |
10.4: 다중행 (multi-line) 문자열 처리하기 (0) | 2021.01.19 |
10.3: 축자 문자열을 이용하여 컴파일러로 하여금 escape 문자를 사용하지 않도록 하기 (0) | 2021.01.18 |