Windows의 문자 코드를 정리한다 - 왜 문자 깨짐이 일어나는가, 특히 Linux와 조합했을 때 무엇이 어긋나는가

· · Windows, 문자 깨짐, UTF-8, CP932, Linux, PowerShell, Unicode

Windows의 문자 깨짐은 일본어가 어렵기 때문에 일어나는 것이 아닙니다. 대부분은 같은 바이트 열을 다른 문자 코드로 읽었거나, 잘못 읽은 결과를 다른 문자 코드로 저장했기 때문에 일어납니다.

특히 Windows와 Linux를 가로지르면 Windows 쪽에는 CP932, UTF-8, UTF-16, console의 code page, PowerShell의 버전 차이 등 여러 문맥이 남고, Linux 쪽은 UTF-8 전제로 흐르는 경우가 많아서, 평소 보이지 않던 전제의 어긋남이 단번에 표면화됩니다.

이 이야기는 일본어 처리의 어려움이라기보다 bytes를 어떤 전제로 다루고 있는가를 맞출 수 있는가의 이야기입니다. 이 글에서는 Windows의 문자 코드 주변을 「왜 문자 깨짐이 일어나는가」라는 관점에서 정리하고, 특히 Linux와 조합했을 때 사고가 늘어나는 포인트를 실무 쪽으로 정리합니다.

1. 먼저 잡아 두고 싶은 것

먼저 요점만 쓰면 중요한 것은 다음 6점입니다.

  • 문자 깨짐은 「문자」의 문제가 아니라 「바이트 열을 어떻게 해석했는가」의 문제입니다.
  • Windows에는 Unicode 계와 legacy code page 계가 공존하고 있어, 1대 안에서도 문맥별로 전제가 다릅니다.
  • Linux 쪽은 UTF-8 전제가 강하므로 Windows 쪽의 CP932나 UTF-16이 섞이면 사고가 되기 쉽습니다.
  • 표시가 무너진 것뿐인 단계깨진 내용을 저장해 버린 단계는 나누어 생각해야 합니다.
  • 신규 텍스트는 UTF-8을 제1 후보로 하고, 기존의 legacy 파일은 명시적인 이행 태스크까지 현상 유지로 하는 것이 안전합니다.
  • 파일의 encoding, 에디터의 encoding, console의 code page, 앱 내부의 문자열 형식은 별개입니다. 여기를 혼동하면 조사가 길을 잃습니다.

「Windows에서 문자 깨짐이 있었다」는 말만으로는 원인을 특정할 수 없습니다. 적어도 다음 중 어느 것이 어긋나 있는지를 나눌 필요가 있습니다.

  • 파일 자체의 문자 코드
  • 저장 시의 문자 코드
  • 에디터의 해석
  • console의 input/output code page
  • 앱 내부의 문자열 형식
  • Linux 쪽의 locale과 상정 encoding

2. 문자 깨짐의 정체

문자 깨짐의 정체는 꽤 단순합니다.

  1. 문자열을 어딘가의 문자 코드로 encode해 바이트 열로 한다
  2. 그 바이트 열을 어딘가의 문자 코드로 decode해 문자열로 되돌린다
  3. encode와 decode의 전제가 일치하지 않으면 다른 문자열로 읽힌다

예를 들어 를 UTF-8로 저장하면 바이트 열은 다음입니다.

E3 81 82

이 바이트 열을 UTF-8로 읽으면 지만 CP932 쪽의 문맥으로 읽으면 縺� 같은 다른 문자열로 보입니다. 이것이 문자 깨짐입니다.

중요한 것은 여기서 일어나고 있는 것이 「일본어가 망가졌다」가 아니라 같은 bytes에 대한 해석이 어긋났을 뿐이라는 점입니다.

2.1 표시가 무너진 것뿐이라면 아직 되돌릴 수 있는 경우가 있다

