사전 모지바케
문제

모지바케

gabury1고친 사람 github-actions[bot]

모지바케는 멀쩡한 글자가 엉뚱한 기호로 깨져 보이는 고장입니다. 저장할 때 쓴 규칙과 읽을 때 쓴 규칙이 서로 어긋난 탓입니다. 「안녕」이 「안녕」으로 보이는 것이 흔한 모습입니다. 저장된 데이터는 대개 멀쩡합니다. 규칙을 맞춰 다시 읽는 것만으로 되살아나기도 합니다.

쉽고 빠른 이해

모지바케는 글자가 뜻 없는 기호 더미로 깨져 나오는 고장입니다. 한글로 적은 이름이 「안녕」 같은 낯선 글자로 보이는 것이 그 예입니다.

이 현상에 따로 이름이 붙은 까닭은 망가진 것처럼 보여도 대개 안 망가졌기 때문입니다. 저장된 값은 멀쩡합니다. 읽는 쪽이 규칙을 잘못 골랐을 뿐입니다.

이걸 모르면 멀쩡한 데이터를 버립니다. 깨진 글자를 다시 저장해 진짜로 망가뜨리기도 합니다.

어떻게 도나:

  1. 쓰는 쪽이 글자를 한 규칙으로 바꿔 저장하거나 보냅니다
  2. 저장된 값에는 어느 규칙을 썼는지가 따라가지 않습니다
  3. 읽는 쪽이 다른 규칙으로 그 값을 글자로 되돌립니다

무엇이 나빠지나 — 깨진 글자를 한 번 더 저장하면 되돌리기가 어려워집니다. 읽다가 해석 못 한 부분이 「�」로 바뀐 채 저장됐다면 원래 값은 이미 사라진 것입니다.

막는 법 — 데이터가 프로그램 사이를 넘어갈 때마다 규칙 이름을 빠짐없이 적습니다.

상세

이 절은 「안녕」 두 글자를 따라가 봅니다. 모지바케가 왜 생기는지, 어떤 조건에서 다시 만들어지는지를 보입니다. 그다음 깨진 모양으로 원인을 짚는 법과, 되돌릴 수 있는 경우와 없는 경우를 가릅니다.

모지바케는 일본어 「文字化け」를 소리 나는 대로 옮긴 말입니다. 글자(文字)가 둔갑한다(化け)는 뜻입니다. 한국 실무에서는 「글자 깨짐」이나 「한글 깨짐」이라고도 부릅니다.

글자와 바이트 사이의 번역 규칙

바이트는 컴퓨터가 저장하고 주고받는 가장 작은 단위입니다. 값 하나가 0부터 255 사이의 수입니다. 파일도 네트워크 메시지도 결국 바이트를 늘어놓은 줄입니다.

이 줄 안에는 글자가 따로 표시되어 있지 않습니다. 그래서 글자를 바이트로 바꾸는 규칙이 필요합니다. 이 규칙을 문자 인코딩이라고 부릅니다. 글자를 바이트로 바꾸는 일을 인코딩, 바이트를 글자로 되돌리는 일을 디코딩이라고 합니다.

규칙은 하나가 아닙니다. 이 편에서는 넷이 자주 나옵니다. 오늘날 가장 널리 쓰는 규칙은 UTF-8(Unicode Transformation Format 8-bit)입니다. 한국어용 규칙으로는 EUC-KR(Extended Unix Code for Korean)이 오래 쓰였습니다.

CP949(Code Page 949)는 EUC-KR 을 넓힌 규칙입니다. windows-1252는 서유럽 글자용 규칙입니다. 이 규칙은 바이트 하나에 글자 하나를 매겨 두었습니다.

규칙 담는 글자 영문 한 글자 한글 한 글자
UTF-8 세계 글자를 모은 유니코드의 모든 글자 1바이트 3바이트
EUC-KR 영문과 한국어 1바이트 2바이트
CP949 EUC-KR 이 못 담는 한글 음절까지 1바이트 2바이트
windows-1252 영문과 서유럽 글자 1바이트 담지 못한다

영문 칸이 전부 1바이트인 데는 까닭이 있습니다. 네 규칙 모두 영문과 숫자를 ASCII(American Standard Code for Information Interchange)와 똑같은 값으로 적습니다. 이 공통 구간이 뒤에서 조건 하나가 됩니다.

