Monday, August 20, 2018

MySQL에서 이모지(emoji)가 물음표로 나올 때: utf8mb4 완벽 적용기

오늘날 현대적인 웹 애플리케이션과 모바일 앱 환경에서 사용자의 입력은 단순한 텍스트를 넘어섭니다. 다양한 언어의 문자는 물론, 감정과 상징을 표현하는 이모지(emoji)는 이제 필수적인 데이터 요소가 되었습니다. 하지만 많은 개발자들이 데이터베이스, 특히 MySQL에서 이모지를 저장하려 할 때 데이터가 깨지거나(물음표 '???'로 표시), Incorrect string value 오류가 발생하는 골치 아픈 문제를 겪습니다. 이 문제의 핵심에는 MySQL의 문자 인코딩 설정, 구체적으로는 utf8utf8mb4의 차이에 대한 이해 부족이 자리 잡고 있습니다.

많은 가이드가 단순히 'charset을 utf8mb4로 바꾸세요'라고 말하지만, 문제는 그렇게 간단하지 않을 때가 많습니다. 서버 설정, 데이터베이스 기본값, 테이블 구조, 심지어 애플리케이션의 데이터베이스 연결(Connection) 설정까지 모두 일관성을 갖춰야 비로소 이모지가 아무 문제 없이 데이터베이스에 저장되고 조회될 수 있습니다. 이 글에서는 MySQL에서 이모지 및 다국어 문자를 완벽하게 처리하기 위해 utf8mb4를 올바르게 적용하는 전 과정을, 단순한 설정 변경을 넘어 심층적인 원리 이해와 실전 문제 해결까지 모두 다루고자 합니다.

1. 모든 문제의 시작: 왜 MySQL의 'utf8'은 진짜 UTF-8이 아닌가?

문제를 해결하기 위해선 먼저 원인을 정확히 알아야 합니다. 대부분의 개발자들은 'utf8' 캐릭터셋이 유니코드 문자를 모두 지원한다고 생각합니다. 하지만 안타깝게도 MySQL의 utf8 캐릭터셋은 반쪽짜리 지원에 불과합니다.

유니코드 표준에서 문자는 코드 포인트(Code Point)로 표현됩니다. 이 코드 포인트를 컴퓨터가 이해할 수 있는 바이트 시퀀스로 변환하는 규칙이 바로 '인코딩'입니다. UTF-8(Unicode Transformation Format - 8-bit)은 가변 길이 인코딩 방식으로, 하나의 문자를 1바이트에서 최대 4바이트까지 사용하여 표현합니다.

  • 1바이트: 기본 ASCII 문자 (a-z, 0-9 등)
  • 2바이트: 대부분의 유럽 언어 문자
  • 3바이트: 한글, 한자 등 기본적인 다국어 평면(BMP, Basic Multilingual Plane)에 속한 문자
  • 4바이트: 고대 문자, 수학 기호, 그리고 대부분의 현대적인 이모지(😂, 🤔, 🚀 등)

문제는 MySQL이 utf8 캐릭터셋을 구현할 때, 유니코드 표준의 모든 것을 담지 않고 당시 널리 사용되던 BMP 영역의 문자만을 고려하여 문자당 최대 3바이트만 사용하도록 제한했다는 점입니다. 이 때문에 MySQL의 utf8은 사실상 utf8mb3(UTF-8 Multi-Byte 3)의 별칭(alias)입니다. 결과적으로, 4바이트가 필요한 이모지나 일부 특수 문자는 MySQL의 utf8 캐릭터셋을 사용하는 컬럼에 저장될 수 없습니다.

이러한 혼란과 한계를 해결하기 위해 MySQL 5.5.3 버전부터 utf8mb4(UTF-8 Multi-Byte 4)라는 새로운 캐릭터셋이 도입되었습니다. 이름에서 알 수 있듯이, utf8mb4는 문자당 최대 4바이트를 사용하여 모든 유니코드 문자를 손실 없이 저장할 수 있는, 진정한 의미의 UTF-8 구현체입니다. 따라서 새로운 프로젝트를 시작한다면 고민할 필요 없이 처음부터 utf8mb4를 사용하는 것이 정답입니다.