문자 깨짐에는 아직 되찾을 수 있는 단계가 있습니다. 예를 들어 원래의 bytes가 바뀌지 않았다면 올바른 encoding으로 다시 열면 되돌릴 수 있는 경우가 있습니다.

반대로 위험한 것은 다음과 같은 흐름입니다.

  1. UTF-8의 파일을 CP932로 잘못 읽는다
  2. 화면상에서는 縺�처럼 보인다
  3. 그대로 「보이고 있는 문자열」을 저장한다
  4. 원래의 UTF-8 bytes가 사라진다

이 단계에 들어가면 단순한 표시 무너짐이 아니라 데이터 파손입니다.

2.2 더 위험한 것은 「표현할 수 없는 문자」를 좁은 code page로 떨어뜨릴 때

또 하나의 전형 사고는 Unicode 문자열을 CP932 같은 legacy code page로 떨어뜨릴 때입니다.

예를 들어 상대의 code page에 존재하지 않는 문자가 포함되어 있으면,

  • ?로 치환된다
  • 치환 문자 가 들어간다
  • 가까운 다른 문자로 변환된다
  • 변환 실패가 된다

같은 일이 일어납니다.

이 사고는 읽을 수 있다·없다뿐만 아니라 왕복 변환해서 원래대로 돌아가는가로 봐야 합니다. 한 번 잃은 문자는 나중에 올바른 encoding을 알아도 복원할 수 없습니다.

3. 왜 Windows에서는 까다로워지기 쉬운가

Windows가 까다로운 것은 단순히 오래되었기 때문이 아닙니다. Unicode의 세계와 legacy code page의 세계가, 지금도 동거하고 있기 때문입니다.

3.1 Windows API에는 Unicode 계와 code page 계가 공존한다

Windows API에는 크게 2계통이 있습니다.

  • W 계: wide character. Unicode를 UTF-16으로 다루는 계
  • A 계: ANSI라 불리는 code page 계

즉 Windows 안에는 처음부터 「Unicode로 다루는 길」과 「그 시점의 active code page로 다루는 길」이 양쪽 다 있습니다. 그래서 같은 Windows 상에서도 어느 API나 어느 도구를 거쳤는가로 전제가 바뀝니다.

3.2 「Windows의 일본어」는 1개가 아니다

Windows의 일본어 주변에서 실무상 자주 섞이는 것은 다음 4가지입니다.

  • CP932: 일본어 Windows의 legacy text에서 자주 나온다
  • UTF-8: 새로운 text 자산, web, cross-platform 계에서 늘어나고 있다
  • UTF-16LE: Windows 계 도구나 API의 문맥에서 지금도 평범하게 나온다
  • console의 code page: cmd.exe나 일부 console tool의 입출력에 효과가 있는 다른 레이어

여기서 중요한 것은 chcp 65001 했다고 해서 파일도 UTF-8이 된다는 것은 아니라는 점입니다. console의 code page를 바꾸는 것과 기존 파일의 bytes가 무엇인지는 다른 문제입니다.

덧붙여 일본어 Windows의 legacy text를 거칠게 「Shift_JIS」라고 부르는 경우가 많지만, 실무에서는 CP932라는 이름으로 의식해 두는 편이 대화가 흔들리기 어렵습니다. 적어도 「Windows 유래의 일본어 legacy encoding 이야기를 하고 있다」고 명시할 수 있습니다.

3.3 파일명과 파일 내용은 다른 문제

Windows에서 일본어 파일명이 평범하게 보이면 「그럼 내용도 괜찮을 것」이라고 생각하기 쉽습니다. 여기가 위험합니다.

  • path / file name을 다루는 층
  • 파일의 내용을 읽는 층
  • console에 표시하는 층

이 3가지는 별개입니다.

예를 들어 일본어 path는 문제없이 다룰 수 있어도, 파일의 내용은 CP932로 저장되어 있고 Linux 쪽에서 UTF-8로 읽으면 망가집니다. 반대로 파일의 내용이 UTF-8이어도, console의 code page가 맞지 않으면 표시만 무너집니다.

