대체 문자
고친 사람 github-actions[bot]
대체 문자는 읽을 수 없는 바이트 대신 끼워 넣는 글자입니다.
화면에는 물음표가 든 검은 마름모 � 로 보입니다.
이 글자 덕분에 프로그램은 글 전체를 버리지 않고 끝까지 읽어 나갑니다.
문자 메시지 발송에서 말하는 대체 문자와는 이름만 같습니다.
쉽고 빠른 이해
텍스트를 읽다가 해석이 안 되는 바이트가 나오면 그 바이트 대신 넣는 표시용 글자입니다.
웹 페이지나 로그에서 한글 대신 � 가 줄줄이 보인다면 이 글자를 만난 것입니다.
이 글자가 없으면 읽는 프로그램에 남는 방법은 둘입니다. 오류를 내고 글 전체를 포기하거나, 못 읽은 부분을 말없이 지우는 것입니다. 앞쪽은 멀쩡한 나머지까지 잃습니다. 뒤쪽은 흔적조차 안 남깁니다.
- 프로그램이 바이트를 약속된 규칙대로 글자로 바꿉니다
- 규칙에 안 맞는 바이트가 나오면 그 바이트 대신
�하나를 넣습니다 - 다음 바이트부터 다시 읽어 나갑니다
대가는 원래 바이트가 사라진다는 것입니다. 한 번 저장된 � 는 원래 무슨 글자였는지 알려 주지
않습니다. 그래서 이 글자는 화면에 띄워 보고 말 텍스트에만 씁니다. 저장하거나 넘길 텍스트는
오류를 내고 멈추게 합니다.
상세
오래된 문서를 옮겨 적는 사람을 떠올려 보면 가깝습니다. 먹이 번져 읽을 수 없는 글자를 만나면 그 칸에 표시를 하나 해 둡니다. 그리고 다음 글자로 넘어갑니다. 옮겨 적은 글을 읽는 사람은 원래 무슨 글자였는지 몰라도 거기서 무언가 빠졌다는 것은 압니다.
컴퓨터는 글자를 번호로 다룹니다. 유니코드는 세상의 글자마다 번호를 하나씩 매겨 둔 표입니다. 이 표가 있어 어느 프로그램이든 같은 글자를 같은 번호로 부릅니다.
유니코드가 매긴 번호 하나를 코드 포인트라고 부릅니다. 적을 때는 U+ 뒤에 십육진수를
붙입니다. 영문 대문자 A 의 코드 포인트는 U+0041 입니다.
대체 문자는 유니코드가 「여기 못 읽은 것이 있었다」를 나타내려고 따로 떼어 둔 글자입니다.
번호는 U+FFFD 이고 영어 이름은 REPLACEMENT CHARACTER 입니다. 화면에는 검은 마름모 안에
흰 물음표가 든 모양 � 로 그려집니다.
바이트를 글자로 되돌리는 디코딩
대체 문자가 언제 끼어드는지 보려면 텍스트가 어떻게 저장되는지부터 알아야 합니다. 글자는 바이트가 되어 저장되고, 읽을 때 다시 글자로 돌아옵니다.
글자 번호를 바이트로 적는 규칙을 문자 인코딩이라고 부릅니다. 파일과 네트워크에는 글자가 아니라 바이트만 실리기 때문에 이 규칙이 필요합니다. 규칙에 따라 글자를 바이트로 바꾸는 부품은 인코더라고 부릅니다.
UTF-8(Unicode Transformation Format 8-bit, 유니코드 변환 형식 8비트)은 코드 포인트 하나를
한 바이트에서 네 바이트로 적는 문자 인코딩입니다. 한글 「한」을 UTF-8 로 적으면 ED 95 9C 세
바이트가 됩니다. 이 문서의 바이트 값은 모두 십육진수입니다.
반대로 바이트를 글자로 되돌리는 일을 디코딩이라고 합니다. 이 일을 맡는 부품을 디코더라고 부릅니다. 파일을 열 때, 요청 본문을 문자열로 받을 때, 로그를 읽을 때마다 어딘가에서 디코더가 돕니다.
대체 문자는 이 디코딩 단계에서 생깁니다. 디코더가 받은 바이트가 규칙에 안 맞을 때입니다. 흔한 원인은 둘입니다. 다른 인코딩으로 적힌 바이트를 넘겨받은 경우와 한 글자를 이루는 바이트 묶음이 중간에서 잘린 경우입니다.
디코더가 고를 수 있는 세 대응
규칙에 안 맞는 바이트를 만난 디코더가 할 수 있는 일은 셋입니다. 셋을 견주면 대체 문자가 왜 따로 마련됐는지 보입니다.
| 대응 | 디코더가 하는 일 | 결과 |
|---|---|---|
| 멈춘다 | 오류를 내고 디코딩을 그만둔다 | 멀쩡한 나머지 글자까지 못 받는다 |
| 지운다 | 못 읽은 바이트를 건너뛴다 | 무언가 빠졌다는 흔적이 안 남는다 |
| 바꾼다 | 못 읽은 바이트 대신 U+FFFD 를 넣는다 |
끝까지 읽고, 빠진 곳에 표시가 남는다 |
많은 디코더가 이 가운데 멈추는 쪽과 바꾸는 쪽을 설정으로 고르게 합니다. 이 설정을 디코더의 오류 모드라고 부릅니다. 같은 바이트를 넣어도 이 설정에 따라 결과가 갈립니다.
오류 모드의 두 값에는 흔히 부르는 이름이 있습니다. 멈추는 쪽은 엄격 모드, 바꾸는 쪽은 대체 모드입니다.
지우는 방식은 위험합니다. 못 읽은 바이트 앞뒤의 조각이 붙어 원래 없던 낱말이 생길 수 있습니다.
입력 검증이 <script> 라는 글자를 찾아 막는다고 해 봅시다. <scr 와 ipt> 사이에 깨진
바이트가 끼어 있으면 검증은 이 낱말을 못 찾고 통과시킵니다.
그 뒤 디코더가 깨진 바이트를 지우면 두 조각이 붙어 <script> 가 됩니다. 검증을 통과한 입력이
이렇게 교차 사이트 스크립팅 공격 코드로 바뀝니다. 남의 웹 페이지에 스크립트를 심어 방문자
브라우저에서 돌게 하는 공격입니다.
대체 문자는 두 문제를 함께 피합니다. 읽기를 멈추지 않으면서 빠진 곳에 눈에 보이는 표시를 남깁니다. 표시가 끼어 있으니 앞뒤 조각이 붙어 새 낱말이 되지도 않습니다.
아래 그림은 두 모드의 디코더가 바이트를 읽어 나가는 순서입니다. 엄격 모드는 첫 오류에서
멈춥니다. 대체 모드는 못 읽은 바이트 묶음마다 U+FFFD 를 하나씩 넣고 끝까지 갑니다.
flowchart TD
A["다음 바이트 묶음을 본다"] --> B{"인코딩 규칙에 맞나"}
B -->|맞다| C["글자로 바꿔 내보낸다"]
B -->|안 맞다| D{"오류 모드"}
D -->|엄격 모드| E["오류를 내고 멈춘다"]
D -->|대체 모드| F["U+FFFD 를 내보낸다"]
C --> A
F --> A
코드에서 만나는 모습
한글을 EUC-KR(Extended Unix Code for Korean, 한국어 확장 유닉스 코드)로 적은 바이트를 UTF-8 로 잘못 읽는 상황을 파이썬으로 옮겨 봅니다. EUC-KR 은 유니코드 이전부터 쓰던 한국어 전용 인코딩입니다.
b = "한".encode("euc-kr") # b'\xc7\xd1'
b.decode("utf-8") # 예외
b.decode("utf-8", "replace") # '��'
첫 줄은 「한」을 EUC-KR 바이트 C7 D1 로 바꿉니다. 둘째 줄은 엄격 모드로 UTF-8 디코딩을 해서
UnicodeDecodeError 예외가 납니다. 셋째 줄은 대체 모드로 같은 일을 합니다. 두 바이트가 UTF-8
규칙에 안 맞아 각각 대체 문자 하나로 바뀌었습니다.
언어마다 기본 모드가 다릅니다. 파이썬의 decode 는 따로 말하지 않으면 엄격 모드로 돕니다.
자바의 new String(bytes, UTF_8) 은 오류를 내는 설정이 없습니다. 언제나 대체 모드로 돕니다.
그래서 인코딩을 잘못 짚어도 예외 없이 � 가 섞인 문자열이 만들어집니다.
한 번 들어가면 되돌릴 수 없는 까닭
읽기를 이어 가는 대신 잃는 것이 있습니다. 원래 바이트입니다.
디코더가 U+FFFD 를 넣는 순간 원래 바이트는 버려집니다. C7 이었든 FF 였든 결과는 똑같은
� 하나입니다. 문자열만 받아 든 쪽은 � 가 원래 무슨 바이트였는지 알아낼 방법이 없습니다.
게다가 � 는 오류 표시가 아니라 멀쩡한 글자입니다. 데이터베이스에 저장되고 입력 검증도
통과합니다. UTF-8 로 다시 적으면 EF BF BD 세 바이트가 되어 파일에도 남습니다.
그래서 대체 문자를 없애려면 그것이 생긴 디코딩 단계로 거슬러 올라가야 합니다. 그 단계에서
올바른 인코딩을 지정하면 다음부터는 생기지 않습니다. 이미 저장된 � 는 원본 바이트를 따로
남겨 두지 않았다면 되살릴 수 없습니다.
두 모드를 고르는 기준
앞 소절의 대가 때문에 두 모드는 쓰임새가 갈립니다. 기준은 하나입니다. 디코딩한 글자를 사람이 보고 마는지, 저장하거나 다른 곳으로 넘기는지입니다.
| 상황 | 고르는 모드 | 까닭 |
|---|---|---|
| 로그를 화면에 띄워 훑어본다 | 대체 모드 | 깨진 몇 글자 때문에 나머지를 못 보면 안 된다 |
| 받은 텍스트를 데이터베이스에 저장한다 | 엄격 모드 | 오류가 나야 잘못 짚은 인코딩을 알아챈다 |
| 받은 텍스트를 다른 서비스로 넘긴다 | 엄격 모드 | � 가 섞인 채 넘어가면 받는 쪽도 원본을 못 찾는다 |
보고 마는 텍스트는 한 글자가 틀려도 잃는 것이 없습니다. 저장하는 텍스트는 거기서 원본 바이트가 사라집니다. 그래서 저장 전에는 오류를 받아 원인을 고치는 쪽을 고릅니다.
대체 문자와 헷갈리는 글자 깨짐
한글이 깨져 보이는 모습은 여럿입니다. 대체 문자를 이웃한 두 깨짐과 견주면 화면만 보고 원인을 가를 수 있습니다.
모지바케는 바이트를 다른 인코딩 규칙으로 읽어 엉뚱한 글자가 나오는 현상입니다. 「가」의
UTF-8 바이트 EA B0 80 을 서유럽 언어용 인코딩인 Windows-1252 로 읽으면 ê°€ 가 됩니다.
디코더가 규칙에 맞는 바이트로 여겼으므로 대체 문자는 안 들어갑니다. 바이트가 남아 있어 올바른
규칙으로 다시 읽으면 대개 되돌아옵니다.
빈 네모 □ 는 원인이 또 다릅니다. 디코딩은 제대로 끝났고 글자도 멀쩡합니다. 화면에 그릴
글꼴에 그 글자의 모양이 없어 네모로 대신 그린 것입니다. 모양이 두부를 닮아 흔히 두부라고
부릅니다.
세 가지를 화면 모양으로 견주면 이렇습니다.
| 화면 | 이름 | 무슨 일이 있었나 | 원래 바이트 |
|---|---|---|---|
� |
대체 문자 | 디코더가 못 읽고 표시를 넣었다 | 사라졌다 |
ê°€ 같은 엉뚱한 글자 |
모지바케 | 다른 인코딩 규칙으로 읽었다 | 남아 있다 |
□ |
두부 | 글꼴에 그 글자의 모양이 없다 | 남아 있다 |
둘이 겹친 모습도 자주 보입니다. � 는 대체 문자의 UTF-8 바이트 EF BF BD 를 Windows-1252
로 다시 읽은 결과입니다. 대체 문자가 한 번 들어간 뒤에 모지바케가 한 번 더 일어났다는
흔적입니다.
글자를 바이트로 적을 때 생기는 물음표
지금까지는 바이트를 글자로 읽는 쪽이었습니다. 반대 방향, 인코더가 글자를 바이트로 적는 쪽에서도 비슷한 치환이 일어납니다.
적으려는 인코딩에 그 글자가 아예 없을 때가 그렇습니다. 서유럽 언어용 인코딩에는 한글이
없습니다. 이런 인코딩은 U+FFFD 도 담지 못해 대체 문자를 적을 수도 없습니다. 그래서 인코더는
대개 대신 물음표 ? 를 적습니다.
"한".encode("latin-1", "replace") # b'?'
latin-1 은 서유럽 언어용 인코딩의 하나입니다. 「한」이 이 인코딩에 없어
물음표 한 바이트로 적혔습니다. 여기서도 "replace" 가 멈추는 대신 바꾸라는 설정입니다.
데이터베이스에 저장한 한글이 ??? 로 보인다면 대개 이 경우입니다. 물음표는 대체 문자보다
알아보기 어렵습니다. 본래 물음표였던 글자와 구별되지 않기 때문입니다.
같은 이름을 쓰는 다른 대상
「대체 문자」라는 이름은 다른 두 곳에서도 쓰입니다. 둘 다 U+FFFD 와는 다른 대상입니다.
ASCII(American Standard Code for Information Interchange, 미국 정보 교환 표준 부호)에는
SUB(substitute)라는 제어 문자가 있습니다. 제어 문자는 화면에 그릴 글자가 아니라 장치에
신호를 주려고 만든 글자입니다. SUB 의 번호는 십육진수 1A 입니다.
SUB 는 오류가 난 글자 대신 넣으라고 만든 글자라서 영어로 substitute character 라고 부릅니다.
용도는 대체 문자와 닮았지만 ASCII 시절의 제어 문자입니다. 유니코드 디코더가 못 읽은 바이트
대신 넣는 글자는 SUB 가 아니라 U+FFFD 입니다.
메시지 발송 쪽에서는 뜻이 전혀 다릅니다. 메신저 알림이 실패했을 때 대신 보내는 SMS(Short Message Service, 단문 메시지 서비스)를 대체 문자라고 부릅니다. 이 항목의 대체 문자와는 이름만 같습니다.
관련 항목
대체 문자를 정의하는 문자 체계
Unicode · 코드 포인트 · UTF-8 · UTF-16 · UTF-32 · 문자 인코딩
대체 문자를 끼워 넣는 변환 단계
디코딩 · 인코딩 · 디코더 · 인코더 · 오류 모드 · 인코딩 스니핑 알고리즘
대체 문자가 생기는 원인
인코딩 불일치 · 잘린 멀티바이트 문자 · 문자열 자르기 · 대리 쌍 · 과잉 길이 인코딩 · 인코딩 추측 · EUC-KR
대체 문자와 헷갈리는 글자 깨짐
모지바케 · 글꼴 대체 · 글꼴 · 글리프 · 보이지 않는 문자 · 바이트 순서 표시
대체 문자 대신 물음표를 적는 인코딩
Windows-1252 · ISO-8859-1 · ASCII
지우기 대신 대체를 고르게 만든 보안 문제
교차 사이트 스크립팅 · 입력 검증 · 유니코드 정규화 · 동형 문자 공격
대체 문자와 이름이 겹치는 다른 대상
제어 문자 · SUB 제어 문자 · SMS · 알림톡
다른 이름: replacement character · REPLACEMENT CHARACTER · U+FFFD