카테고리 : MySQL/기술노트

문자셋과 정렬 규칙(Charset/collation) 변경 절차: utf8mb4 전환과 인덱스 길이

MySQL의 utf8mb4 전환에서 연결 문자셋, 정렬 규칙 충돌, 인덱스 바이트 한도와 DDL 재구축을 검증하고 안전한 변경 절차를 정리한다.

저자: MySQL 기술 노트 작성: 2026.10.17 약 10분 5,966자
다운로드

문자셋 변경은 이모지를 저장할 수 있게 만드는 설정 변경만이 아니다. 저장 가능한 문자 범위, 문자열의 동등성, 인덱스 키의 최대 바이트 수, 애플리케이션의 연결 계약을 함께 바꾸는 스키마 마이그레이션이다. DDL이 끝나도 검색 결과나 고유성 판정이 달라지면 업무적으로는 실패일 수 있다.

이 글은 MySQL 8.0 이상 InnoDB를 기준으로 utf8mb3에서 utf8mb4로 전환하는 절차를 설명한다. 예제는 폐기 가능한 MySQL 8.0 인스턴스에서 실행하며, 과거 문자셋은 전환 전 상태를 만들기 위해서만 사용한다. SQL은 운영 테이블이 아닌 별도 실습 데이터베이스에서 실행한다. Aurora의 관리형 파라미터 적용과 운영 부하는 별도 검증 대상이다.

1. 문자셋, 정렬 규칙, 연결 설정은 서로 다른 계약이다

문자셋(character set)은 문자를 표현하는 인코딩과 지원 범위를 정한다. utf8mb3는 문자당 최대 3바이트로 BMP 범위의 문자를 표현하며 보조 평면 문자는 지원하지 않는다. utf8mb4는 최대 4바이트를 사용해 그 범위까지 지원한다. utf8라는 모호한 이름에 의존하지 말고 신규 정의에는 utf8mb4를 명시한다.

정렬 규칙(collation)은 인코딩된 문자열을 어떻게 비교하고 정렬하는지 정한다. _ai_ci는 악센트와 대소문자를 구별하지 않는 규칙을 나타낸다. 따라서 같은 바이트를 보존해도 =, GROUP BY, DISTINCT, ORDER BY, UNIQUE의 결과가 달라질 수 있다. 정렬 규칙 변경은 단순한 표시 순서 변경이 아니다.

연결 문자셋은 클라이언트가 보낸 바이트를 서버가 어떤 문자로 해석하며 결과를 어떤 인코딩으로 돌려주는지에 관여한다. 테이블이 utf8mb4여도 드라이버 연결이 잘못되면 저장 전부터 값이 깨질 수 있다. SET NAMES는 실습 연결을 명확하게 만드는 도구이지 테이블 변환 명령이 아니다. 실제 서비스에서는 드라이버의 문자셋 설정과 연결 풀 초기화·재연결까지 함께 시험해야 한다.

서버 → 데이터베이스 → 테이블 → 컬럼의 기본값은 주로 객체 생성 시 정의를 결정한다. 상위 기본값을 바꾸어도 기존 컬럼의 값과 정의가 일괄 변환되는 것은 아니다. 또한 컬럼에 명시된 정렬 규칙, SQL의 명시적 COLLATE, 비교식의 collation coercibility가 실제 비교에 영향을 준다. 연결 기본값 하나만 보고 모든 쿼리의 비교 규칙을 판단하지 않는다.

비교 의미를 먼저 고정한다

아래 예제는 저장 데이터와 무관하게 대소문자 및 후행 공백 비교의 차이를 확인한다. _bin이라고 해서 항상 후행 공백까지 구별하는 것은 아니다. PAD_ATTRIBUTE를 별도로 보아야 한다.

SET NAMES utf8mb4;
SELECT @@innodb_page_size AS page_bytes,
       @@character_set_client AS client_charset,
       @@character_set_connection AS connection_charset;
SELECT COLLATION_NAME, PAD_ATTRIBUTE
FROM information_schema.COLLATIONS
WHERE COLLATION_NAME IN ('utf8mb4_bin', 'utf8mb4_0900_ai_ci')
ORDER BY COLLATION_NAME;
SELECT _utf8mb4'A' COLLATE utf8mb4_bin = _utf8mb4'a' AS old_case_equal,
       _utf8mb4'A' COLLATE utf8mb4_0900_ai_ci = _utf8mb4'a' AS new_case_equal,
       _utf8mb4'x' COLLATE utf8mb4_bin = _utf8mb4'x ' AS old_pad_equal,
       _utf8mb4'x' COLLATE utf8mb4_0900_ai_ci = _utf8mb4'x ' AS new_pad_equal;