3.4 PowerShell이나 주변 도구의 기정값도 갖춰져 있지 않다

Windows에서 수수하게 사고를 늘리는 것이 같은 「텍스트를 썼다고 생각한」 것이어도 경로에 따라 출력 bytes가 다른 점입니다.

특히 주의해야 할 것은 다음입니다.

  • Windows PowerShell 5.1은 기정 encoding이 일관되지 않다
  • 일부 cmdlet이나 redirection은 UTF-16LE를 만든다
  • 다른 경로에서는 active ANSI code page가 쓰인다
  • PowerShell 7 이후는 UTF-8 no BOM이 기정이 되어 있다

즉 「PowerShell에서 낸 text」만으로는 encoding이 정해지지 않습니다. 어느 버전에서, 어느 cmdlet으로, 어느 쓰기 경로를 썼는가까지 봐야 합니다.

4. Linux와 조합했을 때의 전형 사고

Windows 단체에서는 왠지 돌아가고 있던 것이 Linux를 끼운 순간 망가지는 것은 드물지 않습니다. 이유는 단순해서 Linux 쪽에서는 UTF-8 전제가 강하기 때문입니다.

4.1 Windows에서 CP932 저장한 text를 Linux가 UTF-8로 읽는다

가장 흔한 사고입니다.

  • Windows의 legacy app이나 옛날 운영이 CP932로 CSV / TXT / log를 쓴다
  • Linux 쪽의 script나 tool은 locale에 따라 UTF-8 전제로 읽는다
  • 결과적으로 decode error, , 의미 불명의 문자열이 된다

이때 Linux 쪽의 tool이 나쁜 것이 아니라 받은 bytes에 encoding의 약속이 붙어 있지 않은 것이 근본 원인입니다.

4.2 Linux / VS Code에서 만든 UTF-8 no BOM을 Windows 쪽이 ANSI라고 간주한다

역방향의 사고도 있습니다.

  • Linux나 VS Code에서 UTF-8 no BOM의 script / config / text를 만든다
  • Windows PowerShell 5.1이나 legacy tool이 BOM 없는 파일을 ANSI 쪽의 code page라고 간주한다
  • 일본어나 non-ASCII를 포함한 행만 망가진다

이 사고는 UTF-8 자체가 나쁜 것이 아니라 BOM 없는 UTF-8을 올바르게 추정해 주지 않는 읽는 쪽이 섞여 있는 것이 원인입니다.

4.3 Windows 쪽이 UTF-16LE를 쓰고, Linux 쪽에서는 「텍스트답게 보이지 않는」다

이것도 꽤 있습니다.

  • Windows PowerShell 5.1의 일부 출력이나 legacy tool이 UTF-16LE를 쓴다
  • Linux 쪽의 text tool은 UTF-8의 1 byte stream을 상정하고 있다
  • 결과적으로 NUL byte가 대량으로 섞인 「바이너리 같은 text」가 된다

UTF-16LE 자체는 나쁘지 않습니다. 다만 Linux의 text processing tool에 그대로 흘리는 전제와는 맞물리지 않는 장면이 많습니다.

4.4 BOM의 유무로도 friction이 일어난다

BOM은 encoding 그 자체는 아니지만 실무에서는 꽤 효과가 있습니다.

  • Windows 쪽의 일부 tool은 BOM이 있으면 도움이 된다
  • Linux 쪽의 일부 tool은 BOM을 선두의 쓸데없는 bytes로 다룬다
  • 결과적으로 1열째나 1행째의 선두만 망가지거나, 보이지 않는 쓰레기가 붙거나, 비교 결과가 어긋난다

특히 UTF-8에서는 같은 UTF-8이어도 BOM 있음 / 없음으로 bytes는 별개입니다. 「UTF-8로 했다」만으로는 운영 규칙으로서 아직 절반밖에 정해지지 않았습니다.

