성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Java - How to use the Foreign Funct...
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>.NET 2.0의 유니코드 관련 문자열 비교 오류</h1> <p> 오호~~~ 재미있는 문제가 하나 올라왔습니다. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C#에서 유니코드 정규화 또는 .StartsWith()에 문제가 있는 것 같습니다 ; <a target='tab' href='https://social.msdn.microsoft.com/Forums/ko-KR/8ff66698-793f-4953-8c16-0cb61477b1a6/c5064049436-50976457685307646300-512214450854868-4660845716?forum=visualcsharpko'>https://social.msdn.microsoft.com/Forums/ko-KR/8ff66698-793f-4953-8c16-0cb61477b1a6/c5064049436-50976457685307646300-512214450854868-4660845716?forum=visualcsharpko</a> </pre> <br /> 위의 글에 나온 소스 코드는 매우 간단한데요.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > string test1 = "끅"; test1 = test1.Normalize(NormalizationForm.FormD); string test2 = "끊"; test2 = test2.Normalize(NormalizationForm.FormD); bool result1 = test1.StartsWith(test2); bool result2 = test2.StartsWith(test1); string t = (result1 ? "true" : "false") + " " + (result2 ? "true" : "false"); Console.WriteLine(t); </pre> <br /> 실제로 이를 실행해 보면, .NET 2.0에서 "ㄲㅡㄱ", "ㄲㅡㄶ"으로 분리된 문자열이 동일하다고 true 값을 반환합니다.<br /> <br /> .NET Reflector로 확인해 보면, string.StartsWith 메서드는 결국 내부적으로 CultureInfo.CurrentCulture.CompareInfo.IsPrefix 메서드를 호출하기 때문에 소스 코드를 다음과 같이 바꿔도 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > string test1 = "끅"; test1 = test1.Normalize(NormalizationForm.FormD); string test2 = "끊"; test2 = test2.Normalize(NormalizationForm.FormD); <span style='color: blue; font-weight: bold'>bool result1 = CultureInfo.CurrentCulture.CompareInfo.IsPrefix(test1, test2); bool result2 = CultureInfo.CurrentCulture.CompareInfo.IsPrefix(test2, test1);</span> string t = (result1 ? "true" : "false") + " " + (result2 ? "true" : "false"); Console.WriteLine(t); </pre> <br /> 역시 .NET Reflector로 디버깅해서 들어가 보면 IsPrefix 메서드 중에서도 nativeIsPrefix를 호출하는 것을 알 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public virtual unsafe bool IsPrefix(string source, string prefix, CompareOptions options) { if ((source == null) || (prefix == null)) { throw new ArgumentNullException((source == null) ? "source" : "prefix", Environment.GetResourceString("ArgumentNull_String")); } if (prefix.Length == 0) { return true; } if (options == CompareOptions.OrdinalIgnoreCase) { return source.StartsWith(prefix, StringComparison.OrdinalIgnoreCase); } if (((options & ~(CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase)) != CompareOptions.None) && (options != CompareOptions.Ordinal)) { throw new ArgumentException(Environment.GetResourceString("Argument_InvalidFlag"), "options"); } if (!this.IsSynthetic) { <span style='color: blue; font-weight: bold'>return nativeIsPrefix(this.m_pSortingTable, this.m_sortingLCID, source, prefix, options);</span> } if (options == CompareOptions.Ordinal) { return nativeIsPrefix(CultureInfo.InvariantCulture.CompareInfo.m_pSortingTable, this.m_sortingLCID, source, prefix, options); } return this.SyntheticIsPrefix(source, 0, source.Length, prefix, GetNativeCompareFlags(options)); } </pre> <br /> 이 때, m_pSortingTable은 포인터 변수이고, m_sortingLCID 변수는 언어 코드가 들어가는 데 한글 윈도우의 경우 1042 값이 대입됩니다. 아쉽지만, 여기까지가 분석의 끝입니다. nativeIsPrefix는 InternalCall로써 .NET Framework 내부에서 C/C++ 코드로 구현되어 있으므로 디버깅이 안됩니다. (혹시나 싶어 웹에 검색해 보니 소스 코드가 역시 없군요. ^^)<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [MethodImpl(MethodImplOptions.InternalCall)] private static extern unsafe bool nativeIsPrefix(void* pSortingTable, int sortingLCID, string source, string prefix, CompareOptions options); </pre> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, 왜 .NET 4.0에서는 잘 동작하는 걸까요? .NET 4.0 역시 StartsWith가 IsPrefix 메서드를 호출하는 것까지는 거의 비슷합니다. 그런데, 결정적으로 nativeIsPrefix가 아닌, 함수 이름조차도 변경된 InternalFindNLSStringEx 메서드를 부르는 차이가 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [SecuritySafeCritical, __DynamicallyInvokable] public virtual bool IsPrefix(string source, string prefix, CompareOptions options) { if ((source == null) || (prefix == null)) { throw new ArgumentNullException((source == null) ? "source" : "prefix", Environment.GetResourceString("ArgumentNull_String")); } if (prefix.Length == 0) { return true; } if (options == CompareOptions.OrdinalIgnoreCase) { return source.StartsWith(prefix, StringComparison.OrdinalIgnoreCase); } if (options == CompareOptions.Ordinal) { return source.StartsWith(prefix, StringComparison.Ordinal); } if ((options & ~(CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase)) != CompareOptions.None) { throw new ArgumentException(Environment.GetResourceString("Argument_InvalidFlag"), "options"); } <span style='color: blue; font-weight: bold'>return (InternalFindNLSStringEx(this.m_dataHandle, this.m_handleOrigin, this.m_sortName, (GetNativeCompareFlags(options) | 0x100000) | ((source.IsAscii() && prefix.IsAscii()) ? 0x20000000 : 0), source, source.Length, 0, prefix, prefix.Length) > -1);</span> } </pre> <br /> 물론, 이 메서드 역시 P/Invoke 호출이기 때문에 더 이상 볼 것이 없습니다. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [SuppressUnmanagedCodeSecurity, SecurityCritical, DllImport("QCall", CharSet=CharSet.Unicode)] private static extern int InternalFindNLSStringEx(IntPtr handle, IntPtr handleOrigin, string localeName, int flags, string source, int sourceCount, int startIndex, string target, int targetCount); </pre> <br /> <hr style='width: 50%' /><br /> <br /> 그나저나, 문제 자체를 좀 더 들여다 볼까요?<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ㄱ: 4353 ㅡ: 4467 (받침) ㄱ: 4520 ㄲ: 4353 ㅡ: 4467 (받침) ㄶ: 4525 </pre> <br /> "극"의 받침을 바꿔서 테스트 해보면 다음과 같은 결과가 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ㄲ: 4521 - false ㄳ: 4522 - false ㄴ: 4523 - false ㄵ: 4534 - false ㄷ: 4526 - false ㄹ: 4527 - false ...[생략: 모두 false]... 'ㄻㅅ': 4562 - true # 이런 받침은 거의(?) 없으므로 무시해도 좋고. </pre> <br /> 재미있군요. ^^ 2가지 종성(ㄶ, ㄻㅅ)에 대해서 true 판단을 해버리는 것입니다. 반면 초성/중성의 경우에는 어떤 것으로 바꿔도 결과는 같습니다. 딱히 규칙을 찾을만한 것이 없어서 더 이상의 탐구는 할 수 없을 것 같습니다.<br /> <br /> 단지, .NET 2.0의 경우 한글이라면 '문화권'에 대한 영향이 없으므로 비교 옵션을 다음과 같이 Ordinal로 명시해 주는 임시 조치를 할 수는 있겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > bool result1 = CultureInfo.CurrentCulture.CompareInfo.IsPrefix(test1, test2, <span style='color: blue; font-weight: bold'>CompareOptions.Ordinal</span>); bool result2 = CultureInfo.CurrentCulture.CompareInfo.IsPrefix(test2, test1, <span style='color: blue; font-weight: bold'>CompareOptions.Ordinal</span>); // .NET 2.0 에서도 result1 == result2 == false </pre> <br /> 그런데, Ordinal 옵션을 함부로 주어도 되는 걸까요? ^^ 이를 위해 MSDN 문서를 들여다 봐야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > CompareOptions Enumeration ; <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.globalization.compareoptions'>https://learn.microsoft.com/en-us/dotnet/api/system.globalization.compareoptions</a> </pre> <br /> <div style='BACKGROUND-COLOR: #ccffcc; padding: 10px 10px 5px 10px; MARGIN: 0px 10px 10px 10px; FONT-FAMILY: Malgun Gothic, Consolas, Verdana; COLOR: #005555'> When possible, the application should use string comparison methods that accept a CompareOptions value to specify the kind of comparison expected. As a general rule, user-facing comparisons are best served by the use of linguistic options (using the current culture), while security comparisons should specify Ordinal or OrdinalIgnoreCase.<br /> <br /> 가능한 경우 응용 프로그램이 예상되는 비교 종류를 지정하려면 CompareOptions 값을 받는 문자열 비교 메서드를 사용해야 합니다. 일반적으로 사용자 쪽 비교는 언어적 옵션(현재 문화권 사용)을 사용하는 것이 가장 좋지만 보안 비교는 Ordinal 또는 OrdinalIgnoreCase를 지정해야 합니다.<br /> </div><br /> <br /> "보안 비교(security comparison)"라는 것은 웹 브라우저의 피싱 사이트 문제를 아시는 분들에게는 익숙한 이야기일 것입니다. 예를 들어, "www.sos.com"의 도메인 명을 가진 웹 사이트가 있는데, 사용자들로 하여금 헷갈릴 수 있도록 "www.søs.com"이라는 유사한 도메인으로 피싱 사이트를 운영할 수 있다는 것입니다. 숫자 0과 영문자 O에 대한 차이를 노리는 것도 마찬가지고. 자세한 것은 다음의 문서를 참조하세요. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Unicode Security Mechanisms ; <a target='tab' href='http://www.unicode.org/reports/tr39/'>http://www.unicode.org/reports/tr39/</a> </pre> <br /> MSDN 예제에도 "AE", "Æ" 비교가 나옵니다. 문화권으로 봤을 때는 이 2개의 글자를 동일하게 보지만, (웹 브라우저의 주소창처럼) 보안 문제를 고려했을 때 이것을 다르게 인식하는 것이 좋기 때문에 같다고 보면 안됩니다. 실제로 다음의 코드는 true를 반환하지만,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > string t1 = "AE"; string t2 = "Æ"; Console.WriteLine(t1.StartsWith(t2)); // true Console.WriteLine(t2.StartsWith(t1)); // true </pre> <br /> Ordinal 옵션을 주면 false를 반환합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > string t1 = "AE"; string t2 = "Æ"; Console.WriteLine(t1.StartsWith(t2, StringComparison.Ordinal)); // false Console.WriteLine(t2.StartsWith(t1, StringComparison.Ordinal)); // false </pre> <br /> <hr style='width: 50%' /><br /> <br /> 정리해 보면, 분명히 "ㄲㅡㄱ", "ㄲㅡㄶ" 을 동일하다고 반환하는 것은 버그가 맞고 이를 해결하려면 .NET 4.0 응용 프로그램으로 마이그레이션하는 것이 가장 적절한 선택입니다. 그게 불가능하다면 그나마 차선책으로 Ordinal 옵션을 주면 된다는!<br /> <br /> 참고로, 기왕에 이글을 읽어 보셨다면 아래의 글도 한번 읽어보세요. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 유니코드와 한글 - 유니코드와 닷넷을 이용한 한글 처리 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/1294'>http://www.sysnet.pe.kr/2/0/1294</a> 개발자 PC 환경 - 유니코드(Unicode)를 위한 설정 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/762'>http://www.sysnet.pe.kr/2/0/762</a> </pre> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1722
(왼쪽의 숫자를 입력해야 합니다.)