목차
“글자 수”라는 말이 층마다 다른 것을 가리킨다. 그래서 셀 때마다 답이 달라진다.
레고로 만든 강아지는 몇 개인가
책상에 레고로 조립한 강아지가 하나 놓여 있다. 몇 개냐고 물으면 답이 갈린다. 눈에 보이는 물건은 하나이고, 뜯어 보면 블록은 열두 개다. 둘 다 맞는 답인데 세는 단위가 다르다.
글자도 똑같다. 그리고 컴퓨터가 세는 단위는 하나가 아니라 여럿이다.
세는 층이 넷이다
네 식구가 그려진 가족 이모지 하나를 예로 보자. 화면에는 그림 하나가 보인다.
하나를 세는데 층마다 답이 다르다. 어느 자로 세는지 정하지 않으면 화면과 코드와 DB가 서로 다른 수를 든다.
같은 것을 세는데 1, 7, 11, 25가 나온다. 넷 다 틀리지 않았다.
언어마다 답이 갈리는 것도 여기서 나온다. 파이썬의 len은 코드포인트를 세서 7을 내놓고, 자바의 length()와 자바스크립트의 .length는 코드 유닛을 세서 11을 내놓는다. 버그가 아니라 서로 다른 자를 든 것이다.
| 세는 단위 | 무엇을 세나 | 어디서 쓰나 |
|---|---|---|
| 사람이 보는 글자 | 화면에 보이는 덩어리 | 사용자가 기대하는 수 |
| 코드포인트 | 유니코드 번호 | 유니코드 처리 |
| 코드 유닛 | 언어 안쪽의 저장 단위 | 자바·자바스크립트의 length |
| 바이트 | 파일·네트워크에 실린 양 | DB 길이 제한, 전송량 |
입력창 사고가 여기서 난다. 화면의 100자는 맨 윗줄로 세고, 코드의 length는 셋째 줄로 세고, DB 컬럼 제한은 넷째 줄로 센다. 세 군데가 서로 다른 자를 들고 있으니 사용자는 40자를 쓰고도 막힌다.
이모지는 조각을 붙여 만든다
왜 하나가 여럿이 되는지는 이모지를 뜯어 보면 보인다. 가족 이모지는 통째로 하나의 번호를 받은 게 아니라 사람 이모지 여럿을 보이지 않는 접착제로 이어 붙인 것이다.
- 접착제 문자가 사이에 들어가 “이 조각들을 하나로 그려라”라고 지시한다. 눈에는 안 보이지만 데이터에는 있다.
- 피부색도 별도 조각이다. 기본 이모지 뒤에 색 변형 문자를 붙여 만든다.
- 국기는 알파벳 두 글자짜리 특수 문자를 이어 붙인 것이다. 한국 국기는
K와R에 해당하는 조각 둘이다.
이 방식의 값은 조합 폭발을 막는 것이다. 가족 구성과 피부색 조합마다 번호를 따로 매기면 번호부가 감당이 안 된다. 조각을 조합해 만들면 번호 몇 개로 수만 가지를 표현한다.
대가가 지금 보고 있는 그것이다. 하나로 보이는 것이 실은 여러 개다. 그래서 뒤에서 한 글자를 지우면 이모지가 사라지는 게 아니라 조각 하나만 떨어져 나가 다른 그림으로 변하는 일이 벌어진다.
같아 보이는데 다른 글자
더 고약한 경우가 있다. 화면에 완전히 똑같이 보이는데 데이터가 다른 경우다.
눈에는 같은 글자인데 바이트가 다르면 검색도 비교도 중복 검사도 전부 빗나간다.
é 같은 글자를 만드는 방법이 둘이다. 통째로 하나의 번호를 쓸 수도 있고, e 다음에 악센트 기호를 붙일 수도 있다. 화면에는 똑같이 그려지지만 바이트는 다르다.
그래서 이런 일이 벌어진다.
- 눈으로는 같은 문자열인데
equals가false다 - 검색창에 똑같이 입력했는데 결과가 안 나온다
- 중복 가입을 막았는데 같은 이름으로 또 가입된다
한글도 해당한다. 한을 통째로 담을 수도 있고 ㅎ·ㅏ·ㄴ 자모로 나눠 담을 수도 있다.
정규화가 이걸 푼다
해법은 정규화다. 어느 한쪽 형태로 통일하는 변환이다.
- 붙이는 쪽(NFC) - 가능한 것은 통째로 된 하나의 번호로 합친다. 웹 표준이 권장하는 방식이고 대개 이걸 쓴다.
- 쪼개는 쪽(NFD) - 반대로 전부 조각으로 나눈다.
중요한 건 어느 쪽을 고르냐보다 어디서 하느냐다. 원칙은 둘이다.
- 들어올 때 정규화한다. 저장 전에 한 번 통일해 두면 그 뒤로는 신경 쓸 일이 없다.
- 비교하기 직전에도 정규화한다. 밖에서 들어온 값과 비교할 때는 그쪽이 어느 형태일지 모른다.
이건 해시테이블로 중복을 걸러 낼 때도 그대로 적용된다. 같은 이름인데 형태가 다르면 해시값 자체가 달라서 서로 다른 칸에 들어간다. 자료구조가 잘못한 게 아니라 들어간 데이터가 다른 것이다.
자를 때 무엇이 부서지나
앞 글에서 바이트 중간에서 자르면 글자가 깨진다고 했다. 여기까지 오면 자를 수 있는 자리가 층마다 다르다는 게 보인다.
| 어디서 자르나 | 결과 |
|---|---|
| 바이트 중간 | � 하나가 남는다 |
| 코드 유닛 중간 | 이모지가 깨진 조각이 된다 |
| 코드포인트 사이 | 글자는 온전한데 가족이 흩어진다 |
| 사람이 보는 글자 사이 | 안전하다 |
그래서 “50자로 잘라 미리보기” 같은 기능은 생각보다 까다롭다. 맨 아랫줄로 자르려면 언어나 라이브러리가 사람이 보는 글자 단위를 지원해야 한다. 안 되면 최소한 코드포인트 경계로는 자르고, 끝에 붙은 접착제 문자는 같이 떼는 정도의 처리가 필요하다.
실무에서: 어느 자를 쓸지 먼저 정한다
이 층 문제는 코드를 잘 짜서 피하는 게 아니라 어느 단위로 셀지 정해서 피한다.
- 길이 제한 - 사용자에게 보이는 제한이면 사람이 보는 글자로 세고, DB 컬럼 제한이면 바이트로 센다. 둘을 같은 숫자로 두면 반드시 어긋난다. DB 쪽을 넉넉히 잡는 것이 가장 싸다.
- 중복·검색 - 저장 전 정규화가 답이다. 여기에 대소문자·앞뒤 공백 처리까지 묶어 한 함수에서 한 번에 하는 편이 안전하다. 여러 군데서 제각기 하면 반드시 빠지는 자리가 생긴다.
- 파일 이름 - 맥은 자모를 쪼개는 형태로 저장하고 다른 OS는 합친 형태를 쓴다. 그래서 맥에서 만든 압축 파일의 한글 이름이 윈도우에서 자모 분리되어 보인다. 인코딩 사고가 아니라 정규화 사고다.
- 비밀번호 - 정규화 시점이 로그인과 가입에서 다르면 가입은 됐는데 로그인이 안 되는 사고가 난다.
정리
- “글자 수”는 층마다 다르다. 사람이 보는 글자 · 코드포인트 · 코드 유닛 · 바이트 넷이 각각 다른 값을 낸다.
- 이모지는 조각을 접착제로 이어 붙인 것이다. 그래서 하나로 보여도 여럿이다.
- 같아 보이는데 바이트가 다른 경우가 있다. 합친 형태와 쪼갠 형태다.
- 해법은 정규화다. 들어올 때 한 번, 비교 직전에 한 번.
- 자를 수 있는 안전한 자리는 사람이 보는 글자 사이뿐이다.
- 실무에서는 코드를 고치기 전에 어느 자로 셀지부터 정한다.
다음 글은 방향을 뒤집는다. 지금까지는 글자를 바이트로 담았는데, 바이트를 글자로 담아야 하는 상황이 있다.