4.5 console의 보이는 방식을 믿으면 헤맨다

Windows와 Linux를 가로지를 때 또 하나 위험한 것이 console입니다.

  • Windows console에는 input / output의 code page가 있다
  • Linux terminal 쪽은 UTF-8 locale 전제로 도는 경우가 많다
  • WSL, SSH, container, CI를 경유하면 표시 경로가 늘어난다

이 상태에서 「console에서는 읽혔으니까 파일도 괜찮다」, 「console에서는 무너졌으니까 파일이 망가져 있다」고 판단하면 빗나가기 쉽습니다. 보이고 있는 것이 망가졌는가, 저장되어 있는 bytes가 망가졌는가는 별도로 확인하는 편이 안전합니다.

4.6 전형 사고를 표로 하면 이렇게 된다

장면 실제의 bytes 읽는 쪽의 상정 전형 증상
Windows의 legacy app이 저장한 CSV CP932 Linux 쪽은 UTF-8 , decode error, 의미 불명의 일본어
Linux / VS Code에서 만든 파일 UTF-8 no BOM Windows PowerShell 5.1이 ANSI 취급 일본어 행만 망가진다
Windows PowerShell 5.1의 일부 출력 UTF-16LE 또는 ANSI Linux 쪽은 UTF-8 text를 기대 NUL byte 혼입, 바이너리 같은 동작
UTF-8 with BOM의 파일 UTF-8 + BOM Unix 계 tool은 plain UTF-8 전제 선두 열만 망가진다, 쓸데없는 문자가 붙는다
console 표시만 믿는다 파일과 console에서 다른 전제 조사자가 표시만으로 판단 원인 구분을 빗나간다

5. 문자 깨짐 조사는 이 4가지 질문으로 진행한다

문자 깨짐 조사에서 헷갈리면 다음 4가지 질문으로 돌아가는 것이 가장 빠릅니다.

5.1 원래의 bytes는 무엇인가

먼저 봐야 할 것은 「지금 이 파일이 몇 bytes인가」입니다. 겉보기가 아니라 bytes를 보는 의식이 필요합니다.

  • UTF-8인가
  • UTF-8 with BOM인가
  • CP932인가
  • UTF-16LE인가
  • 도중에 다시 저장되어 다른 것이 되어 있지 않은가

5.2 맨 처음 누가, 어느 전제로 썼는가

다음으로 「처음의 쓰는 쪽」을 특정합니다.

  • Windows의 legacy app인가
  • PowerShell 5.1인가 7인가
  • Linux의 script인가
  • VS Code인가
  • Excel 유래의 export인가
  • 뭔가의 middleware / batch / CI인가

여기가 애매한 채로는 encoding 추정이 운이 됩니다.

5.3 지금 누가, 어느 전제로 읽고 있는가

쓰는 쪽뿐만 아니라 읽는 쪽의 전제도 필요합니다.

  • editor가 auto-detect하고 있는가
  • PowerShell이 BOM을 보고 있는가
  • Linux 쪽이 locale에 따라 UTF-8 취급하고 있는가
  • library가 기정 encoding을 쓰고 있는가
  • 명시적으로 Encoding.UTF8이나 cp932를 지정하고 있는가

문자 깨짐은 거의 여기서 발생합니다.

5.4 잘못 읽은 내용이 이미 저장되었는가

마지막으로 피해가 표시만으로 멈춰 있는가를 확인합니다.

  • 아직 bytes는 원래대로인가
  • 망가져 보이는 내용을 누군가가 저장했는가
  • ?가 차분에 들어 있지 않은가
  • 전문이 다른 encoding으로 다시 쓰여 있지 않은가

이 4가지 질문이 채워지면 대체로 원인은 보입니다.

6. 사고를 줄이는 운영 규칙

여기서부터는 실무 쪽의 이야기입니다. Windows와 Linux를 가로지르는 안건에서는 다음 규칙을 먼저 정해 두면 꽤 사고가 줄어듭니다.