2. 단계별 완벽 전환: 체계적인 utf8mb4 적용 전략

기존 시스템을 utf8mb4로 전환하거나, 문제가 발생하는 시스템을 진단하려면 체계적인 접근이 필요합니다. 단순히 설정 파일 하나를 고치는 것으로는 부족하며, 다음의 4가지 계층을 모두 확인하고 수정해야 합니다.

  1. MySQL 서버 설정
  2. 데이터베이스(스키마) 설정
  3. 테이블 및 컬럼 설정
  4. 애플리케이션 연결 설정

1단계: 현재 상태 정밀 진단하기

전환 작업을 시작하기 전에, 현재 시스템의 각 계층이 어떤 캐릭터셋으로 설정되어 있는지 확인하는 것이 매우 중요합니다. 다음 SQL 쿼리들을 통해 현재 상태를 파악할 수 있습니다.

서버 레벨 캐릭터셋 확인


SHOW VARIABLES LIKE 'character\_set\_%';
-- 또는
SHOW VARIABLES WHERE Variable_name LIKE 'character\_set\_%' OR Variable_name LIKE 'collation%';

위 쿼리를 실행하면 다음과 유사한 결과를 볼 수 있습니다. utf8mb4로의 전환이 필요한 시스템은 대부분의 값이 utf8 또는 latin1 등으로 설정되어 있을 것입니다.


+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------|
| character_set_client     | utf8mb4                    |
| character_set_connection | utf8mb4                    |
| character_set_database   | utf8                       | <-- 문제의 소지
| character_set_filesystem | binary                     |
| character_set_results    | utf8mb4                    |
| character_set_server     | utf8                       | <-- 문제의 소지
| character_set_system     | utf8                       |
| collation_connection     | utf8mb4_unicode_ci         |
| collation_database       | utf8_general_ci            | <-- 문제의 소지
| collation_server         | utf8_general_ci            | <-- 문제의 소지
+--------------------------+----------------------------+

여기서 중요한 변수는 character_set_servercollation_server입니다. 이 값들이 utf8mb4와 그에 맞는 collation(예: utf8mb4_unicode_ci)이 아니라면 서버 설정 변경이 필요합니다.

데이터베이스 레벨 캐릭터셋 확인


SELECT
    schema_name AS 'Database',
    default_character_set_name AS 'Charset',
    default_collation_name AS 'Collation'
FROM information_schema.schemata;

-- 또는 특정 데이터베이스만 확인하려면
SHOW CREATE DATABASE your_database_name;

이 쿼리는 각 데이터베이스의 기본 캐릭터셋과 콜레이션(Collation, 정렬 규칙)을 보여줍니다. 이 값이 utf8이라면 새로 생성되는 테이블에 문제가 발생할 수 있습니다.

테이블 및 컬럼 레벨 캐릭터셋 확인

가장 중요한 부분입니다. 이미 생성된 테이블과 컬럼은 데이터베이스나 서버의 기본값이 변경되어도 자동으로 바뀌지 않습니다.


-- 특정 테이블의 캐릭터셋 확인
SHOW CREATE TABLE your_table_name;

-- 데이터베이스 내 모든 테이블의 캐릭터셋 확인
SELECT
    table_name AS 'Table',
    table_collation AS 'Collation'
FROM information_schema.tables
WHERE table_schema = 'your_database_name'
AND table_type = 'BASE TABLE';

SHOW CREATE TABLE의 결과에서 DEFAULT CHARSET=utf8와 같이 표시된다면 해당 테이블은 변환이 필요합니다.

더 나아가 특정 컬럼들만 다른 캐릭터셋을 가질 수도 있습니다.


SELECT
    column_name AS 'Column',
    character_set_name AS 'Charset',
    collation_name AS 'Collation'
FROM information_schema.columns
WHERE table_schema = 'your_database_name' AND table_name = 'your_table_name'
AND character_set_name IS NOT NULL;

