Skip to Content

17장. utf8은 진짜 UTF-8이 아니다 — MySQL utf8 vs utf8mb4

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

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

MySQL utf8은 3바이트 utf8mb3라 이모지를 담지 못하며, 저장소부터 커넥션까지 utf8mb4로 맞춰야 진짜 UTF-8이 완성된다.

면접 실전 질문: ① MySQL utf8과 utf8mb4의 핵심 차이는? ② 이모지 저장 오류가 나는 이유는? ③ 문자셋 전환 때 함께 확인할 경로는?


배경 — UTF-8이라며 UTF-8을 거부한다

시나리오는 흔해요. 이모지가 들어간 댓글을 MySQL에 저장하는데, 이런 에러가 터집니다.

ERROR 1366 (HY000): Incorrect string value: '\xF0\x9F\x98\x80' for column 'content' at row 1

분명히 테이블 문자셋을 utf8로 설정했는데도요. 저 \xF0\x9F\x98\x80은 눈치채셨겠지만 웃는 이모지 😀의 UTF-8 바이트예요(13장에서 배운 그 4바이트죠). UTF-8이라며 왜 UTF-8 문자를 거부할까요? 답은 황당합니다. MySQL의 utf8은 진짜 UTF-8이 아니거든요.

스토리 — 3바이트짜리 가짜 UTF-8

MySQL의 utf8은 사실 한 글자에 최대 3바이트까지만 담는 인코딩이에요. 정식 이름은 utf8mb3(mb3 = max bytes 3)입니다. 그런데 13장에서 봤듯 진짜 UTF-8은 최대 4바이트죠. BMP를 벗어난 문자 — 이모지, 일부 확장 한자 — 는 4바이트인데, utf8mb3은 이걸 담을 수가 없습니다.

print(len("한".encode("utf-8"))) # 3 — BMP 문자라 MySQL utf8(=utf8mb3)에 OK print(len("😀".encode("utf-8"))) # 4 — 4바이트! utf8mb3의 3칸엔 안 들어간다 print("😀".encode("utf-8").hex()) # f09f9880 — 첫 바이트 F0은 '4바이트' 리더(13장)

왜 이런 반쪽짜리가 생겼을까요? MySQL이 3바이트 utf8을 도입한 2000년대 중반(4.1)엔, 유니코드 문자가 아직 BMP(3바이트) 안에 다 들어갔어요. 그땐 “3바이트면 충분”이 맞는 판단이었죠. 그런데 그 뒤로 확장 한자 같은 보충 평면 문자가, 그리고 곧이어 이모지가 4바이트 영역으로 쏟아져 들어왔습니다. 11장에서 본 “16비트면 충분한 줄 알았다”의 데이터베이스 판이에요.

문제는 그다음이에요. MySQL은 기존 utf84바이트로 고치는 대신(인덱스 크기 등 하위 호환이 걸렸거든요), 진짜 4바이트 UTF-8을 utf8mb4라는 새 이름으로 2010년에 따로 추가합니다. 그래서 오늘날 MySQL에는 이름이 둘이에요.

  • utf8(= utf8mb3): 최대 3바이트. BMP만. 이모지 저장 불가. 이름만 UTF-8.
  • utf8mb4: 최대 4바이트. 이게 진짜 UTF-8입니다.

핵심 — 이름을 믿지 말고 스펙을 확인하라

이 함정의 본질은 이름이 거짓말을 한다는 거예요. 이 책 내내 우리는 “약속표가 뭔지 확인 하라”고 배웠는데, 여기선 벤더가 붙인 이름 자체가 잘못된 약속이었습니다. utf8이라고 쓰여 있으니 당연히 UTF-8일 줄 알았던 수많은 개발자가, 서비스에 이모지가 처음 올라온 날 Incorrect string value로 데였죠.

해법은 단순해요. 어디서든 utf8 대신 utf8mb4를 쓰는 것. 그런데 한 군데만 바꿔선 안 되고, 데이터가 지나는 모든 경로를 맞춰야 합니다.

  • 컬럼·테이블·데이터베이스의 문자셋을 utf8mb4로.
  • 커넥션 문자셋utf8mb4로(SET NAMES utf8mb4, 또는 드라이버 설정). 저장소가 utf8mb4여도 앱↔DB 연결이 utf8이면 거기서 다시 깨져요.
  • 콜레이션도 짝을 맞춰서(예: utf8mb4_0900_ai_ci, utf8mb4_unicode_ci).
-- 기존 테이블을 utf8mb4로 전환 (3바이트 데이터는 그대로 보존) ALTER TABLE comments CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; -- 저장소가 utf8mb4여도 앱↔DB 커넥션이 utf8이면 거기서 또 깨진다 SET NAMES utf8mb4;

이렇게 경로를 다 맞추면, 아까 거부당하던 😀(F0 9F 98 80)가 그제야 저장됩니다. utf8mb4utf8완전한 상위 집합이라, 3바이트 문자는 그대로 두고 4바이트를 더 담습니다. 기존 데이터를 안 깨뜨리죠. 8장 CP949가 EUC-KR을 확장하던 방식, 14장 UTF-8이 ASCII를 끌어안던 방식과 똑같은 하위 호환 확장이에요. (그래서 MySQL 8.0은 utf8utf8mb3의 별칭으로 비권장하고, 기본을 utf8mb4로 옮겼습니다.)

교훈은 이 책 전체를 관통하는 그 한마디로 돌아옵니다. 인코딩은 이름이 아니라 스펙으로 확인하라. utf8이라는 라벨이 실제로 몇 바이트까지 담는지, latin1 컬럼에 UTF-8을 밀어 넣고 있진 않은지 — 라벨을 믿는 순간 모지바케가 시작됩니다.

정리

  • MySQL의 **utf8은 진짜 UTF-8이 아니라 최대 3바이트짜리 utf8mb3**다. BMP만 담아서 이모지·확장 문자(4바이트)를 저장하면 Incorrect string value 에러가 난다.
  • 원인: 3바이트 utf8 도입 시점(2000년대 중반, MySQL 4.1)엔 유니코드가 BMP(3바이트)에 다 들어갔다. 이후 보충 평면 문자·이모지가 4바이트로 늘자, MySQL은 기존 utf8을 하위 호환 때문에 그대로 두고 진짜 UTF-8을 utf8mb4(2010, 5.5.3)로 따로 추가했다. (11장 “16비트면 충분한 줄 알았다”의 DB 판.)
  • 해법: 컬럼·테이블·DB·커넥션·콜레이션을 전부 utf8mb4로. utf8mb4utf8의 완전한 상위 집합(8·14장식 하위 호환 확장)이라 기존 데이터를 안 깨뜨린다. MySQL 8.0은 utf8mb4가 기본.
  • 교훈: 인코딩은 이름이 아니라 스펙으로 확인하라. 라벨(utf8)을 믿는 순간 모지바케가 시작된다.

생각해볼 질문: 자, 이제 여러분은 인코딩의 역사도, 원리도, length 함정도, MySQL utf8 함정도 다 알게 됐어요. 그런데 실전은 예고 없이 옵니다. 어느 날 로그에 밸린 같은 외계어가 찍히거나, 잘 되던 API가 특정 사용자 이름에서만 터지죠. 이럴 때 바이트를 열어 어느 단계에서 어느 약속표가 어긋났는지 추적하는 법 — 이 책이 쌓아온 모든 지식을 하나의 디버깅 절차로 꿰는 마지막 장으로 갑니다.

18장 · 깨진 글자를 디버깅하다 — 인코딩 버그 현장

Last updated on