6.1 신규 파일은 UTF-8을 제1 후보로 한다

신규의 text 파일은 우선 UTF-8을 제1 후보로 하는 것이 무난합니다. 다만 여기서 멈춰서는 안 됩니다. BOM을 어떻게 할지도 포함해서 정할 필요가 있습니다.

추천하는 사고방식은 다음입니다.

  • Linux 쪽에서 읽는 일이 많은 text: UTF-8 no BOM을 기본으로 한다
  • Windows의 legacy tool이나 Windows PowerShell 5.1이 읽는 script: BOM 유무를 상대 사정으로 명시한다
  • UTF-16LE가 필요한 명확한 상대가 있다면 그 요건을 사양으로 쓴다

「UTF-8로 통일」이라고만 쓰면 나중에 BOM으로 다툽니다.

6.2 기존 legacy 파일은 명시적인 이행 태스크까지 유지한다

기존 파일이 CP932라면 일상의 기능 수정 김에 멋대로 UTF-8화하지 않는 편이 안전합니다.

안전한 것은 다음 운영입니다.

  • 기존 파일은 원래의 encoding / BOM / 개행을 유지한다
  • encoding 변경은 이행 태스크로 분리한다
  • 변환 대상, 영향 범위, 하류 consumer를 확인한 후 일괄 변환한다

문자 깨짐 사고의 대부분은 선의의 「김에 UTF-8화」에서 시작됩니다.

6.3 encoding을 interface의 일부로 다룬다

CSV, TXT, log, 설정 파일, 간이 protocol은 내용뿐만 아니라 encoding 자체가 interface입니다.

예를 들어 사양으로서 최소한 여기까지는 쓰고 싶습니다.

  • 이 파일은 UTF-8 / CP932 / UTF-16LE 중 어느 것인가
  • UTF-8의 경우 BOM은 붙는가
  • 개행은 LF / CRLF 중 어느 것인가
  • Linux / Windows 중 어느 것이 producer / consumer인가
  • 도중의 batch나 ETL가 다시 저장하지 않는가

「text로 건넨다」는 사양이 되어 있지 않습니다.

6.4 기정값을 신용하지 말고, 쓰기 시에는 명시한다

code상에서도 script상에서도 encoding은 명시하는 편이 안전합니다.

위험한 사고방식은 다음입니다.

  • 기정대로 저장한다
  • OS에 맞춰 아마 좋은 느낌이 될 것이다
  • console에서 읽혔으니까 파일도 괜찮을 것이다
  • auto-detect가 있으니까 괜찮다

기정값은 Windows / Linux, PowerShell 5.1 / 7, editor, runtime에서 평범하게 바뀝니다. 명시하지 않는 한, 우연히 돌아가고 있을 뿐이 되기 쉽습니다.

6.5 console과 파일을 나누어 확인한다

다음은 꽤 효과가 있는 규칙입니다.

  • console에서의 표시 확인
  • 파일을 다시 열어서의 확인

이 2가지를 나눕니다.

chcp나 terminal의 표시가 맞아도 저장 파일이 다른 encoding이라면 의미가 없습니다. 반대로 파일은 정상이어도 console의 표시 code page가 맞지 않으면 겉보기만 망가집니다.

6.6 Git은 encoding을 고쳐 주지 않는다

수수하지만 중요합니다.

Git은 기본적으로 bytes를 추적할 뿐입니다. 즉 망가진 bytes도 그대로 성실하게 이력에 넣습니다.

그래서,

  • 아무것도 바꾸지 않았는데 거대 차분이 나왔다
  • 일본어 행만 수수께끼 차분이 되었다
  • 선두 행만 바뀌었다
  • 개행과 encoding이 함께 바뀌었다

라는 때는 내용 변경보다 먼저 re-encoding의 사고를 의심하는 편이 좋습니다.

