70 와이드 문자 ① — wchar_t와 멀티바이트 변환
먼저 알아야 할 것
LC_CTYPE돌아보기
9장에서 UTF-8은 한 글자가 1~4바이트라고 했다. 그렇다면 “글자 하나를 담는 타입”을 만들면 되지 않는가? C에는 그런 것이 있는가?
답. 있다. 그것이 wchar_t이고, C95가 바로 그 발상으로 들여왔다 — 한 글자를 한 단위로 담자. 문제는 그 발상이 30년 뒤의 현실과 어긋났다는 데 있다.
표준은 wchar_t의 크기도 인코딩도 정하지 않았다. “지원하는 로케일 중 가장 큰 확장 문자집합의 모든 원소를 서로 다른 코드로 표현할 수 있는 정수형”이라고만 했다. 그 결과 리눅스에서는 4바이트 UTF-32가 되었고 Windows에서는 2바이트 UTF-16이 되었다 — 그리고 뒤쪽에서는 「한 글자 = 한 단위」가 성립하지 않는다.
이 장은 그 타입의 정확한 계약과, 바이트열이 글자로 풀리는 과정을 한 단계씩 본다. 어긋남의 결과는 다음 장에서 본다.
이 장의 필요성과 맥락
mbrtowc 가 무엇에 의존하는지 말할 수 없다.이 장이 끝나면
wchar_t의 정체를 밑바닥까지 본다. 표준의 정의와 구현이 밝히는 매크로, 다섯 가지 문자 상수와 문자열의 타입·크기·바이트, MB_CUR_MAX와 MB_LEN_MAX의 차이, mbstate_t가 왜 필요한지, 그리고 mbrtowc가 바이트를 먹으며 글자를 만들어 내는 과정을 단계별 실측으로.이 장에서 답할 질문
- 소스 코드에
"한"이라고 적으면 어떤 바이트가 되는가? - UTF-8은 최대 4바이트인데 왜
MB_CUR_MAX가 6인가?
70.1 표준이 정한 것과 정하지 않은 것
wchar_t는 <stddef.h>가 정의하는 정수 타입이다. 표준의 정의를 그대로 옮기면 “지원되는 로케일 중에서 지정된 가장 큰 확장 문자집합의 모든 원소에 대해 서로 다른 코드를 표현할 수 있는 정수 타입”이다.
여기에 없는 것을 보아야 한다.
| 표준이 정한 것 | 정하지 않은 것 |
|---|---|
| 정수 타입이다 | 몇 바이트인지 |
| 문자마다 다른 값을 가진다 | 부호가 있는지 |
| 기본 문자집합의 값은 양수 | 어떤 인코딩인지(유니코드인지도) |
표 70.1
그래서 이식성 있는 코드는 wchar_t의 값을 유니코드 코드포인트로 가정할 수 없다. 대신 구현이 그 사실을 밝히는 매크로가 있다.
| 매크로 | 정의되어 있으면 |
|---|---|
__STDC_ISO_10646__ | wchar_t 값이 ISO/IEC 10646(유니코드) 코드포인트와 같다. 값은 yyyymmL 꼴 |
__STDC_UTF_16__ | char16_t가 UTF-16이다 |
__STDC_UTF_32__ | char32_t가 UTF-32이다 |
__STDC_MB_MIGHT_NEQ_WC__ | 기본 문자집합의 문자라도 'x'와 L'x'의 값이 다를 수 있다 |
표 70.2
examples/ch70/wide_lit.c
/* 다섯 가지 문자 상수와 문자열 — 타입, 요소 크기, 그리고 실제 바이트. */
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <uchar.h>
#include <wchar.h>
/* 어떤 배열이든 요소 하나씩 16진수로 늘어놓는다.
char 은 부호 있는 구현이 흔하므로(27장) 요소 크기만큼 잘라 낸다 —
그러지 않으면 0xED 가 FFFFFFED 로 늘어나 보인다. */
#define DUMP(label, arr) \
do { \
printf(" %-10s %zu elements of %zu bytes: ", (label), \
sizeof (arr)[0], sizeof (arr) / sizeof (arr)[0]); \
for (size_t i = 0; i < sizeof (arr) / sizeof (arr)[0]; i++) \
printf(" %0*llX", (int)(sizeof (arr)[0] * 2), \
(unsigned long long)(arr)[i] \
& ((1ULL << (8 * sizeof (arr)[0])) - 1)); \
putchar('\n'); \
} while (0)
int main(void)
{
puts("[character constants] the same 'A' written five ways");
printf(" %-14s type size %zu, value %d\n", "'A'", sizeof('A'), 'A');
printf(" %-14s type size %zu, value %ld\n", "L'A'", sizeof(L'A'), (long)L'A');
printf(" %-14s type size %zu, value %u\n", "u'A'", sizeof(u'A'), (unsigned)u'A');
printf(" %-14s type size %zu, value %u\n", "U'A'", sizeof(U'A'), (unsigned)U'A');
printf(" %-14s type size %zu, value %u\n", "u8'A'", sizeof(u8'A'), (unsigned)u8'A');
puts(" ('A' is an int - in C a character constant is not a char)");
puts("\n[strings] the single character \"한\" written five ways");
static const char s_plain[] = "한";
static const char8_t s_u8[] = u8"한";
static const char16_t s_u16[] = u"한";
static const char32_t s_u32[] = U"한";
static const wchar_t s_wide[] = L"한";
DUMP("char", s_plain);
DUMP("char8_t", s_u8);
DUMP("char16_t", s_u16);
DUMP("char32_t", s_u32);
DUMP("wchar_t", s_wide);
puts("\n[a character outside the BMP] U+1F600 (a smiling face)");
static const char e_plain[] = "\U0001F600";
static const char16_t e_u16[] = u"\U0001F600";
static const char32_t e_u32[] = U"\U0001F600";
static const wchar_t e_wide[] = L"\U0001F600";
DUMP("char", e_plain);
DUMP("char16_t", e_u16); /* ← 두 요소가 된다: 서러게이트 쌍 */
DUMP("char32_t", e_u32);
DUMP("wchar_t", e_wide);
puts(" only char16_t needs two elements - it does not fit in 16 bits, so it splits into a pair.");
puts("\n[what the implementation declares]");
#ifdef __STDC_ISO_10646__
printf(" __STDC_ISO_10646__ = %ldL - wchar_t values are Unicode code points\n",
(long)__STDC_ISO_10646__);
#else
puts(" __STDC_ISO_10646__ absent - the wchar_t encoding is up to the implementation (as on Windows)");
#endif
#ifdef __STDC_UTF_16__
puts(" __STDC_UTF_16__ = 1 - char16_t is UTF-16");
#endif
#ifdef __STDC_UTF_32__
puts(" __STDC_UTF_32__ = 1 - char32_t is UTF-32");
#endif
printf(" sizeof(wchar_t) = %zu, WCHAR_MIN = %ld, WCHAR_MAX = %lld\n",
sizeof(wchar_t), (long)WCHAR_MIN, (long long)WCHAR_MAX);
return 0;
}
실행 결과
[character constants] the same 'A' written five ways
'A' type size 4, value 65
L'A' type size 4, value 65
u'A' type size 2, value 65
U'A' type size 4, value 65
u8'A' type size 1, value 65
('A' is an int - in C a character constant is not a char)
[strings] the single character "한" written five ways
char 1 elements of 4 bytes: ED 95 9C 00
char8_t 1 elements of 4 bytes: ED 95 9C 00
char16_t 2 elements of 2 bytes: D55C 0000
char32_t 4 elements of 2 bytes: 0000D55C 00000000
wchar_t 4 elements of 2 bytes: 0000D55C 00000000
[a character outside the BMP] U+1F600 (a smiling face)
char 1 elements of 5 bytes: F0 9F 98 80 00
char16_t 2 elements of 3 bytes: D83D DE00 0000
char32_t 4 elements of 2 bytes: 0001F600 00000000
wchar_t 4 elements of 2 bytes: 0001F600 00000000
only char16_t needs two elements - it does not fit in 16 bits, so it splits into a pair.
[what the implementation declares]
__STDC_ISO_10646__ = 201706L - wchar_t values are Unicode code points
__STDC_UTF_16__ = 1 - char16_t is UTF-16
__STDC_UTF_32__ = 1 - char32_t is UTF-32
sizeof(wchar_t) = 4, WCHAR_MIN = -2147483648, WCHAR_MAX = 2147483647
시연의 마지막 부분이 이 기계의 답이다 — __STDC_ISO_10646__이 정의되어 있고, wchar_t는 4바이트다. Windows에서는 이 매크로가 정의되지 않는다. 2바이트로는 유니코드의 모든 문자를 하나의 값으로 담을 수 없어 조건을 만족하지 못하기 때문이다. 이 한 줄이 71장 전체의 씨앗이다.
70.2 다섯 가지 문자 상수와 문자열
C23에는 문자를 적는 방법이 다섯 가지다. 각각 타입과 인코딩이 다르다.
| 표기 | 상수의 타입 | 문자열 요소 | 인코딩 |
|---|---|---|---|
'A' | int | char | 실행 문자집합(대개 UTF-8 바이트) |
u8'A' / u8"…" | unsigned char | char8_t | UTF-8 |
u'A' / u"…" | char16_t | char16_t | UTF-16 |
U'A' / U"…" | char32_t | char32_t | UTF-32 |
L'A' / L"…" | wchar_t | wchar_t | 구현이 정한다 |
표 70.3
시연의 첫 부분이 이 표를 실물로 보인다. 눈에 띄는 것이 셋이다.
첫째, 'A'의 타입은 char가 아니라 int다. C에서 문자 상수는 int이고, 그래서 sizeof('A')가 4다(C++와 다른 자리다).
둘째, u8 접두사는 C23에서 문자 상수에도 붙는다. 다만 한 UTF-8 코드 단위로 표현되는 문자, 즉 아스키 범위만 쓸 수 있다.
셋째, 같은 “한”이 표기마다 전혀 다른 바이트가 된다. char로는 ED 95 9C 세 바이트, char16_t로는 D55C 한 단위, char32_t와 wchar_t로는 0000D55C 한 단위다.
그리고 BMP 밖의 문자에서 결정적인 차이가 드러난다. U+1F600을 담을 때 char16_t만 요소가 둘이 된다 — D83D DE00. 16비트에 담기지 않아 쌍으로 쪼갠 것이고, 그 쌍의 이름이 서러게이트다(71장).
문. 소스 코드에 "한"이라고 적으면 어떤 바이트가 되는가?
답. 소스 파일의 인코딩과 실행 문자집합이라는 두 단계를 거친다(17장의 인코딩 사슬). 컴파일러는 소스 파일을 어떤 인코딩으로 읽을지 정하고(GCC는 기본 UTF-8, -finput-charset으로 바꾼다), 그것을 실행 문자집합으로 옮겨 문자열 리터럴의 바이트를 만든다(-fexec-charset).
이 둘이 어긋나면 컴파일은 되는데 문자열만 깨진다. 그래서 이식성이 중요한 코드는 비아스키 문자를 소스에 직접 적는 대신 유니버설 문자 이름을 쓴다.
const char *s = "한"; /* U+D55C 를 실행 문자집합으로 */
const char32_t *t = U"\U0001F600";\u는 네 자리, \U는 여덟 자리다. 시연도 이모지를 이 표기로 적었다 — 그래야 이 책의 예제가 어느 편집기·어느 빌드 설정에서도 같은 바이트를 만든다.
70.3 MB_CUR_MAX와 MB_LEN_MAX
이름이 닮은 두 매크로가 전혀 다른 것을 뜻한다.
| 매크로 | 헤더 | 무엇 |
|---|---|---|
MB_CUR_MAX | <stdlib.h> | 지금 로케일에서 한 글자의 최대 바이트 수 — 실행 중에 바뀐다 |
MB_LEN_MAX | <limits.h> | 지원하는 어느 로케일에서든 넘지 않는 상한 — 컴파일 타임 상수 |
표 70.4
68장 시연에서 MB_CUR_MAX가 로케일에 따라 1(C)·2(EUC-KR)·6(UTF-8)으로 바뀌는 것을 보았다. 버퍼를 잡을 때 어느 쪽을 써야 하는지가 여기서 갈린다 — 배열 크기는 MB_LEN_MAX로 잡고, 루프의 판단은 MB_CUR_MAX로 한다.
문. UTF-8은 최대 4바이트인데 왜 MB_CUR_MAX가 6인가?
답. 역사의 흔적이다. UTF-8의 원래 정의(RFC 2279)는 최대 6바이트까지 허용했고, 31비트 코드까지 적을 수 있었다. 유니코드가 U+10FFFF에서 멈추기로 하면서 RFC 3629가 4바이트로 잘라 냈지만, glibc의 MB_CUR_MAX는 여유 있는 옛 값을 그대로 두고 있다.
실무의 결론은 바뀌지 않는다 — 버퍼는 MB_CUR_MAX(또는 MB_LEN_MAX)만큼 잡는다. 실제로 4바이트를 넘지 않는다고 해서 4로 잡으면, 다른 인코딩의 로케일에서 넘칠 수 있다.
70.4 변환 함수의 세 층
바이트열과 와이드 문자 사이를 오가는 함수가 여럿인데, 세 층으로 정리된다.
| 층 | 함수 | 성격 |
|---|---|---|
| 옛 층(C89) | mbtowc·wctomb·mbstowcs·wcstombs | 변환 상태를 함수 내부에 숨겨 둔다 — 재진입 불가 |
| 상태를 드러낸 층(C95) | mbrtowc·wcrtomb·mbsrtowcs·wcsrtombs | mbstate_t *를 인자로 받는다. r은 restartable |
| 유니코드 전용 층(C11) | mbrtoc16·c16rtomb·mbrtoc32·c32rtomb | <uchar.h>. 로케일의 인코딩과 UTF-16/32 사이 |
표 70.5
새로 쓰는 코드는 가운데 층을 쓴다. 이름의 r이 「재시작 가능」을 뜻하고, 그 재시작을 가능하게 하는 것이 mbstate_t다.
70.4.1 mbstate_t가 왜 필요한가
변환에 상태가 왜 필요한가. 두 가지 사정 때문이다.
첫째, 글자가 잘려서 온다. 네트워크나 파일에서 읽으면 버퍼 경계가 글자 가운데를 지날 수 있다. “한”의 세 바이트 중 둘만 들어온 상태에서 함수는 “틀렸다”가 아니라 “아직 모자라다”라고 답해야 하고, 다음 조각이 왔을 때 이어서 읽어야 한다.
둘째, 상태가 있는 인코딩이 있다. ISO-2022-JP 같은 인코딩은 이스케이프 시퀀스로 「지금부터 한자」·「지금부터 아스키」를 전환한다. 지금 어느 모드인지를 기억하지 않으면 같은 바이트가 다른 글자가 된다.
mbstate_t는 그 두 가지를 담는 불투명한 객체다. 규칙은 셋이다 — 0으로 채우면 초기 상태이고, 변환 하나마다 자기 객체를 쓰며, 속을 들여다보지 않는다.
mbstate_t st;
memset(&st, 0, sizeof st); /* 초기 상태로 */70.5 mbrtowc — 한 단계씩 뜯어보기
이 장의 핵심 함수다. 계약이 촘촘한 만큼, 반환값을 정확히 읽는 것이 전부다.
size_t mbrtowc(wchar_t * restrict pwc, const char * restrict s,
size_t n, mbstate_t * restrict ps);| 반환값 | 뜻 |
|---|---|
0 | 널 문자를 완성했다(변환 결과가 널 와이드 문자) |
1~n | 이만큼의 바이트를 소비해 한 글자를 완성했다 |
(size_t)-2 | n바이트를 전부 보았지만 아직 글자가 완성되지 않았다 — 상태는 갱신된다 |
(size_t)-1 | 부정한 시퀀스다. errno에 EILSEQ가 담기고 상태는 미정 |
표 70.6
examples/ch70/mbrtowc_step.c
/* mbrtowc — 바이트열이 한 글자씩 wchar_t 로 풀리는 과정을 한 단계씩. */
#include <errno.h>
#include <locale.h>
#include <stdlib.h> /* MB_CUR_MAX */
#include <stdio.h>
#include <string.h>
#include <wchar.h>
/* 반환값의 네 갈래를 사람 말로 옮긴다 */
static const char *explain(size_t r)
{
if (r == 0) return "hit the null character (returns 0)";
if (r == (size_t)-1) return "invalid sequence (errno=EILSEQ)";
if (r == (size_t)-2) return "not a whole character yet - more bytes needed";
return "consumed this many bytes and produced one character";
}
static void walk(const char *label, const char *s, size_t len)
{
mbstate_t st;
memset(&st, 0, sizeof st); /* 초기 변환 상태 */
printf("\n[%s] %zu bytes:", label, len);
for (size_t i = 0; i < len; i++) printf(" %02X", (unsigned char)s[i]);
puts("");
size_t pos = 0;
while (pos < len) {
wchar_t wc = 0;
errno = 0;
size_t r = mbrtowc(&wc, s + pos, len - pos, &st);
printf(" at %zu: mbrtowc -> ", pos);
if (r == (size_t)-1) printf("(size_t)-1 ");
else if (r == (size_t)-2) printf("(size_t)-2 ");
else printf("%-12zu", r);
printf("%s", explain(r));
if (r != (size_t)-1 && r != (size_t)-2)
printf(" → U+%04lX", (unsigned long)wc);
puts("");
if (r == (size_t)-1 || r == (size_t)-2) break;
pos += (r == 0) ? 1 : r;
}
}
int main(void)
{
/* 이 예제는 UTF-8 로케일이 필요하다 — 없으면 그 사실을 밝히고 끝낸다 */
const char *loc = setlocale(LC_CTYPE, "C.UTF-8");
if (!loc) loc = setlocale(LC_CTYPE, "en_US.UTF-8");
if (!loc) { puts("no UTF-8 locale - skipping this demonstration"); return 0; }
printf("LC_CTYPE=%s, MB_CUR_MAX=%zu\n", loc, (size_t)MB_CUR_MAX);
walk("two ASCII characters", "Hi", 2);
walk("one Hangul syllable", "한", 3);
walk("an emoji outside the BMP", "\xF0\x9F\x98\x80", 4);
/* 잘린 문자: 세 바이트 중 둘만 준다 → (size_t)-2 */
walk("a character cut in half", "\xED\x95", 2);
/* 부정한 시퀀스: 후행 바이트로 시작 → (size_t)-1 */
walk("an invalid sequence", "\x9C\x41", 2);
/* 상태는 이어진다: 잘린 조각을 두 번에 나눠 넣어도 이어 붙는다 */
puts("\n[when it arrives in pieces, as from a stream] feeding \"한\" as 2+1 bytes");
mbstate_t st;
memset(&st, 0, sizeof st);
wchar_t wc = 0;
size_t r1 = mbrtowc(&wc, "\xED\x95", 2, &st);
printf(" first pass (2 bytes): %s\n", explain(r1));
size_t r2 = mbrtowc(&wc, "\x9C", 1, &st);
printf(" second pass (1 byte): %s -> U+%04lX\n", explain(r2), (unsigned long)wc);
puts(" mbstate_t remembers how far it has seen, so the pieces join up.");
return 0;
}
실행 결과
LC_CTYPE=C.UTF-8, MB_CUR_MAX=6
[two ASCII characters] 2 bytes: 48 69
at 0: mbrtowc -> 1 consumed this many bytes and produced one character → U+0048
at 1: mbrtowc -> 1 consumed this many bytes and produced one character → U+0069
[one Hangul syllable] 3 bytes: ED 95 9C
at 0: mbrtowc -> 3 consumed this many bytes and produced one character → U+D55C
[an emoji outside the BMP] 4 bytes: F0 9F 98 80
at 0: mbrtowc -> 4 consumed this many bytes and produced one character → U+1F600
[a character cut in half] 2 bytes: ED 95
at 0: mbrtowc -> (size_t)-2 not a whole character yet - more bytes needed
[an invalid sequence] 2 bytes: 9C 41
at 0: mbrtowc -> (size_t)-1 invalid sequence (errno=EILSEQ)
[when it arrives in pieces, as from a stream] feeding "한" as 2+1 bytes
first pass (2 bytes): not a whole character yet - more bytes needed
second pass (1 byte): consumed this many bytes and produced one character -> U+D55C
mbstate_t remembers how far it has seen, so the pieces join up.
시연이 이 네 갈래를 차례로 보인다. 아스키는 1바이트씩, 한글은 3바이트를 한 번에, BMP 밖 이모지는 4바이트를 한 번에 먹는다. 잘린 조각에는 (size_t)-2, 후행 바이트로 시작하는 조각에는 (size_t)-1이 나온다.
마지막 시연이 mbstate_t의 존재 이유를 그대로 보여 준다. “한”의 세 바이트를 2+1로 나눠 넣었더니, 첫 호출은 (size_t)-2를 돌려주고 둘째 호출에서 U+D55C가 완성됐다. 상태 객체가 「두 바이트까지 봤다」를 들고 있었기 때문이다.
흔한 오해. “(size_t)-2는 오류다”
아니다. 정상적인 중간 상태다. 오류는 (size_t)-1 하나뿐이다.
이 둘을 뭉뚱그려 처리하면 스트림 처리기가 조각난 입력에서 멀쩡한 글자를 버리게 된다. 네트워크에서 4096바이트씩 읽는 코드라면 거의 매 버퍼의 끝에서 (size_t)-2를 만난다 — 그때 해야 할 일은 남은 바이트를 다음 버퍼 앞에 붙여 이어 읽는 것이지, 오류로 끊는 것이 아니다.
두 값이 모두 (size_t)의 큰 수라는 점도 함정이다. int에 받아 < 0으로 견주면 구현에 따라 엉뚱해진다. 반드시 size_t로 받아 (size_t)-1, (size_t)-2와 직접 견준다.
70.5.1 문자열 통째로 — mbsrtowcs
한 글자씩이 아니라 문자열 전체를 옮길 때는 mbsrtowcs를 쓴다. 계약의 요령이 둘 있다.
mbstate_t st;
memset(&st, 0, sizeof st);
const char *p = utf8; /* 진행 위치를 담는 포인터 */
size_t need = mbsrtowcs(NULL, &p, 0, &st); /* ① 필요한 길이를 먼저 묻는다 */
if (need == (size_t)-1) { /* EILSEQ */ }
wchar_t *buf = malloc((need + 1) * sizeof *buf);
p = utf8; /* ② 포인터와 상태를 되돌리고 */
memset(&st, 0, sizeof st);
mbsrtowcs(buf, &p, need + 1, &st); /* ③ 실제로 옮긴다 */첫 인자를 널로 주면 길이만 센다 — 그래서 버퍼 크기를 미리 알 수 있다. 그리고 s가 const char *인 이유는, 변환이 도중에 멈췄을 때 어디까지 읽었는지를 호출자에게 돌려주기 위해서다. 옛 mbstowcs에는 이 기능이 없어 스트림 처리에 쓸 수 없었다.
플랫폼 노트. 변환은 로케일에 매여 있다
여기서 놓치기 쉬운 사실 하나. mbrtowc가 UTF-8을 이해하는 것은 지금 로케일이 UTF-8이기 때문이다. LC_CTYPE이 "C"이면 같은 바이트가 부정한 시퀀스가 된다.
그래서 와이드 문자를 쓰는 프로그램은 시작하자마자 setlocale(LC_ALL, "")을 불러야 하고, 그 로케일이 UTF-8이 아닐 수도 있다는 사실을 감당해야 한다. 시연이 UTF-8 로케일을 명시적으로 찾고, 없으면 건너뛰는 이유가 이것이다.
「이 함수들은 인코딩을 고를 수 없다」는 제약이 실무에서 와이드 문자를 멀리하게 만든 큰 이유다. 프로그램이 다루려는 인코딩과 사용자의 로케일이 같아야 한다는 전제는 서버 프로그램에서 특히 지키기 어렵다.
복습 정리
| 기억할 것 | 요점 |
|---|---|
wchar_t | 정수 타입. 크기도 인코딩도 표준이 정하지 않는다 |
| 유니코드 보장 | __STDC_ISO_10646__이 정의되어야 코드포인트다 |
| 문자 상수 | 'A'는 int. u8/u/U/L 네 접두사 |
| BMP 밖 | char16_t만 요소가 둘이 된다(서러게이트) |
MB_CUR_MAX | 로케일에 따라 바뀐다. 상한은 MB_LEN_MAX |
| 변환 함수 | r 붙은 것(mbrtowc)을 쓴다 |
mbstate_t | 0으로 채워 시작. 잘린 글자와 상태 있는 인코딩 때문 |
| 반환값 | -2는 오류가 아니라 “더 필요” |
| 전제 | LC_CTYPE이 그 인코딩이어야 한다 |
표 70.7
wchar_t의 계약과 변환의 과정을 보았다. 다음 장은 그 계약이 실제 플랫폼에서 어떻게 갈라졌는지다 — Windows의 2바이트, 리눅스의 4바이트, 그리고 UTF-16이 유니코드를 다 담지 못해 생긴 일들.