문자열을 저장하는 컬럼(VARCHAR, TEXT, CHAR 등)들이 모두 utf8mb4로 설정되어 있는지 확인해야 합니다.

2단계: MySQL 서버 설정 변경 (my.cnf)

진단이 끝났다면 이제 실질적인 변경 작업에 들어갑니다. 가장 근본적인 해결책은 MySQL 서버 자체의 기본 설정을 utf8mb4로 바꾸는 것입니다. 이를 위해 MySQL 설정 파일(my.cnf 또는 my.ini)을 수정해야 합니다.

설정 파일 위치

  • Ubuntu / Debian: /etc/mysql/my.cnf, /etc/mysql/mysql.conf.d/mysqld.cnf
  • CentOS / RHEL / Fedora: /etc/my.cnf
  • Windows: MySQL 설치 경로의 my.ini
  • Docker: 커스텀 설정 파일을 만들어 볼륨 마운트하거나, `command`에 옵션을 추가합니다.

설정 파일 수정

에디터로 설정 파일을 열고, 다음 내용을 각 섹션에 추가하거나 기존 내용을 수정합니다.


[client]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4

[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

각 설정의 의미는 다음과 같습니다.

  • [client]: mysql 클라이언트 프로그램 등 MySQL 서버에 접속하는 클라이언트 애플리케이션의 기본 캐릭터셋을 지정합니다.
  • [mysql]: 터미널에서 사용하는 mysql 커맨드라인 클라이언트의 기본 캐릭터셋을 지정합니다.
  • [mysqld]: MySQL 서버 데몬(daemon) 자체의 설정을 지정합니다.
    • character-set-server: 서버의 기본 캐릭터셋입니다. 데이터베이스 생성 시 명시하지 않으면 이 값이 기본값으로 사용됩니다.
    • collation-server: 서버의 기본 콜레이션입니다.
    • character-set-client-handshake = FALSE: 이 옵션은 클라이언트가 연결 시 보내는 캐릭터셋 설정을 무시하고, 서버의 character-set-server 설정을 강제하도록 합니다. 연결 단계에서의 혼란을 줄여주므로 설정하는 것이 좋습니다. (MySQL 8.0 이상에서는 Deprecated 되었으며 기본 동작에 통합되었습니다.)

중요: 설정을 변경한 후에는 반드시 MySQL 서비스를 재시작해야 변경 내용이 적용됩니다.


# systemd를 사용하는 시스템 (Ubuntu 16.04+, CentOS 7+)
sudo systemctl restart mysql

# init.d를 사용하는 구형 시스템
sudo service mysql restart

3단계: 기존 데이터베이스, 테이블, 컬럼 변환

서버 설정을 변경했다고 해서 기존 데이터가 마법처럼 바뀌지는 않습니다. 이미 생성된 데이터베이스, 테이블, 컬럼은 수동으로 utf8mb4로 변환해주어야 합니다.

데이터베이스 변환


ALTER DATABASE your_database_name
    CHARACTER SET = utf8mb4
    COLLATE = utf8mb4_unicode_ci;

이 작업은 데이터베이스의 기본값을 변경하여, 앞으로 이 데이터베이스에 새로 생성되는 테이블이 utf8mb4를 갖도록 합니다.

테이블 및 데이터 변환

가장 중요한 단계입니다. CONVERT TO 구문을 사용해야 테이블의 기본 캐릭터셋뿐만 아니라, 그 안에 있는 모든 문자열 컬럼의 데이터까지 utf8mb4로 변환됩니다.


ALTER TABLE your_table_name
    CONVERT TO CHARACTER SET utf8mb4
    COLLATE utf8mb4_unicode_ci;

데이터베이스의 모든 테이블에 대해 이 작업을 반복 수행해야 합니다. 셸 스크립트나 간단한 프로그래밍 코드를 작성하여 자동화할 수 있습니다.

🚨 잠재적 문제: 인덱스 길이 제한 (Index Key Prefix Length)

utf8에서 utf8mb4로 전환할 때 가장 흔하게 마주치는 장애물은 인덱스 길이 제한 문제입니다. InnoDB 스토리지 엔진에서 단일 컬럼 인덱스의 최대 크기는 기본적으로 767바이트입니다.

  • utf8 (utf8mb3)의 경우: VARCHAR(255) 컬럼은 인덱스를 생성할 때 255 * 3 = 765 바이트를 차지하므로, 767바이트 제한 내에 들어옵니다.
  • utf8mb4의 경우: 같은 VARCHAR(255) 컬럼은 최대 255 * 4 = 1020 바이트를 차지할 수 있으므로, 767바이트 제한을 초과하여 ERROR 1071 (42000): Specified key was too long; max key length is 767 bytes 오류를 발생시킵니다.

이 문제를 해결하기 위해서는 다음 설정을 확인하고 적용해야 합니다. (MySQL 5.7.7 이상 버전에서는 대부분 기본값으로 활성화되어 있습니다.)


[mysqld]
innodb_file_format=Barracuda
innodb_file_per_table=1
innodb_large_prefix=1
  • innodb_file_per_table=1: 각 테이블을 별도의 .ibd 파일에 저장합니다. (기본값)
  • innodb_file_format=Barracuda: 새로운 `Barracuda` 파일 형식을 사용하도록 합니다. 이 형식은 압축이나 동적 행 형식 등 향상된 기능을 지원합니다.
  • innodb_large_prefix=1: 인덱스 키 프리픽스 길이를 767바이트에서 3072바이트까지 확장할 수 있도록 허용합니다.

위 설정을 my.cnf에 추가하고 MySQL을 재시작한 뒤, 테이블 생성 시 `ROW_FORMAT=DYNAMIC` 또는 `ROW_FORMAT=COMPRESSED` 옵션을 추가하면 문제가 해결됩니다. CONVERT TO 구문은 자동으로 이 과정을 처리해주는 경우가 많습니다.

4단계: 애플리케이션 연결 캐릭터셋 설정 확인

마지막 관문은 애플리케이션입니다. 서버와 데이터베이스가 모두 utf8mb4를 사용하도록 완벽하게 설정되었더라도, 애플리케이션이 데이터베이스에 연결할 때 '나는 utf8로 통신할거야' 라고 선언해버리면 모든 노력이 수포로 돌아갑니다. 데이터는 클라이언트-커넥션-서버를 거치면서 변환되는데, 이 과정에서 4바이트 문자가 유실될 수 있습니다.

따라서 사용하는 프로그래밍 언어나 프레임워크의 데이터베이스 연결 설정에서 캐릭터셋을 반드시 utf8mb4로 명시해주어야 합니다.

Java (JDBC)

JDBC 연결 URL에 `characterEncoding` 파라미터를 추가합니다. 값은 `utf-8`이 아닌 `utf8mb4`로 명시하는 것이 좋습니다. 하지만 일부 드라이버는 `UTF-8`로 지정해야 `utf8mb4`를 제대로 인식하기도 합니다.


jdbc:mysql://localhost:3306/your_database_name?characterEncoding=utf8mb4&serverTimezone=UTC

Python (PyMySQL, mysql-connector-python)

연결 파라미터에 `charset`을 명시합니다.


import pymysql
connection = pymysql.connect(
    host='localhost',
    user='user',
    password='password',
    database='your_database_name',
    charset='utf8mb4', # <-- 중요!
    cursorclass=pymysql.cursors.DictCursor
)

Node.js (mysql2)

`mysql2` 라이브러리를 사용할 때 연결 옵션에 `charset`을 지정합니다.


const mysql = require('mysql2');
const connection = mysql.createConnection({
  host: 'localhost',
  user: 'user',
  database: 'your_database_name',
  password: 'password',
  charset: 'utf8mb4' // <-- 중요!
});

PHP (PDO)

DSN(Data Source Name) 문자열에 `charset`을 포함시킵니다.


 PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];