규칙 이름이 없는 바이트

바이트 줄은 자기가 어느 규칙으로 적혔는지 말해 주지 않습니다. ec 95 88 세 바이트는 UTF-8 로 읽으면 「안」 한 글자입니다. windows-1252 로 읽으면 바이트마다 글자 하나씩, 「안」 세 글자입니다.

둘 다 그 규칙으로는 문제없는 해석입니다. 그래서 읽는 프로그램은 오류를 내지 않고 깨진 글자를 태연히 내보냅니다.

flowchart TD
    subgraph 쓰는쪽["쓰는 쪽"]
        A["「안녕」"]
    end
    A -->|"UTF-8 로 인코딩"| B["바이트 ec 95 88 eb 85 95"]
    subgraph 읽는쪽["읽는 쪽"]
        C["「안녕」"]
        D["「안녕」"]
    end
    B -->|"UTF-8 로 디코딩"| C
    B -->|"windows-1252 로 디코딩"| D

가운데 바이트는 한 번도 바뀌지 않았습니다. 갈린 것은 읽는 쪽이 고른 규칙 하나입니다.

그래서 규칙 이름은 바이트 바깥에서 따로 전해야 합니다. 이름이 빠지거나 틀리게 전해지면 읽는 쪽은 짐작하거나 자기 기본값을 씁니다. 모지바케는 그 짐작이 빗나간 결과입니다.

재현되는 조건

넷이 다 서면 모지바케가 납니다.

  1. 글자를 한 규칙으로 인코딩해 저장하거나 보냅니다
  2. 규칙 이름이 바이트와 함께 전해지지 않거나, 틀린 이름이 전해집니다
  3. 읽는 쪽이 쓴 쪽과 다른 규칙으로 디코딩합니다
  4. 그 바이트 값에 두 규칙이 서로 다른 글자를 매겨 두었습니다

넷째 조건 때문에 영문과 숫자만 든 글은 안 깨집니다. 네 규칙이 그 구간을 똑같이 적기 때문입니다. 그래서 모지바케는 시험 데이터에서는 안 보이다가 한글 이름이 든 첫 데이터에서 드러나곤 합니다.

코드로 다시 만들기

앞의 그림을 파이썬으로 옮겨 조건 셋째를 직접 세워 봅니다. encode 는 글자를 바이트로, decode 는 바이트를 글자로 바꾸는 함수입니다. 괄호 안이 쓸 규칙의 이름입니다. cp1252 는 windows-1252 를 파이썬에서 부르는 이름입니다.

Python
raw = "안녕".encode("utf-8")
raw.hex(" ")          # 'ec 95 88 eb 85 95'
raw.decode("cp1252")  # '안녕'
raw.decode("utf-8")   # '안녕'

셋째 줄이 조건 셋째입니다. UTF-8 로 쓴 바이트를 windows-1252 로 읽었습니다. 같은 raw 를 넷째 줄처럼 쓴 규칙으로 읽으면 멀쩡히 돌아옵니다.

깨진 모양으로 원인 짚기

어느 규칙끼리 어긋났는지에 따라 깨진 모양이 다릅니다. 모양을 보면 쓴 규칙과 읽은 규칙을 대략 짐작할 수 있습니다.

모양을 읽으려면 「�」 하나를 먼저 알아야 합니다. 이 글자는 대체 문자입니다. 디코딩하던 규칙이 해석할 수 없는 바이트를 만나면 그 바이트 대신 이 글자를 넣습니다. 오류로 멈추지 않고 읽기를 이어 가려는 장치입니다.

보이는 모양 특징 쓴 규칙 → 읽은 규칙
「안녕」 라틴 문자와 기호가 섞인다 UTF-8 → windows-1252
「¾È³ç」 라틴 문자와 기호가 섞인다 EUC-KR → windows-1252
「�븞�뀞」 드문 한글 음절 사이에 「�」가 낀다 UTF-8 → CP949
「�ȳ�」 「�」 사이에 라틴 문자가 끼기도 한다 EUC-KR → UTF-8

원래 글은 넷 다 「안녕」입니다. 「�」 없이 라틴 문자와 기호만 섞이면 서유럽 규칙이 읽은 것입니다.