실행 결과(MySQL 8.0.x):

mysql> SET NAMES utf8mb4;

Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @@innodb_page_size AS page_bytes,
    ->        @@character_set_client AS client_charset,
    ->        @@character_set_connection AS connection_charset;

+------------+----------------+--------------------+
| page_bytes | client_charset | connection_charset |
+------------+----------------+--------------------+
|      16384 | utf8mb4        | utf8mb4            |
+------------+----------------+--------------------+
1 row in set (0.00 sec)

mysql> SELECT COLLATION_NAME, PAD_ATTRIBUTE
    -> FROM information_schema.COLLATIONS
    -> WHERE COLLATION_NAME IN ('utf8mb4_bin', 'utf8mb4_0900_ai_ci')
    -> ORDER BY COLLATION_NAME;

+--------------------+---------------+
| COLLATION_NAME     | PAD_ATTRIBUTE |
+--------------------+---------------+
| utf8mb4_0900_ai_ci | NO PAD        |
| utf8mb4_bin        | PAD SPACE     |
+--------------------+---------------+
2 rows in set (0.00 sec)

mysql> SELECT _utf8mb4'A' COLLATE utf8mb4_bin = _utf8mb4'a' AS old_case_equal,
    ->        _utf8mb4'A' COLLATE utf8mb4_0900_ai_ci = _utf8mb4'a' AS new_case_equal,
    ->        _utf8mb4'x' COLLATE utf8mb4_bin = _utf8mb4'x ' AS old_pad_equal,
    ->        _utf8mb4'x' COLLATE utf8mb4_0900_ai_ci = _utf8mb4'x ' AS new_pad_equal;

+----------------+----------------+---------------+---------------+
| old_case_equal | new_case_equal | old_pad_equal | new_pad_equal |
+----------------+----------------+---------------+---------------+
|              0 |              1 |             1 |             0 |
+----------------+----------------+---------------+---------------+
1 row in set (0.00 sec)

utf8mb4_0900_ai_ci는 MySQL 8.0의 UCA 9.0 기반 규칙이며 NO PAD이다. 기존 규칙에서 같던 후행 공백 포함 문자열이 새 규칙에서는 달라질 수 있고, 반대로 대소문자만 다르던 값은 같아질 수 있다. 계정명, 외부 식별자, 쿠폰 코드처럼 비교 의미가 중요한 컬럼은 일괄 변환 대상에서 분리해 업무 규칙을 먼저 확정한다.

2. 기본값 변경과 실제 데이터 변환을 구분한다

다음 재현은 한글을 담은 기존 컬럼에 테이블 기본값만 바꾼 경우와 CONVERT TO CHARACTER SET을 실행한 경우를 비교한다. 마지막에는 4바이트 문자를 추가하고 HEX()와 바이트 길이로 실제 저장을 확인한다.