try {
     $pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
     throw new \PDOException($e->getMessage(), (int)$e->getCode());
}
?>

3. 콜레이션(Collation)에 대한 깊은 이해: 무엇을 선택해야 할까?

캐릭터셋을 `utf8mb4`로 결정했다면, 그에 맞는 콜레이션도 선택해야 합니다. 콜레이션은 문자를 정렬(ORDER BY)하거나 비교(WHERE)할 때 사용하는 규칙의 집합입니다. utf8mb4에는 여러 콜레이션이 있지만, 주로 다음 두 가지(또는 최신 버전의 경우 세 가지)가 고려됩니다.

  • utf8mb4_general_ci:
    • 성능: 빠릅니다. 문자를 비교하기 위한 규칙이 단순화되어 있어 속도 면에서 이점이 있습니다.
    • 정확성: 정확성이 다소 떨어집니다. 예를 들어, 일부 유럽 언어에서 특정 문자들이 동일하게 취급되는 등 언어학적으로 완벽한 정렬을 보장하지 않습니다.
    • 과거에는 많이 사용되었지만, 지금은 특별한 이유가 없다면 권장되지 않습니다.
  • utf8mb4_unicode_ci:
    • 성능: general_ci에 비해 아주 약간 느립니다. 하지만 현대의 하드웨어에서는 그 차이를 체감하기 어렵습니다.
    • 정확성: 국제 표준인 UCA(Unicode Collation Algorithm)를 기반으로 하여 매우 정확한 정렬과 비교를 지원합니다.
    • 대부분의 경우에 권장되는 표준적인 선택입니다.
  • utf8mb4_0900_ai_ci:
    • MySQL 8.0부터 기본 콜레이션이 된 최신 버전입니다. (Unicode 9.0 기반)
    • unicode_ci보다 더 정확하고 빠릅니다. 특히 다국어 환경에서 더 나은 성능과 정확성을 보입니다.
    • ai는 'accent-insensitive'(악센트 부호 무시), ci는 'case-insensitive'(대소문자 무시)를 의미합니다.
    • MySQL 8.0 이상을 사용한다면, 이 콜레이션을 사용하는 것이 가장 좋습니다.