7. 최소한의 체크리스트

Windows와 Linux가 섞이는 안건에서 먼저 고정하고 싶은 체크리스트를 둡니다.

7.1 편집 전

  • 이 파일의 현재 encoding은 무엇인가
  • BOM은 있는가
  • 개행은 LF / CRLF 중 어느 것인가
  • 대표적인 일본어 행을 2~3개 메모했는가
  • Linux 쪽 / Windows 쪽 중 어느 쪽이 최종 consumer인지 알고 있는가

7.2 편집 중

  • 기정 encoding에 의존한 쓰기를 하고 있지 않은가
  • auto-detect에 맡겨서 save하고 있지 않은가
  • PowerShell이나 shell redirection의 경로를 거칠게 쓰고 있지 않은가
  • 「표시가 읽힌다」만으로 안심하고 있지 않은가

7.3 편집 후

  • 저장 후에 다시 열어 확인했는가
  • Linux 쪽에서도 Windows 쪽에서도 대표 행이 무너지지 않았는가
  • ?가 차분에 늘지 않았는가
  • 선두 행이나 선두 열만 망가지지 않았는가
  • BOM / 개행만의 대차분이 되지 않았는가

7.4 이행 태스크로서 해야 하는 것

  • CP932 → UTF-8의 일괄 변환
  • UTF-8 BOM policy의 통일
  • PowerShell 5.1 전제 script의 재고 조사
  • CI / container / WSL / SSH 경유의 text pass의 명문화
  • editor / formatter / batch의 저장 설정의 통일

8. 정리

Windows의 문자 코드 문제를 한마디로 말하면 Unicode의 세계와 legacy code page의 세계가, 지금도 동거하고 있는 것이 본질입니다.

그리고 Linux와 조합했을 때 사고가 늘어나는 것은 Linux 쪽이 UTF-8 전제로 흐르는 경우가 많고, Windows 쪽의 CP932나 UTF-16, console code page, PowerShell의 버전 차이가 단번에 표면에 나오기 때문입니다.

기억해 두고 싶은 것은 다음 5점입니다.

  • 문자 깨짐은 bytes의 해석 어긋남
  • 표시 무너짐과 데이터 파손은 별개
  • Windows에서는 파일 / editor / console / API의 층을 나누어 생각한다
  • Linux와 주고받는 text는 UTF-8을 제1 후보로 한다
  • 기존 legacy 파일의 변환은 통상 수정과 분리한다

「Windows에서 문자 깨짐이 있었다」를 그대로 다루면 이야기가 너무 넓습니다. 하지만,

  • 원래의 bytes는 무엇인가
  • 누가 어떻게 썼는가
  • 누가 어떻게 읽는가
  • 이미 저장됐는가

의 4가지 질문으로 자르면 꽤 정리할 수 있습니다.

문자 코드는 수수하지만 Windows와 Linux 사이에서는 I/O 계약 그 자체입니다. 여기를 애매하게 두지 않는 것이 가장 효과가 있는 대책입니다.

9. 참고 자료

Windows / Microsoft

  • [Code Pages - Win32 apps Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/intl/code-pages)
  • [Code Page Identifiers - Win32 apps Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/intl/code-page-identifiers)
  • [Unicode in the Windows API - Win32 apps Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/intl/unicode-in-the-windows-api)
  • [Console Code Pages - Windows Console Microsoft Learn](https://learn.microsoft.com/en-us/windows/console/console-code-pages)
  • [chcp Microsoft Learn](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/chcp)
  • [Use UTF-8 code pages in Windows apps Microsoft Learn](https://learn.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page)

PowerShell / VS Code

  • [about_Character_Encoding Microsoft Learn](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_character_encoding)
  • [Understanding file encoding in VS Code and PowerShell Microsoft Learn](https://learn.microsoft.com/en-us/powershell/scripting/dev-cross-plat/vscode/understanding-file-encoding)

GNU / Linux locale

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기