Skip to Content

10장. ???의 정체 — 모지바케는 왜 생기나

만화로 보는 요약 — 먼저 읽어보세요

만화로 보는 요약 — 먼저 읽어보세요

모지바케는 인코딩 표와 디코딩 표가 어긋날 때 생기며, 바이트가 남아 있으면 복구할 수 있지만 물음표나 교체 문자로 저장된 뒤에는 원본을 잃을 수 있다.

면접 실전 질문: ① 모지바케의 근본 원인은? ② 오해형과 실패형 깨짐의 차이는? ③ 원본 바이트가 왜 중요한가?


배경 — 아홉 장의 깨짐, 하나의 원인

1장부터 여기까지, 우리는 참 많이도 깨뜨렸어요. 봉화 셋이 “전면전”에서 “축제”로 뒤집혔고, é가 러시아에서 И가 됐고, 한글 은 8바이트 편법으로 겨우 연명했고, 일본어 는 빌드를 무너뜨렸죠. 시대도 나라도 다 다른데, 이 모든 사고에는 딱 하나의 공통 원인이 있습니다.

이번 장은 새로운 인코딩을 배우는 장이 아니에요. 지금까지 본 모든 깨짐을 한 문장으로 꿰뚫고, 여러분이 실무에서 ???를 만났을 때 범인을 역추적하는 법을 익히는 장입니다.

스토리 — 모지바케라는 이름

깨진 글자를 일본어로 모지바케(文字化け, もじばけ) 라고 불러요. 직역하면 “글자(文字)가 둔갑하다(化け)”. 글자가 유령처럼 다른 모습으로 바뀌어 버린다는 뜻이죠. 이 이름이 현상의 본질을 정확히 담고 있습니다. 글자가 사라진 게 아니라 둔갑한 거예요.

왜 둔갑할까요? 컴퓨터가 글자를 다루는 과정은 딱 두 단계입니다.

  • 인코딩(encode): 글자 → 바이트. 저장하거나 전송할 때. ("한"0xC7 0xD1)
  • 디코딩(decode): 바이트 → 글자. 읽거나 표시할 때. (0xC7 0xD1"한")

이 왕복이 성립하려면 조건이 하나 있어요. 인코딩할 때 쓴 약속표와 디코딩할 때 쓴 약속표가 같아야 합니다. 같으면 글자가 온전히 돌아와요.

b = "한글".encode("euc-kr") # EUC-KR로 저장 print(b.decode("euc-kr")) # 한글 — 같은 표로 읽으니 온전히 복구(round-trip 성공)

모지바케는 이 조건이 깨질 때, 즉 인코딩 표 ≠ 디코딩 표일 때 생깁니다. 1장에서 이미 봤던 “같은 신호, 다른 약속표” 바로 그거예요. 아홉 장을 돌아 다시 이 문장으로 온 겁니다.

핵심 — 두 가지 깨짐과 ???의 정체

“표가 다르다”는 하나의 원인이, 현실에선 두 가지 얼굴로 나타납니다. 이 둘을 구분하는 게 실무 디버깅의 핵심이에요.

① 오해형(둔갑) — 바이트는 멀쩡, 다른 글자로 읽힘.

바이트 자체는 그 표에서 유효한데, 하필 다른 표로 읽어서 엉뚱한 글자가 나오는 경우예요.

b = "한글".encode("utf-8") # UTF-8로 저장: b'\xed\x95\x9c\xea\xb8\x80' print(b.decode("cp1252")) # 한글 — 서유럽 표로 읽으니 외계어로 둔갑

위에선 한글한글으로 둔갑했지만, 똑같은 원리로 UTF-8로 쓴 이름 밸린을 Latin-1로 읽으면 여는 글의 그 밸린이 튀어나와요. 중요한 건, 이 경우 원본 바이트(b)는 손대지 않은 채 그대로라는 거예요. 해석만 틀렸을 뿐이라, 같은 바이트를 올바른 표(UTF-8)로 다시 읽으면 한글이 멀쩡히 돌아옵니다. (깨진 글자 문자열 자체로 되돌리는 것도, 잘못 읽은 표가 256개 값이 모두 정의된 Latin-1이면 안전하게 보장돼요.) 데이터는 안 죽었어요, 해석만 틀린 겁니다.

② 실패형(치환) — 바이트가 규칙에 안 맞아 뭉개짐.

바이트 시퀀스가 읽는 표의 규칙에 아예 안 맞으면 어떻게 될까요? 대부분의 라이브러리는 기본적으로 예외(UnicodeDecodeError)로 터지고, errors="replace"를 줬거나 브라우저처럼 관대한 렌더러일 땐 그 자리를 교체 문자로 치환해 버립니다.

b = "한글".encode("euc-kr") # EUC-KR로 저장 print(b.decode("utf-8", errors="replace")) # �ѱ� — UTF-8 규칙에 안 맞아 �로 치환

여기 나온 가 바로 그 유명한 U+FFFD, 교체 문자(replacement character) 예요. “여긴 못 읽겠다”는 항복 표시죠.

오해형과의 결정적 차이는 여기 있어요. 오해형은 깨진 글자(한글) 안에 원본이 그대로 인코딩돼 있어서, 그 문자열만 올바른 표로 다시 해석하면 복구됩니다. 하지만 실패형은 로 치환되는 순간, 그 깨진 문자열만 봐서는 원본을 되살릴 수 없어요. 원본 정보가 하나로 뭉개졌으니까요. 단, 이 치환은 어디까지나 ‘읽어낸 결과 문자열’에서 일어난 거예요. 원본 바이트(b)를 아직 손에 쥐고 있다면, 올바른 표로 다시 디코드해 얼마든지 살릴 수 있습니다. 정말로 끝나는 건, 그 원본 바이트마저 잃었을 때예요.

이제 여는 글의 두 기호를 해부할 수 있어요.

  • ???의 정체. 많은 시스템이 “이 표에 담을 수 없는 글자”를 만나면 물음표로 뭉갭니다. DB 컬럼이 Latin-1인데 한글을 넣으면?

    print("한글".encode("latin-1", errors="replace")) # b'??' — 담을 수 없어 ?로 치환

    한글이 물음표 두 개로 영구 소실됩니다. DB에서 한글이 ???로 나오는 고전 사고가 정확히 이거예요. 이미 저장 단계에서 죽은 거라, 조회를 아무리 고쳐도 못 살립니다.

  • �의 정체. 이건 이중 깨짐이에요. 교체 문자 (U+FFFD)를 UTF-8로 쓰면 바이트가 EF BF BD인데, 이걸 Latin-1로 잘못 읽으면 �가 됩니다. 한 번 깨진 걸(실패형) 한 번 더 잘못 읽은(오해형) 흔적이죠.

정리하면 모지바케 진단은 이 질문들로 역추적합니다. 어느 단계에서 표가 어긋났나? (저장? 전송? 조회?) 그리고 원본 바이트가 아직 남아 있나? 원본 바이트가 살아 있으면 (오해형이든 실패형이든) 올바른 표로 다시 읽어 복구하고, 원본까지 ?·로 덮여 사라졌으면 — 슬프지만 원본부터 다시 만들어야 합니다.

정리

  • 모지바케(文字化け, 글자가 둔갑함)의 유일한 원인인코딩 표 ≠ 디코딩 표. 1장 “같은 신호, 다른 약속표”의 컴퓨터판이자, 이 책 전체의 결론이다.
  • 두 가지 얼굴:
    • 오해형(둔갑): 바이트는 유효하나 다른 표로 읽어 엉뚱한 글자(밸린). 깨진 문자열 자체에 원본이 보존돼, 올바른 표로 다시 읽으면 복구된다.
    • 실패형(치환): 바이트가 표 규칙에 안 맞아 (U+FFFD)나 ?로 치환. 깨진 출력 문자열만으론 복구 불가 — 원본 바이트가 남았으면 재디코드로 살리고, 원본까지 잃으면 (???가 그대로 저장) 영구 손실.
  • ??? = 담을 수 없는 표로 변환하며 물음표로 소실. � = 교체 문자()를 또 잘못 읽은 이중 깨짐.
  • 진단은 “어느 단계에서 어떤 표가 어긋났나”의 역추적이다. 그리고 근본 원인은 늘 같다 — 바이트에는 인코딩 정보가 없어서(5장), 읽는 쪽의 가정이 틀리면 깨진다.

생각해볼 질문: 원인이 이렇게 명확해요. “표가 서로 다르다.” 그렇다면 해결책도 자명하지 않나요? 나라마다, 회사마다 다른 표를 쓰는 게 문제라면 — 세상 모든 문자를 담는 단 하나의 표를 만들어 다 같이 쓰면 되잖아요. 말은 쉽죠. 한자만 수만 자에, 지구상 문자가 십수만 개인데, 그걸 정말 하나의 표에 다 넣을 수 있을까요? 불가능해 보이던 그 일에 전 인류가 도전합니다. 다음 장, 유니코드의 시작이에요.

11장 · 모든 문자에 번호를 — 유니코드와 코드포인트

Last updated on