4. 최종 검증 및 결론

위의 모든 단계를 마쳤다면, 이제 실제로 이모지가 잘 저장되는지 테스트해볼 차례입니다. 4바이트를 사용하는 대표적인 이모지(예: 🤔, 🐘)를 데이터베이스에 직접 INSERT 해보고, SELECT 했을 때 깨지지 않고 그대로 나오는지 확인합니다.


-- 테스트 테이블 생성
CREATE TABLE emoji_test (
    id INT AUTO_INCREMENT PRIMARY KEY,
    content VARCHAR(100)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 이모지 데이터 삽입
INSERT INTO emoji_test (content) VALUES ('Hello World! 🤔');
INSERT INTO emoji_test (content) VALUES ('코끼리 이모지: 🐘');

-- 데이터 조회
SELECT * FROM emoji_test;

위 쿼리가 오류 없이 실행되고, 조회 결과에서 이모지가 정상적으로 보인다면 성공적으로 전환을 마친 것입니다.


MySQL에서 이모지 깨짐 현상은 단순한 해프닝이 아니라, 데이터 무결성과 직결되는 중요한 문제입니다. 사용자가 입력한 소중한 데이터가 물음표로 변하는 순간, 애플리케이션의 신뢰도는 떨어질 수밖에 없습니다.

핵심은 "서버, 데이터베이스, 테이블, 컬럼, 그리고 애플리케이션 연결까지 모든 계층에서 일관되게 `utf8mb4`를 사용한다"는 원칙을 기억하는 것입니다. 이 글에서 제시한 단계별 진단, 설정, 변환, 검증 과정을 차근차근 따라간다면, 더 이상 데이터베이스에서 깨진 문자를 마주하는 일 없이 안정적으로 서비스를 운영할 수 있을 것입니다. 지금 바로 여러분의 데이터베이스 캐릭터셋 설정을 점검해보세요.


1 comment: