[C++] 마이크로소프트는 왜 setlocale에서 UTF-8을 지원하지 않을까?

지난 글에서 우리는 setlocale에서 UTF-7이나 UTF-8을 지정할 경우 실패한다는 것을 알게 되었다. 하지만 여전히 의문은 남는다. 왜 MS는 setlocale에서 흔해빠진 UTF-8 따위를 지원하지 않을까? 도대체 무엇이 우리를 가로 막는 것일까? 안 될 이유가 없는게 안되면 한번쯤은 내부가 궁금해지게 마련이다. 그래서 이런 간단한 코드를 디버깅 해보기로 했다.

int main()
{
    printf("%p\n", setlocale(LC_CTYPE, "Korean_Korea.65001"));
    return 0;
}

디버깅 결과 총 세 지점에 우리를 가로막는 허들이 존재한다는 사실을 알게 되었다. 첫번째는 아래 지점이다. 대놓고 CP_UTF7과 CP_UTF8은 안 된다는 소리다. 재미난 사실은 if 문에 이 코드가 들어간 것이 Visual Studio의 특정 버전 부터라는 점이다. Visual C++ 6.0에는 CP_UTF7과 CP_UTF8을 검사하는 코드가 없다. 단지 IsValidCodePage로만 체크한다.

BOOL __cdecl __get_qualified_locale(const LPLC_STRINGS lpInStr, LPLC_ID lpOutId,
                                    LPLC_STRINGS lpOutStr)
{

    // ...

    //  verify codepage validity
    if (!iCodePage || iCodePage == CP_UTF7 || iCodePage == CP_UTF8 ||
        !IsValidCodePage((WORD)iCodePage))
        return FALSE;

    // ...
}

다음 허들은 아래 지점이다. 현재 코드페이지로 변환 되는 최대 글자 개수가 MB_LEN_MAX보다 크면 안 된다는 소리다. MB_LEN_MAX의 경우 Visual Studio 2015는 5로 정의되어 있다. 따라서 이 코드는 넘어간다. 반면에 Visual C++ 6.0에서는 MB_LEN_MAX가 2고 CP_UTF8은 이 구문을 넘지 못하고 실패한다.

int __cdecl __init_ctype (
        pthreadlocinfo ploci
        )
{
    // ...

        if (lpCPInfo.MaxCharSize > MB_LEN_MAX)
            goto error_cleanup;

    // ...
}

마지막 허들은 아래 지점이다. MultiByteToWideChar 함수를 통해서 버퍼가 얼마나 커야 하는지 구하는 부분이다. code_page에 CP_UTF8이 들어 있으면 플래그로 0이나 MB_ERR_INVALID_CHARS가 전달되어야 하지만 여기에서는 MB_PRECOMPOSED가 합쳐서 전달된다. MSDN 페이지에 나와 있는 것처럼 ERROR_INVALID_FLAGS로 실패한다.

static BOOL __cdecl __crtGetStringTypeA_stat(
        _locale_t plocinfo,
        DWORD    dwInfoType,
        LPCSTR   lpSrcStr,
        int      cchSrc,
        LPWORD   lpCharType,
        int      code_page,
        int      lcid,
        BOOL     bError
        )
{
    // ...

        /* find out how big a buffer we need */
        if ( 0 == (buff_size = MultiByteToWideChar( code_page,
                                                    bError ?
                                                        MB_PRECOMPOSED |
                                                        MB_ERR_INVALID_CHARS
                                                        : MB_PRECOMPOSED,
                                                    lpSrcStr,
                                                    cchSrc,
                                                    NULL,
                                                    0 )) )
            return FALSE;

    // ...
}

여기까지 따라오면 대충 뭔가 감이 잡힌다. 마지막 MSDN의 주석에 있던 코드페이지를 넘기면 setlocale은 죄다 실패한다. 그리고 그 코드 페이지가 가지고 있는 대체적인 특징은 한 글자가 2바이트가 넘는 3바이트, 4바이트 등으로 변환되는 코드페이지라는 점이다.

For the code pages listed below, dwFlags must be set to 0. Otherwise, the function fails with ERROR_INVALID_FLAGS.

50220
50221
50222
50225
50227
50229
57002 through 57011
65000 (UTF-7)
42 (Symbol)

Note For UTF-8 or code page 54936 (GB18030, starting with Windows Vista), dwFlags must be set to either 0 or MB_ERR_INVALID_CHARS. Otherwise, the function fails with ERROR_INVALID_FLAGS.

— MultiBytesToWideChar MSDN

다시 setlocale의 MSDN 페이지를 살펴보자. UTF-7과 UTF-8을 넣으면 실패한다는 앞 문장에 의미심장한 설명이 있다. 한 글자당 두 바이트를 넘게 사용하는 코드 페이지는 예외라는 설명이다. 예외~ 즉, UTF-8이 안 되는 것이 아니라 한 글자에 두 바이트를 넘게 쓰는 코드페이지는 죄다 setlocale로 지정할 수 없고, wcstombs나 mbstowcs 함수로 변환할 수 없다는 것을 의미한다.

The set of available locale names, languages, country/region codes, and code pages includes all those supported by the Windows NLS API except code pages that require more than two bytes per character, such as UTF-7 and UTF-8.

— setlocale MSDN

그렇다면 MS는 왜 한 글자당 두 바이트 이상을 사용하는 코드페이지는 예외 처리를 했을까? 이 페이지에 약간의 힌트가 나와 있다. 수많은 레거시 코드가 멀티바이트에서 한 글자에 사용할 수 있는 저장 공간이 최대 두 바이트라고 가정하고 작성했기 때문이란다. 말 그대로 레거시 코드와의 호환성을 위해서 MS는 setlocale로 UTF-8을 지정할 수 없도록 만들었다. 미래 세대의 불편함 따위보다는 과거 세대와의 호환성 유지가 더 중요했기 때문이리라…​