SET NAMES utf8mb4;
CREATE TABLE charset_demo (
  id INT PRIMARY KEY,
  label VARCHAR(40) NOT NULL
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC
  DEFAULT CHARACTER SET utf8mb3 COLLATE utf8mb3_bin;
INSERT INTO charset_demo VALUES (1, '한글');
ALTER TABLE charset_demo
  DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
SELECT CHARACTER_SET_NAME, COLLATION_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'charset_demo'
  AND COLUMN_NAME = 'label';
ALTER TABLE charset_demo
  CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
  ALGORITHM=COPY;
INSERT INTO charset_demo VALUES (2, CONVERT(0xF09F9880 USING utf8mb4));
SELECT CHARACTER_SET_NAME, COLLATION_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'charset_demo'
  AND COLUMN_NAME = 'label';
SELECT id, HEX(label) AS stored_hex,
       CHAR_LENGTH(label) AS characters_count,
       OCTET_LENGTH(label) AS stored_bytes
FROM charset_demo ORDER BY id;
DROP TABLE charset_demo;

실행 결과(MySQL 8.0.x):

핵심 명령과 결과를 발췌했다. 준비·정리 및 반복 조회 명령은 줄였으며, 실제 검증 버전은 MySQL 8.0.46이다.

mysql> ALTER TABLE charset_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
Query OK, 0 rows affected (0.00 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> SELECT CHARACTER_SET_NAME, COLLATION_NAME ...;
| CHARACTER_SET_NAME | COLLATION_NAME |
| utf8mb3            | utf8mb3_bin    |
1 row in set (0.00 sec)

mysql> ALTER TABLE charset_demo CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, ALGORITHM=COPY;
Query OK, 1 row affected (0.01 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> INSERT INTO charset_demo VALUES (2, CONVERT(0xF09F9880 USING utf8mb4));
Query OK, 1 row affected (0.00 sec)

mysql> SELECT CHARACTER_SET_NAME, COLLATION_NAME ...;
| CHARACTER_SET_NAME | COLLATION_NAME     |
| utf8mb4            | utf8mb4_0900_ai_ci |
1 row in set (0.00 sec)

mysql> SELECT id, HEX(label) AS stored_hex, CHAR_LENGTH(label) AS characters_count, OCTET_LENGTH(label) AS stored_bytes FROM charset_demo ORDER BY id;
| id | stored_hex   | characters_count | stored_bytes |
|  1 | ED959CEAB880 |                2 |            6 |
|  2 | F09F9880     |                1 |            4 |
2 rows in set (0.00 sec)

이 예제의 COPY는 재구축을 명시하기 위한 선택이다. 문자셋 변환을 INSTANT 메타데이터 변경으로 가정해서는 안 된다. 동일 문자셋 안의 일부 변경은 세부 조건에 따라 처리 경로가 달라질 수 있으므로, 실제 DDL·버전·인덱스 구성에 맞춰 지원 알고리즘을 확인한다. 자동 선택에 기대어 운영 중 예상하지 못한 긴 복사가 시작되는 일을 피해야 한다.

CONVERT TO CHARACTER SET은 문자열 컬럼과 테이블 기본값을 함께 바꾼다. 반면 특정 컬럼만 MODIFY하면 다른 컬럼이나 테이블 기본값까지 같은 정책으로 바뀌지는 않는다. MODIFY에는 NOT NULL, DEFAULT, COMMENT 등 유지할 정의를 정확히 다시 적어야 한다.

기존 데이터가 올바른 문자로 저장되어 있는지도 선행 조건이다. 예를 들어 UTF-8 바이트를 latin1 컬럼에 잘못 저장한 데이터를 정상적인 latin1 문자로 간주해 변환하면 손상이 더 심해질 수 있다. 화면만 보지 말고 알려진 원본 값과 HEX(), 드라이버 설정을 대조한다. 바이트 재해석을 위한 바이너리 경유 변환은 정상 문자 변환과 다른 복구 작업이며, 전체 테이블에 추측으로 적용하지 않는다.

3. 정렬 규칙 충돌은 데이터 정리 문제다

기존 UNIQUE 인덱스가 통과했다고 해서 새 collation에서도 고유하다는 뜻은 아니다. 아래에서는 A와 a가 구별되는 상태에서 목표 규칙으로 충돌을 조회하고, 실제 변환이 오류 1062로 거부되는지 확인한다. 저장 프로시저의 핸들러는 의도한 오류만 포착하기 위한 실습 장치이며 오류를 무시하고 배포하라는 패턴이 아니다.

SET NAMES utf8mb4;
CREATE TABLE collision_demo (
  id INT PRIMARY KEY,
  code VARCHAR(20) NOT NULL,
  UNIQUE KEY uq_code (code)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
INSERT INTO collision_demo VALUES (1, 'A'), (2, 'a');
SELECT code COLLATE utf8mb4_0900_ai_ci AS target_key,
       COUNT(*) AS duplicate_count,
       GROUP_CONCAT(id ORDER BY id) AS ids
FROM collision_demo
GROUP BY code COLLATE utf8mb4_0900_ai_ci
HAVING COUNT(*) > 1;
DELIMITER //
CREATE PROCEDURE try_collision()
BEGIN
  DECLARE rejected INT DEFAULT 0;
  DECLARE CONTINUE HANDLER FOR 1062 SET rejected = 1062;
  ALTER TABLE collision_demo
    CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
    ALGORITHM=COPY;
  SELECT rejected AS caught_errno;
END//
DELIMITER ;
CALL try_collision();
SELECT COUNT(*) AS retained_rows,
       COUNT(DISTINCT code) AS old_distinct_count
FROM collision_demo;
DROP PROCEDURE try_collision;
DROP TABLE collision_demo;

실행 결과(MySQL 8.0.x):

준비·정리와 프로시저 정의 출력을 생략하고, 조회 명령은 줄였다. 표의 데이터 행은 모두 표시했다.

mysql> INSERT INTO collision_demo VALUES (1, 'A'), (2, 'a');
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> SELECT code COLLATE utf8mb4_0900_ai_ci AS target_key, COUNT(*) AS duplicate_count, GROUP_CONCAT(id ORDER BY id) AS ids ...;
| target_key | duplicate_count | ids  |
| A          |               2 | 1,2  |
1 row in set (0.00 sec)

mysql> CALL try_collision();
| caught_errno |
|         1062 |
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT COUNT(*) AS retained_rows, COUNT(DISTINCT code) AS old_distinct_count FROM collision_demo;
| retained_rows | old_distinct_count |
|             2 |                  2 |
1 row in set (0.00 sec)

여기서 CALL의 Query OK는 프로시저 호출이 완료되었다는 뜻이다. 내부 ALTER TABLE의 성공을 뜻하지 않으며, caught_errno = 1062가 변환 거부를 보여준다. 기존 두 행과 기존 비교 규칙의 구별도 유지되었다.

원본이 다른 문자셋이면 목표 비교식도 CONVERT(code USING utf8mb4) COLLATE ...처럼 변환 후 비교해야 한다. 복합 UNIQUE는 모든 구성 컬럼의 목표 표현으로 묶어 점검한다. NULL을 허용하는 고유 키는 NULL의 UNIQUE 의미까지 고려해야 하므로 단순 GROUP BY의 중복 수를 그대로 위반 수로 해석하지 않는다.

충돌 행의 삭제나 이름 변경은 기술적 자동 정리로 결정할 수 없다. 업무 담당자가 병합·이름 변경·대소문자 구별 유지 중 하나를 결정하고 참조 데이터까지 정합성을 맞춰야 한다. 대량 사전 점검에는 전체 스캔과 정렬 비용이 들며, 조회 이후 새 충돌이 유입될 수 있다. 따라서 최종 쓰기 제어 또는 목표 규칙을 적용한 입력 검증과 마지막 재점검이 필요하다.

4. 인덱스 길이는 문자 수가 아니라 최대 바이트로 판정한다

현재 값이 ASCII 한 글자여도 컬럼이 허용하는 최대 키 길이가 한도를 넘으면 DDL이 거부될 수 있다. 기본 16KiB 페이지에서 InnoDB DYNAMIC·COMPRESSED의 키 길이 한도는 3072바이트이다. COMPACT·REDUNDANT는 767바이트 한도를 가지며, 페이지 크기가 8KiB 또는 4KiB이면 전자의 한도도 각각 1536·768바이트로 줄어든다. 버전 이름만 보고 3072를 고정 상수처럼 적용하지 않는다.

따라서 VARCHAR(800) 단일 전체 인덱스는 utf8mb3에서 최대 2400바이트지만 utf8mb4에서는 3200바이트가 된다. 다음 실험은 짧은 데이터 한 행뿐이어도 변환이 거부되는지 확인한다. 이후 길이를 768로 줄이고 실제 전환한다. 768로의 축소는 실습 데이터가 짧아서 가능한 조치이며 운영의 일괄 처방이 아니다.

SET NAMES utf8mb4;
CREATE TABLE key_limit_demo (
  id INT PRIMARY KEY,
  token VARCHAR(800) NOT NULL,
  KEY ix_token (token)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC
  DEFAULT CHARACTER SET utf8mb3 COLLATE utf8mb3_bin;
INSERT INTO key_limit_demo VALUES (1, 'short');
SELECT 800 * 3 AS old_max_bytes, 800 * 4 AS target_max_bytes,
       768 * 4 AS narrowed_max_bytes,
       MAX(CHAR_LENGTH(token)) AS actual_max_characters
FROM key_limit_demo;
DELIMITER //
CREATE PROCEDURE try_key_limit()
BEGIN
  DECLARE rejected INT DEFAULT 0;
  DECLARE CONTINUE HANDLER FOR 1071 SET rejected = 1071;
  ALTER TABLE key_limit_demo
    CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
    ALGORITHM=COPY;
  SELECT rejected AS caught_errno;
END//
DELIMITER ;
CALL try_key_limit();
ALTER TABLE key_limit_demo
  MODIFY token VARCHAR(768) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin NOT NULL,
  ALGORITHM=COPY;
ALTER TABLE key_limit_demo
  CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
  ALGORITHM=COPY;
SELECT COLUMN_TYPE, CHARACTER_SET_NAME, CHARACTER_OCTET_LENGTH
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'key_limit_demo'
  AND COLUMN_NAME = 'token';
SELECT INDEX_NAME, COLUMN_NAME, SUB_PART
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'key_limit_demo'
  AND INDEX_NAME = 'ix_token';
DROP PROCEDURE try_key_limit;
DROP TABLE key_limit_demo;

실행 결과(MySQL 8.0.x):

핵심 계산·거부·전환 결과를 발췌했다. 준비·정리, 프로시저 정의, utf8mb3 길이 축소의 출력은 생략하고 조회 명령은 줄였다.

mysql> SELECT 800 * 3 AS old_max_bytes, 800 * 4 AS target_max_bytes, 768 * 4 AS narrowed_max_bytes, MAX(CHAR_LENGTH(token)) AS actual_max_characters FROM key_limit_demo;
| old_max_bytes | target_max_bytes | narrowed_max_bytes | actual_max_characters |
|          2400 |             3200 |               3072 |                     5 |
1 row in set (0.00 sec)

mysql> CALL try_key_limit();
| caught_errno |
|         1071 |
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec)

mysql> ALTER TABLE key_limit_demo CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, ALGORITHM=COPY;
Query OK, 1 row affected (0.01 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> SELECT COLUMN_TYPE, CHARACTER_SET_NAME, CHARACTER_OCTET_LENGTH ...;
| COLUMN_TYPE  | CHARACTER_SET_NAME | CHARACTER_OCTET_LENGTH |
| varchar(768) | utf8mb4            |                   3072 |
1 row in set (0.00 sec)

mysql> SELECT INDEX_NAME, COLUMN_NAME, SUB_PART ...;
| INDEX_NAME | COLUMN_NAME | SUB_PART |
| ix_token   | token       |     NULL |
1 row in set (0.00 sec)

caught_errno = 1071은 처음 변환의 키 길이 초과를 확인한다. 이후 768자로 축소한 컬럼은 전환되었고, SUB_PART IS NULL인 전체 컬럼 인덱스가 유지되었다. 이 결과는 16KiB 페이지와 예제의 단일 문자열 키 조건에 한정된다.

복합 인덱스는 각 문자열 키 파트의 최대 바이트와 다른 키 파트를 함께 계산해야 한다. information_schema.STATISTICS의 SEQ_IN_INDEX, COLUMN_NAME, SUB_PART와 COLUMNS의 문자셋·길이를 조합해 목록을 만든다. 문자열 prefix 인덱스의 SUB_PART는 문자 수를 나타내므로 바이트 수와 혼동하지 않는다. 함수 인덱스나 생성 컬럼도 별도로 점검하고, 최종 판단은 실제 정의를 복제한 테이블에서 DDL로 확인한다.

대응 선택에는 서로 다른 대가가 있다.

  • 업무 최대 길이 축소: 실제 입력 계약을 바꿀 수 있을 때만 선택한다. 기존 최댓값뿐 아니라 앞으로 허용할 입력도 확인한다.
  • prefix 인덱스: 검색 후보를 줄이는 용도로 검토한다. 선택도가 떨어지며 전체 문자열을 커버하지 못할 수 있다. UNIQUE prefix는 전체 문자열이 다른 값도 같은 접두부 때문에 거부하므로 기존 고유성과 같지 않다.
  • 인덱스 재설계: 중복 인덱스 제거, 짧은 식별 키 도입, 검색 경로 변경을 검토한다. 해시 컬럼은 충돌 처리와 원문 비교 설계 없이 원래 UNIQUE의 대체물로 취급하지 않는다.
  • 레거시 행 형식 개선: 767바이트 제약이라면 행 형식 변경을 검토하되 테이블 재구축 비용과 페이지 조건도 함께 확인한다. 모든 컬럼을 191자로 줄이는 관행은 현대 InnoDB의 보편적인 요구가 아니다.

실제 저장량이 모든 행에서 일률적으로 증가하는 것도 아니다. 이미 UTF-8로 표현되는 한글의 바이트 수는 전환만으로 늘지 않을 수 있다. 그러나 지원하는 최댓값과 실제 입력 분포가 달라지고 인덱스 재구축·복사 공간이 필요하므로, 저장량 예측과 DDL 가능 여부 계산을 분리한다.

5. TEXT가 MEDIUMTEXT로 바뀌는 것도 변경 범위에 포함된다

CONVERT TO CHARACTER SET은 원래 컬럼이 수용할 수 있던 문자 수를 보존하기 위해 자료형을 넓힐 수 있다. 특히 latin1 TEXT를 utf8mb4로 바꾸면 MEDIUMTEXT가 될 수 있다. 아래는 길이가 짧은 값 하나만 있어도 정의가 변경되는 재현이다.

CREATE TABLE text_width_demo (
  id INT PRIMARY KEY,
  body TEXT
) ENGINE=InnoDB DEFAULT CHARACTER SET latin1;
INSERT INTO text_width_demo VALUES (1, 'abc');
ALTER TABLE text_width_demo
  CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
  ALGORITHM=COPY;
SELECT DATA_TYPE, CHARACTER_SET_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'text_width_demo'
  AND COLUMN_NAME = 'body';
SELECT id, HEX(body) AS stored_hex FROM text_width_demo;
DROP TABLE text_width_demo;

실행 결과(MySQL 8.0.x):

mysql> CREATE TABLE text_width_demo (
    ->   id INT PRIMARY KEY,
    ->   body TEXT
    -> ) ENGINE=InnoDB DEFAULT CHARACTER SET latin1;

Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO text_width_demo VALUES (1, 'abc');

Query OK, 1 row affected (0.00 sec)

mysql> ALTER TABLE text_width_demo
    ->   CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
    ->   ALGORITHM=COPY;

Query OK, 1 row affected (0.01 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> SELECT DATA_TYPE, CHARACTER_SET_NAME
    -> FROM information_schema.COLUMNS
    -> WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'text_width_demo'
    ->   AND COLUMN_NAME = 'body';

+------------+--------------------+
| DATA_TYPE  | CHARACTER_SET_NAME |
+------------+--------------------+
| mediumtext | utf8mb4            |
+------------+--------------------+
1 row in set (0.00 sec)

mysql> SELECT id, HEX(body) AS stored_hex FROM text_width_demo;

+----+------------+
| id | stored_hex |
+----+------------+
|  1 | 616263     |
+----+------------+
1 row in set (0.00 sec)

mysql> DROP TABLE text_width_demo;

Query OK, 0 rows affected (0.00 sec)

자료형 유지가 필요하면 컬럼별 MODIFY를 설계하고 모든 값이 목표 바이트 한도에 들어가는지 검증한다. 자료형 자동 확대를 무조건 장애로 볼 필요는 없지만 ORM 스키마 비교, 덤프 정의, 애플리케이션의 길이 제한과 불일치를 만들 수 있다. 전후 SHOW CREATE TABLE을 비교해 문자셋만이 아니라 타입·기본값·인덱스·외래 키의 최종 계약을 승인해야 한다.

6. 운영 전환은 의미 검증과 실행 자원 검증을 분리한다

flowchart TD
  A[문자셋과 연결 및 인덱스 목록 수집] --> B[목표 비교 규칙과 입력 계약 확정]
  B --> C[중복 충돌과 최대 키 바이트 점검]
  C --> D{충돌 또는 정의 변경 미승인}
  D -->|있음| E[업무 데이터 및 인덱스 설계 수정]
  E --> C
  D -->|없음| F[복원본에서 DDL과 애플리케이션 회귀 시험]
  F --> G[쓰기 제어 및 장기 트랜잭션 해소]
  G --> H[승인한 DDL 실행 및 부하 감시]
  H --> I[연결 풀 갱신과 데이터 및 쿼리 검증]
  I --> J[복귀 가능 기간 관찰 후 종료]

실행 전

컬럼별 문자셋과 collation, 테이블 기본값과 행 형식, 인덱스 전체 정의, FK 양쪽 컬럼을 수집한다. 부모·자식 문자열 FK는 문자셋과 collation의 호환성을 함께 유지해야 한다. foreign_key_checks를 끄면 문제가 해결된다고 생각해서는 안 된다. 불가피하게 비활성화하는 계획이라면 양쪽을 함께 변환하고 참조 정합성을 별도로 검사한다. 다시 활성화하는 것만으로 과거 데이터 전체가 자동 재검사되지는 않는다.

운영 규모의 복원본에서 DDL 소요 시간, 추가 디스크·임시 공간, 쓰기 차단 구간, 복제 지연을 측정한다. COPY 작업은 대상 테이블의 동시 쓰기를 허용하는 작업으로 간주하지 않으며, 읽기 허용 범위도 실제 조건에 맞춰 검토한다. 온라인 스키마 변경 도구를 써도 목표 UNIQUE 충돌이나 잘못된 인코딩이 해결되지는 않는다. 도구의 FK·트리거·binlog 조건과 cut-over 잠금은 별도 심사 대상이다.

실행 중

장기 트랜잭션과 metadata lock 대기를 먼저 통제한다. DDL이 대기 중이면 그 뒤의 요청까지 밀리는지 관측하고, 승인한 대기 시간과 서비스 지연 한도를 넘으면 중단 여부를 결정한다. 시작 이후에는 복사 진행과 디스크 여유, I/O, replica lag, 애플리케이션 오류를 함께 본다. 실행 중 취소나 롤백 자체도 시간이 들 수 있으므로 취소를 즉시 원상복귀로 계산하지 않는다.

실행 후

행 수만 같다고 승인하지 않는다. 알려진 문자열과 바이트, 이모지 왕복 저장, 대상 UNIQUE의 고유성, 한글·악센트·대소문자·후행 공백 검색, 정렬·페이지네이션 결과를 확인한다. 정렬상 동률이 있는 페이지네이션에는 고유 키를 보조 정렬 기준으로 두고, 규칙 변경 전후로 페이지 경계가 달라질 수 있음을 시험한다. 주요 쿼리의 실행 계획과 지연도 비교한다. SQL에 임시로 넣은 CONVERT()나 COLLATE가 인덱스 접근 경로를 바꾸지 않았는지 확인한다.

DDL의 역방향 실행은 데이터 복구와 다르다. 전환 후 4바이트 문자가 유입되면 utf8mb3로 되돌릴 수 없고, 입력 규칙이나 데이터 정리로 잃은 정보도 DDL로 복원되지 않는다. 되돌리기 기준은 검증된 백업·복원 절차와 전환 후 쓰기의 재적용 계획이어야 한다. MySQL의 DDL은 일반 트랜잭션의 ROLLBACK으로 취소하는 작업도 아니다.

7. Aurora MySQL에서는 파라미터와 스키마를 별도로 관리한다

Aurora의 character_set_server, collation_server 등은 해당 엔진 버전의 파라미터 그룹과 적용 방식에 맞춰 관리한다. 파라미터 변경이 기존 테이블 컬럼을 변환해 주지는 않는다. 드라이버가 협상하는 연결 설정도 별개이므로 writer와 reader에 실제 접속한 세션의 값을 확인한다.

공유 스토리지 구조가 문자셋 변환의 인덱스 재구축·비교 의미 변경·metadata lock을 없애는 것은 아니다. 운영과 같은 엔진 버전의 복원 클러스터에서 DDL, 연결 풀, reader 쿼리, 지연을 시험한다. 엔진 계열이 다른 외부 binlog 복제 대상이나 덤프 복원 대상은 utf8mb4_0900_* 지원 여부를 먼저 확인한다. 지원하지 않는 대상에 내보내기 위해 collation 이름만 치환하는 것은 동일한 비교 의미를 보장하지 않는다.

이 글의 실험은 Community MySQL 단일 서버의 정의 변경과 데이터·오류 결과를 확인한 것이다. Aurora에서의 무중단 수행, 특정 소요 시간, 복제 지연 상한을 입증한 실험은 아니다.

8. 전환 승인 체크리스트

맺음말

utf8mb4 전환의 완료 조건은 설정 이름이 바뀌는 것이 아니라, 문자가 올바르게 왕복하고 비교 의미와 인덱스 계약이 유지되며 운영상 복귀 경로가 확보되는 것이다. 기본값과 실제 변환을 구분하고, 데이터 의미 검증과 재구축 자원 검증을 각각 통과시켜야 한다. 이 원칙은 이후 인덱스 재설계와 다른 대규모 스키마 변경에서도 동일하게 적용된다.

참고 문서