깨진 화면을 앞에 두고 어디부터 볼지 고르는 것이 이 글의 주제다.
증상이 원인을 말한다
인코딩 사고는 로그에 에러를 안 남기는 경우가 많다. 그래서 단서가 화면에 보이는 그 모양 하나뿐일 때가 흔하다. 다행히 그 모양은 아무렇게나 정해지지 않는다.
깨진 모양은 아무렇게나 나오지 않는다. 모양마다 처음 볼 곳이 다르다.
증상마다 어긋난 층이 다르다. 이 대응만 알아 두면 처음 볼 곳이 정해진다.
� 물음표 마름모
받는 쪽이 “이 바이트는 내 규칙으로 읽을 수 없다”고 포기한 자리다. 앞서 본 대로 UTF-8은 첫 바이트가 길이를 말하고 이어지는 조각은 정해진 모양으로 시작하는데, 그 모양이 안 맞으면 이 표시가 나온다.
원인은 대개 둘이다.
- 다른 규칙으로 담긴 데이터를 UTF-8로 읽었다. 옛 방식으로 저장된 파일이나 외부 시스템 응답이 흔한 출처다.
- 바이트가 중간에서 잘렸다. 길이 제한에 걸려 잘렸거나, 나눠 받은 조각을 다 모으기 전에 글자로 바꿨다.
둘을 가르는 방법은 위치를 보는 것이다. 문서 전체가 고르게 깨졌으면 앞엣것, 끝에서만 한두 글자가 깨졌으면 뒤엣것이다.
茄臂 엉뚱한 글자
읽기는 성공했는데 다른 번호부로 읽은 경우다. 바이트는 온전하고 해석만 틀렸다.
이쪽이 오히려 다루기 쉽다. 원본 바이트가 그대로 살아 있으니 되돌릴 수 있다. 어느 규칙으로 담겼는지만 알아내면 복구된다.
주의할 것은 여기서 잘못 대응하는 경우다. 깨진 글자를 보고 문자열을 다시 인코딩해 가며 맞는 조합을 찾다가, 중간에 한 번이라도 �가 생기면 그 자리는 영영 못 되돌린다. 포기한 자리는 원본 정보가 사라진 자리라서다.
□ 네모 상자
이건 인코딩 문제가 아니다. 데이터는 멀쩡한데 그 글자를 그릴 폰트가 없는 것이다.
가르는 법은 간단하다. 복사해서 다른 곳에 붙여 넣어 보면 된다. 붙여 넣은 곳에서 제대로 보이면 폰트 문제이고, 거기서도 깨져 있으면 데이터 문제다.
□를 보고 인코딩 설정을 뒤지는 건 흔한 헛수고다. 서버 폰트가 없어 PDF에서만 네모가 나오는 경우가 특히 자주 걸린다.
길이나 비교가 안 맞을 때
화면은 멀쩡한데 숫자와 판정이 이상한 부류가 있다. 이건 깨짐이 아니라 세는 자와 형태의 문제다.
| 증상 | 볼 곳 |
|---|---|
| 글자 수 제한이 사용자 기대와 다르다 | 어느 단위로 세고 있나(사람이 보는 글자·코드 유닛·바이트) |
| 눈에 같은 문자열인데 같지 않다고 나온다 | 정규화 형태가 다른가 |
| DB에 넣을 때만 잘린다 | 컬럼 길이를 바이트로 세고 있나 |
| 정렬 순서가 기대와 다르다 | 코드포인트 순서로 정렬하고 있나 |
되짚는 순서
원인을 찾을 때 코드를 위에서부터 읽는 건 대개 헛수고다. 데이터가 지나온 경계를 거꾸로 따라가는 편이 빠르다.
- 화면에서 시작한다. 브라우저인지, 콘솔인지, 로그 파일인지. 화면 자체가 잘못된 규칙으로 렌더링될 수도 있다.
- 응답을 본다. 헤더의
charset과 실제 바이트가 일치하는지. - 저장소를 본다. DB 클라이언트로 직접 조회했을 때 이미 깨져 있으면 들어갈 때 깨진 것이고, 멀쩡하면 나올 때 깨진 것이다.
- 들어온 자리를 본다. 파일 업로드, 외부 API, 배치 입력처럼 시스템 밖에서 들어온 지점이다.
핵심은 3번이다. “저장된 것이 이미 깨졌나”를 먼저 확인하면 범위가 절반으로 준다. 이 하나만 습관으로 만들어도 대부분의 시간이 절약된다.
이 시리즈가 만든 판단표
들고 다닐 만한 것만 한 표로 접었다.
| 상황 | 물어야 할 것 |
|---|---|
| 파일을 읽고 쓴다 | 규칙을 코드에 명시했나, 시스템 기본값에 기대고 있나 |
| 수가 커진다 | 32비트로 충분한가, 도중 계산에서 넘치지 않나 |
| 소수를 다룬다 | 정확해야 하는 값인가 근사해도 되는가. 돈이면 정수나 십진 타입 |
| 글자 수를 센다 | 사람이 보는 글자인가 바이트인가. 화면과 DB가 같은 자를 쓰나 |
| 문자열을 비교한다 | 정규화했나 |
| 문자열을 자른다 | 바이트로 자르고 있지 않나 |
| 바이너리를 텍스트에 싣는다 | 33% 손해를 감당할 크기인가. 숨기려는 목적은 아닌가 |
각 항목의 근거는 시리즈에 흩어져 있다. 필요한 것만 골라 보면 된다.
- 컴퓨터는 글자를 모른다 - 약속표라는 개념과 경계에서 명시하기
- 비트와 바이트 - 자리 수가 곧 한계, 16진수를 쓰는 이유
- 정수와 넘침 - 되감기, 도중 계산의 함정
- 부동소수점 - 못 맞추는 값, 성겨지는 눈금, 돈 다루기
- 아스키에서 유니코드로 - 번호부 통합, 번호와 바이트의 층 분리
- UTF-8 - 가변 길이, 자기기술, 호환성이 이긴 이유
- 글자 수 세기 - 네 가지 자, 정규화
- Base64와 퍼센트 인코딩 - 텍스트 통로, 33% 비용
여기서 다루지 않은 것
이 시리즈는 글자와 수를 바이트에 담는 일만 다뤘다. 같은 “담는 이야기”인데 뺀 것들이 있다.
- 시간 - 시각을 수로 담는 방식(기준 시점부터 센 초), 시간대, 서머타임. 담는 이야기이면서 규칙이 정치적으로 바뀌는 별종이라 따로 다룰 만하다.
- 압축 - 같은 정보를 더 적은 바이트로 담는 일이다. 인코딩과 층이 겹쳐 보이지만 목적이 다르다.
- 이미지·오디오 포맷 - 바이트 배치를 정한다는 점은 같지만 손실 여부라는 축이 하나 더 붙는다.
- 정렬 규칙 - 같은 언어에서도 나라마다 사전 순서가 다르다. 유니코드 번호 순서와는 별개의 규칙이다.
정리
- 깨진 모양은 어디가 어긋났는지를 말해 준다. 아무렇게나 나오는 게 아니다.
�는 포기한 자리다. 원본 정보가 사라졌으니 여기까지 가기 전에 잡아야 한다.- 엉뚱한 글자는 해석만 틀린 것이라 되돌릴 수 있다. 잘못 만지다
�를 만들지 않는 게 중요하다. □는 폰트 문제다. 복사해서 다른 데 붙여 보면 곧장 갈린다.- 되짚을 때는 저장된 것이 이미 깨졌는지부터 확인한다. 범위가 절반으로 준다.
- 결국 이 시리즈 전체가 한 문장이다. 어떤 약속으로 담았는지 밝히고, 경계마다 그 약속을 못 박는다.