「�」가 보이면 읽은 규칙이 바이트 일부를 해석하지 못한 것입니다. 해석된 나머지 바이트가 우연히 엉뚱한 글자가 되기도 합니다. 넷째 줄의 「ȳ」가 그렇게 끼어든 라틴 문자입니다.

대체 문자가 한 번 더 깨질 때

한국 웹에서 오래 보인 「占쏙옙」은 모지바케를 두 번 거친 모양입니다. 한 번 깨진 글이 그 모양대로 저장됐습니다. 저장된 글이 다시 틀린 규칙으로 읽혔습니다.

flowchart TD
    A["「가」"] -->|"EUC-KR 로 인코딩"| B["바이트 b0 a1"]
    B -->|"UTF-8 로 디코딩"| C["「��」 대체 문자 둘"]
    C -->|"UTF-8 로 인코딩해 저장"| D["바이트 ef bf bd ef bf bd"]
    D -->|"CP949 로 디코딩"| E["「占쏙옙」"]

둘째 화살표가 갈림길입니다. b0 a1 은 UTF-8 로는 해석할 수 없는 바이트라서 대체 문자 둘로 바뀝니다. 이때 원래 바이트는 사라집니다.

대체 문자 하나는 UTF-8 로 ef bf bd 세 바이트입니다. CP949 는 이 여섯 바이트를 두 바이트씩 묶어 한자 하나와 한글 둘로 읽습니다. 원래 글자가 무엇이었든 대체 문자 둘이면 같은 「占쏙옙」이 나옵니다. 이 모양은 원래 글에 대해 아무것도 알려 주지 않습니다.

되돌릴 수 있는 경우와 없는 경우

깨진 글을 봤을 때 먼저 물을 것은 원래 바이트가 아직 남아 있느냐입니다. 남아 있으면 규칙만 바꿔 다시 읽으면 됩니다.

서유럽 1바이트 규칙으로 읽은 경우는 대개 되돌아옵니다. 이런 규칙은 바이트 값 하나에 글자 하나를 매겨 두었습니다. 깨진 글자를 같은 규칙으로 다시 인코딩하면 원래 바이트가 나옵니다.

Python
bad = "안녕"
back = bad.encode("cp1252")  # 처음 6바이트
back.decode("utf-8")         # '안녕'

대체 문자로 바뀐 경우는 못 되돌립니다. 해석 못 한 바이트가 무엇이었든 같은 「�」 하나로 바뀌기 때문입니다. 인코딩할 규칙에 그 글자가 없어서 「?」로 바꿔 적은 경우도 같습니다.

깨진 글자를 UTF-8 로 다시 저장하면 인코딩이 한 번 더 겹칩니다. 이렇게 인코딩이 두 번 겹친 상태를 이중 인코딩이라고 합니다. 「안녕」을 UTF-8 로 저장하면 여섯 바이트였던 「안녕」이 열다섯 바이트가 됩니다. 깨진 글자 여섯 개가 저마다 2~3바이트를 차지하기 때문입니다.

이중 인코딩은 겹친 순서를 거꾸로 하나씩 풀면 돌아옵니다. 풀기 전에 몇 번 겹쳤는지부터 알아내야 합니다. 앞 소절의 「占쏙옙」은 이와 다릅니다. 중간에 바이트가 「�」로 바뀌어 사라졌으므로 거꾸로 풀어도 돌아오지 않습니다.

백엔드에서 규칙이 어긋나는 경계

모지바케는 바이트가 한 프로그램에서 다른 프로그램으로 넘어가는 경계에서 납니다. 경계마다 규칙 이름을 전하는 통로가 따로 있습니다.

아래 표에 나오는 로케일은 운영체제가 언어와 지역에 맞춰 쓰는 기본 설정 묶음입니다. 기본 문자 인코딩도 이 묶음에 들어 있습니다.

바이트 순서 표시(BOM, Byte Order Mark)는 파일 맨 앞에 붙는 몇 바이트입니다. 이름을 적을 데가 없는 평문 파일에서 어느 규칙으로 적혔는지 알리는 표지로 쓰입니다. UTF-8 이면 ef bb bf 세 바이트입니다.

경계 규칙 이름을 전하는 통로 이름이 빠지면
HTTP(HyperText Transfer Protocol) 응답 Content-Type 헤더의 charset 받는 쪽이 짐작한다
HTTP 요청 본문 요청의 Content-Type 헤더 서버 프레임워크의 기본값으로 읽는다
데이터베이스 연결 연결의 문자 집합 설정 서버나 드라이버의 기본값을 쓴다
파일 읽기와 쓰기 파일을 열 때 넘기는 인코딩 이름 운영체제 로케일의 기본값을 쓴다
이름을 적을 데가 없는 평문 파일 맨 앞의 바이트 순서 표시 정도 여는 프로그램이 짐작한다
터미널과 로그 보기 터미널의 로케일 설정 터미널 기본값으로 그린다

셋째 칸이 공통으로 가리키는 것은 기본값입니다. 기본값은 기계와 설정마다 다를 수 있습니다. 개발자 노트북에서 멀쩡하던 코드가 서버에서 깨지는 흔한 까닭입니다.

경계가 여럿이면 어느 경계에서 깨졌는지부터 가려야 합니다. 화면에 보이는 글자만으로는 모릅니다. 저장된 바이트를 16진수로 찍어 보면 가려집니다.

저장된 「안녕」이 ec 95 88 로 시작하면 저장은 맞고 읽는 쪽이 틀린 것입니다. c3 ac 로 시작하면 이미 이중 인코딩된 채 저장된 것입니다. c3 ac 는 「ì」를 UTF-8 로 적은 두 바이트입니다.

물음표·빈 네모와의 차이

글자가 이상하게 보인다고 전부 모지바케는 아닙니다. 「?」와 빈 네모 「□」는 뿌리가 다릅니다. 아래 표는 네 모양을 무엇이 틀렸는지로 가릅니다. 표의 위 두 줄이 모지바케입니다.

보이는 것 무엇이 틀렸나 원래 바이트
엉뚱한 글자 읽는 규칙 남아 있다
엉뚱한 글자 사이의 「�」 읽는 규칙이 해석 못 한 바이트가 있다 「�」째로 저장했다면 사라졌다
「?」 쓰는 규칙에 그 글자가 없다 처음부터 안 적혔다
빈 네모 「□」 글꼴에 그 글자의 그림이 없다 멀쩡하다

「?」는 쓰는 쪽에서 생깁니다. 읽는 규칙을 바꿔도 돌아오지 않습니다.

빈 네모는 바이트도 규칙도 맞았습니다. 글꼴이 그 글자를 못 그렸을 뿐입니다. 그래서 인코딩을 고쳐도 안 없어집니다. 글꼴을 바꾸면 사라집니다.

막는 방법

고치는 길은 하나로 모입니다. 경계마다 규칙 이름을 빠짐없이 적는 것입니다.

  • 시스템 안에서는 규칙을 하나만 씁니다. 요즘은 대개 UTF-8 입니다
  • 들어온 바이트는 경계에서 바로 디코딩합니다. 나갈 글자는 경계 직전에 인코딩합니다
  • 규칙 이름을 기본값에 맡기지 않고 코드와 설정에 적습니다
  • 옛 시스템이 보내는 바이트는 규칙을 확인한 뒤 받습니다. 모르면 인코딩 추측 도구로 짐작합니다. 글이 짧을수록 그 짐작은 빗나가기 쉽습니다

관련 항목

모지바케가 어긋나게 짝짓는 문자 인코딩

문자 인코딩 · UTF-8 · UTF-16 · EUC-KR · CP949 · windows-1252 · latin-1 · Shift_JIS · ASCII

모지바케가 뒤섞는 글자와 바이트의 단위

바이트 · 코드 포인트 · 유니코드 · 문자 집합 · 글리프 · 글꼴 · 16진수

모지바케와 헷갈리는 글자 표시 고장

대체 문자 · 두부 문자 · 이중 인코딩 · 잘린 멀티바이트 문자 · 보이지 않는 문자 · 인코딩 불일치

모지바케를 막으려고 인코딩 이름을 전하는 수단

Content-Type · Content-Type charset · meta charset · 바이트 순서 표시 · 로케일 · IANA 문자 집합 레지스트리

모지바케를 알아채고 되돌리는 도구

인코딩 추측 · iconv · 16진 덤프 · ftfy

모지바케가 흔히 터지는 데이터 경계

HTTP · 데이터베이스 · 터미널 · CSV · 직렬화 · 파일 입출력

다른 이름: mojibake · 文字化け · 글자 깨짐 · 한글 깨짐