---
title: "GTID consistency 제약: CREATE TABLE ... SELECT와 unsafe statement 대응"
description: "MySQL GTID consistency가 금지하는 문장과 CREATE TABLE ... SELECT의 버전별 차이, 안전한 전환 및 대응 절차를 정리한다."
tags: [ MySQL, 복제, 운영, 고가용성, DBA ]
image: "mysql-report-bg.png"
published: "2026-09-07"
updated: "2026-09-07"
author: "MySQL 기술 노트"
source_url: ""
---

GTID는 하나의 논리적 트랜잭션에 하나의 식별자를 부여한다. 이 대응 관계가 유지되어야 source와 replica가 실행 이력을 집합으로 비교하고, `SOURCE_AUTO_POSITION=1`로 누락된 트랜잭션을 정확히 요청할 수 있다. `enforce_gtid_consistency`는 이 전제를 깨뜨릴 수 있는 문장을 binary log에 기록하기 전에 차단하는 안전장치다.

운영 현장에서는 이 변수를 단순히 “GTID를 켤 때 함께 `ON`으로 설정하는 옵션”으로 취급하기 쉽다. 그러나 기존 애플리케이션에 `CREATE TABLE ... SELECT`, 트랜잭션 안의 임시 테이블 DDL, InnoDB와 MyISAM을 함께 변경하는 코드가 남아 있다면 설정 변경 순간부터 정상 업무가 오류로 바뀔 수 있다. 반대로 `WARN`에서 경고가 없었다는 이유만으로 안전하다고 결론 내리면 binary logging이 꺼져 있거나 필터로 제외된 문장을 놓칠 수 있다.

이 글은 MySQL 8.0을 기준으로 GTID consistency의 내부 논리, 버전에 따라 달라진 `CREATE TABLE ... SELECT` 제약, 대표 unsafe statement의 대응 방법, 적용 전후 점검 절차를 설명한다.

## 1. GTID consistency가 보호하는 불변식

GTID 기반 복제의 핵심 불변식은 다음과 같다.

```text
하나의 GTID = 하나의 원자적 논리 트랜잭션
```

source에서 하나의 트랜잭션이 replica에서는 둘로 나뉘거나, source의 두 트랜잭션이 replica에서는 하나로 합쳐진다면 이 대응 관계가 깨진다. GTID 집합만으로 “무엇을 실행했고 무엇이 남았는가”를 판단할 수 없게 된다.

`enforce_gtid_consistency`는 데이터 자체의 업무 정합성을 검사하지 않는다. 외래 키 위반, 잔액 불일치, 중복 주문 같은 애플리케이션 규칙을 확인하는 기능도 아니다. 이 변수의 관심사는 **문장이 GTID를 가진 binary log 트랜잭션으로 일관되게 표현될 수 있는가**다.

```mermaid
flowchart TD
    S[클라이언트 문장 또는 트랜잭션] --> B{binary log에 기록되는가}
    B -- 아니요 --> N[GTID consistency 검사 대상 아님]
    B -- 예 --> C{하나의 원자적 GTID 트랜잭션으로 표현 가능한가}
    C -- 예 --> L[binary log 기록 및 GTID 부여]
    C -- 아니요 --> M{enforce_gtid_consistency}
    M -- OFF --> O[실행 허용]
    M -- WARN --> W[실행 허용 + 경고 생성]
    M -- ON --> E[실행 거부]

    L --> R[replica에서 동일 GTID로 적용]
    O --> K[GTID 전환 또는 복제 시 위험 누적]
    W --> A[경고 수집 후 코드 개선]
    E --> F[애플리케이션 오류로 즉시 노출]
```

그림에서 가장 중요한 분기는 첫 번째다. 이 검사는 문장이 실제로 binary log에 기록될 때만 작동한다. `log_bin=OFF`인 개발 서버에서 테스트하거나, binary log filter에 의해 문장이 제외되면 운영 source에서 발생할 오류를 재현하지 못할 수 있다.

## 2. 세 가지 설정값과 운영 의미

`enforce_gtid_consistency`는 전역 동적 변수이며 `OFF`, `WARN`, `ON` 세 값을 가진다.

| 값 | unsafe statement 실행 | 운영 의미 |
|---|---|---|
| `OFF` | 허용 | 기존 동작을 유지하지만 GTID 전환 준비 상태를 보장하지 않는다. |
| `WARN` | 허용하고 경고 생성 | 호환성 조사 단계다. 경고를 수집하지 않으면 관측 효과가 없다. |
| `ON` | 오류로 거부 | GTID-safe 문장만 binary log에 기록되도록 강제한다. |

현재 서버의 기본 상태는 관련 변수를 한 번에 확인해야 한다.

```sql
SELECT VERSION() AS mysql_version,
       MAX(CASE WHEN VARIABLE_NAME = 'log_bin'
                THEN VARIABLE_VALUE END) AS log_bin,
       MAX(CASE WHEN VARIABLE_NAME = 'binlog_format'
                THEN VARIABLE_VALUE END) AS binlog_format,
       MAX(CASE WHEN VARIABLE_NAME = 'gtid_mode'
                THEN VARIABLE_VALUE END) AS gtid_mode,
       MAX(CASE WHEN VARIABLE_NAME = 'enforce_gtid_consistency'
                THEN VARIABLE_VALUE END) AS enforce_gtid_consistency,
       MAX(CASE WHEN VARIABLE_NAME = 'server_uuid'
                THEN VARIABLE_VALUE END) AS server_uuid
FROM performance_schema.global_variables
WHERE VARIABLE_NAME IN (
    'log_bin', 'binlog_format', 'gtid_mode',
    'enforce_gtid_consistency', 'server_uuid'
);
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT VERSION() AS mysql_version,
    ->        MAX(CASE WHEN VARIABLE_NAME = 'log_bin'
    ->                 THEN VARIABLE_VALUE END) AS log_bin,
    ->        MAX(CASE WHEN VARIABLE_NAME = 'binlog_format'
    ->                 THEN VARIABLE_VALUE END) AS binlog_format,
    ->        MAX(CASE WHEN VARIABLE_NAME = 'gtid_mode'
    ->                 THEN VARIABLE_VALUE END) AS gtid_mode,
    ->        MAX(CASE WHEN VARIABLE_NAME = 'enforce_gtid_consistency'
    ->                 THEN VARIABLE_VALUE END) AS enforce_gtid_consistency,
    ->        MAX(CASE WHEN VARIABLE_NAME = 'server_uuid'
    ->                 THEN VARIABLE_VALUE END) AS server_uuid
    -> FROM performance_schema.global_variables
    -> WHERE VARIABLE_NAME IN (
    ->     'log_bin', 'binlog_format', 'gtid_mode',
    ->     'enforce_gtid_consistency', 'server_uuid'
    -> );

+---------------+---------+---------------+-----------+--------------------------+--------------------------------------+
| mysql_version | log_bin | binlog_format | gtid_mode | enforce_gtid_consistency | server_uuid                          |
+---------------+---------+---------------+-----------+--------------------------+--------------------------------------+
| 8.0.46        | OFF     | ROW           | OFF       | OFF                      | e44a3215-aa4f-11f1-8c4e-0242ac110010 |
+---------------+---------+---------------+-----------+--------------------------+--------------------------------------+
1 row in set (0.00 sec)
```

`enforce_gtid_consistency=ON`만 보고 GTID가 활성화되었다고 판단해서는 안 된다. `gtid_mode`는 별도 변수이며, 온라인 전환 과정에서는 두 변수가 서로 다른 단계에 있을 수 있다. `log_bin=0`이면 unsafe statement 검사가 실질적으로 발생하지 않을 수 있다.

### 2.1 `WARN`은 호환성 모드가 아니라 조사 모드다

`WARN`은 위반 문장을 안전하게 바꾸지 않는다. 실행을 계속 허용하면서 경고만 생성한다. 따라서 다음 조건을 갖추지 않으면 조사 결과가 불완전하다.

- 실제 쓰기 트래픽이 지나가는 모든 source 후보에서 관측한다.
- 배치, 월말 작업, 스키마 배포, 관리자 수동 작업까지 관측 기간에 포함한다.
- MySQL error log의 관련 경고를 중앙 로그에서 검색하고 집계한다.
- `log_bin`, `binlog_format`, replication filter가 운영 목표와 같은지 확인한다.
- replica도 향후 승격될 수 있다면 로컬 쓰기와 운영 작업을 별도로 점검한다.

짧은 무경고 기간은 안전성 증명이 아니다. 일주일에 한 번 실행되는 보고서 CTAS나 월 1회 정산 배치가 숨어 있을 수 있다.

## 3. 어떤 문장이 GTID-unsafe인가

MySQL 8.0에서 실무적으로 먼저 확인할 범주는 세 가지다.

1. atomic DDL로 처리할 수 없는 `CREATE TABLE ... SELECT`
2. transactional engine과 nontransactional engine을 섞어 변경하는 문장 또는 트랜잭션
3. `STATEMENT` binary logging 조건에서 트랜잭션·프로시저·함수·트리거 안에 있는 임시 테이블 생성과 삭제

이 제약들은 모두 “source와 replica에서 트랜잭션 경계가 달라질 수 있는가”와 연결된다.

## 4. `CREATE TABLE ... SELECT`의 버전 경계

`CREATE TABLE ... SELECT`는 테이블 정의와 조회 결과 입력을 한 문장에 결합한다. 과거 구현에서는 DDL과 DML의 binary log 표현이 format에 따라 달라질 수 있었다.

- `STATEMENT` 형식에서는 문장 하나가 한 트랜잭션으로 기록될 수 있었다.
- `ROW` 형식에서는 테이블 생성과 행 입력이 서로 다른 트랜잭션처럼 기록될 수 있었다.
- source와 replica의 format 또는 실행 경계가 달라지면 하나의 GTID와 실제 작업 단위의 대응이 깨질 수 있었다.

이 때문에 **MySQL 8.0.21 미만에서는 GTID 사용 시 `CREATE TABLE ... SELECT`가 금지**되었다. MySQL 8.0.21부터는 atomic DDL을 지원하는 storage engine에 대해 제약이 완화되었다. 해당 조건에서는 생성과 데이터 입력이 하나의 atomic 작업으로 binary log에 기록될 수 있다.

### 4.1 최신 MySQL에서도 모든 CTAS가 안전한 것은 아니다

버전이 8.0.21 이상이라는 사실만으로 충분하지 않다. 대상 storage engine이 atomic DDL을 지원해야 한다. 일반적인 InnoDB 대상 CTAS는 허용되지만 non-atomic engine을 명시하면 여전히 거부될 수 있다.

다음 재현은 InnoDB의 atomic CTAS가 생성과 입력을 함께 완료하는지 확인한다.

```sql
DROP TABLE IF EXISTS gtid_ctas_copy;
DROP TABLE IF EXISTS gtid_ctas_source;

CREATE TABLE gtid_ctas_source (
    id BIGINT NOT NULL PRIMARY KEY,
    payload VARCHAR(100) NOT NULL
) ENGINE=InnoDB;

INSERT INTO gtid_ctas_source (id, payload)
VALUES (1, 'alpha'), (2, 'beta'), (3, 'gamma');

CREATE TABLE gtid_ctas_copy ENGINE=InnoDB
AS
SELECT id, payload
FROM gtid_ctas_source
WHERE id >= 2;

SELECT TABLE_NAME, ENGINE
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
  AND TABLE_NAME = 'gtid_ctas_copy';

SELECT id, payload
FROM gtid_ctas_copy
ORDER BY id;

DROP TABLE gtid_ctas_copy;
DROP TABLE gtid_ctas_source;
```

실행 결과(MySQL 8.0.x):

```text
mysql> DROP TABLE IF EXISTS gtid_ctas_copy;

Query OK, 0 rows affected (0.00 sec)

mysql> DROP TABLE IF EXISTS gtid_ctas_source;

Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE gtid_ctas_source (
    ->     id BIGINT NOT NULL PRIMARY KEY,
    ->     payload VARCHAR(100) NOT NULL
    -> ) ENGINE=InnoDB;

Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO gtid_ctas_source (id, payload)
    -> VALUES (1, 'alpha'), (2, 'beta'), (3, 'gamma');

Query OK, 3 rows affected (0.01 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> CREATE TABLE gtid_ctas_copy ENGINE=InnoDB
    -> AS
    -> SELECT id, payload
    -> FROM gtid_ctas_source
    -> WHERE id >= 2;

Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> SELECT TABLE_NAME, ENGINE
    -> FROM information_schema.TABLES
    -> WHERE TABLE_SCHEMA = DATABASE()
    ->   AND TABLE_NAME = 'gtid_ctas_copy';

+----------------+--------+
| TABLE_NAME     | ENGINE |
+----------------+--------+
| gtid_ctas_copy | InnoDB |
+----------------+--------+
1 row in set (0.00 sec)

mysql> SELECT id, payload
    -> FROM gtid_ctas_copy
    -> ORDER BY id;

+----+---------+
| id | payload |
+----+---------+
|  2 | beta    |
|  3 | gamma   |
+----+---------+
2 rows in set (0.00 sec)

mysql> DROP TABLE gtid_ctas_copy;

Query OK, 0 rows affected (0.00 sec)

mysql> DROP TABLE gtid_ctas_source;

Query OK, 0 rows affected (0.00 sec)
```

이 예제에서 확인할 핵심은 `gtid_ctas_copy`의 engine이 InnoDB이며, 조건에 맞는 두 행이 들어 있다는 사실이다. 실제 GTID 안전성 승인은 대상 서버가 8.0.21 이상이고 binary logging과 `enforce_gtid_consistency=ON`이 켜진 조건에서 최종 확인해야 한다.

### 4.2 실제 강제 조건에서 확인한 결과

별도 검증 인스턴스를 MySQL 8.0.46, `log_bin=ON`, `gtid_mode=ON`, `enforce_gtid_consistency=ON`, `binlog_format=ROW`로 구성해 확인했다. InnoDB CTAS는 성공했지만 MyISAM CTAS는 다음 오류로 거부되었다.

```text
mysql> CREATE TABLE bad_dst ENGINE=MyISAM AS SELECT * FROM src;
ERROR 1786 (HY000): Statement violates GTID consistency: CREATE TABLE ... SELECT.
```

이 결과를 “CTAS는 무조건 허용” 또는 “GTID에서는 CTAS가 무조건 금지”로 단순화해서는 안 된다. 판단 순서는 **서버 버전 → 대상 engine의 atomic DDL 지원 → 실제 binary logging 조건**이다.

## 5. CTAS를 두 단계로 바꾸는 안전한 대응

구버전 호환성이 필요하거나 engine의 atomic DDL 지원을 보장할 수 없다면 DDL과 DML을 명시적으로 분리한다.

```sql
DROP TABLE IF EXISTS gtid_copy_safe;
DROP TABLE IF EXISTS gtid_source_safe;

CREATE TABLE gtid_source_safe (
    id BIGINT NOT NULL PRIMARY KEY,
    payload VARCHAR(100) NOT NULL,
    created_at DATETIME NOT NULL
) ENGINE=InnoDB;

INSERT INTO gtid_source_safe (id, payload, created_at)
VALUES
    (1, 'alpha', '2026-09-01 10:00:00'),
    (2, 'beta',  '2026-09-02 10:00:00'),
    (3, 'gamma', '2026-09-03 10:00:00');

CREATE TABLE gtid_copy_safe LIKE gtid_source_safe;

INSERT INTO gtid_copy_safe (id, payload, created_at)
SELECT id, payload, created_at
FROM gtid_source_safe
WHERE created_at >= '2026-09-02 00:00:00';

SELECT COUNT(*) AS copied_rows,
       MIN(id) AS first_id,
       MAX(id) AS last_id
FROM gtid_copy_safe;

DROP TABLE gtid_copy_safe;
DROP TABLE gtid_source_safe;
```

실행 결과(MySQL 8.0.x):

```text
mysql> DROP TABLE IF EXISTS gtid_copy_safe;

Query OK, 0 rows affected (0.00 sec)

mysql> DROP TABLE IF EXISTS gtid_source_safe;

Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE gtid_source_safe (
    ->     id BIGINT NOT NULL PRIMARY KEY,
    ->     payload VARCHAR(100) NOT NULL,
    ->     created_at DATETIME NOT NULL
    -> ) ENGINE=InnoDB;

Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO gtid_source_safe (id, payload, created_at)
    -> VALUES
    ->     (1, 'alpha', '2026-09-01 10:00:00'),
    ->     (2, 'beta',  '2026-09-02 10:00:00'),
    ->     (3, 'gamma', '2026-09-03 10:00:00');

Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> CREATE TABLE gtid_copy_safe LIKE gtid_source_safe;

Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO gtid_copy_safe (id, payload, created_at)
    -> SELECT id, payload, created_at
    -> FROM gtid_source_safe
    -> WHERE created_at >= '2026-09-02 00:00:00';

Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> SELECT COUNT(*) AS copied_rows,
    ->        MIN(id) AS first_id,
    ->        MAX(id) AS last_id
    -> FROM gtid_copy_safe;

+-------------+----------+---------+
| copied_rows | first_id | last_id |
+-------------+----------+---------+
|           2 |        2 |       3 |
+-------------+----------+---------+
1 row in set (0.00 sec)

mysql> DROP TABLE gtid_copy_safe;

Query OK, 0 rows affected (0.00 sec)

mysql> DROP TABLE gtid_source_safe;

Query OK, 0 rows affected (0.01 sec)
```

이 방식은 `CREATE TABLE`과 `INSERT ... SELECT`를 서로 독립적인 작업으로 만든다. 각 작업은 GTID 경계와 자연스럽게 대응한다. 다만 CTAS의 원자성과 같지는 않다. 테이블 생성 후 데이터 입력이 실패하면 빈 테이블이 남을 수 있으므로 배포 절차가 이를 처리해야 한다.

운영 runbook에는 다음을 포함하는 편이 안전하다.

1. 목적 테이블이 없어야 한다는 사전 조건을 확인한다.
2. `CREATE TABLE ... LIKE` 뒤 필요한 인덱스·제약·테이블 옵션을 재검토한다.
3. `INSERT ... SELECT`의 대상 컬럼을 명시한다.
4. 행 수, checksum 또는 업무 집계로 복사 결과를 검증한다.
5. 실패 시 목적 테이블 보존·삭제 기준을 정한다.
6. 이름 교체가 필요하면 짧은 metadata lock 구간과 롤백 절차를 설계한다.

`CREATE TABLE ... LIKE`가 원본의 모든 속성을 업무 의도대로 복제한다고 가정해서는 안 된다. 외래 키 관계, 트리거, 권한, 파티션 설계, 자동 증가 상태 등은 별도로 확인한다.

## 6. transactional과 nontransactional engine 혼합

InnoDB는 commit과 rollback 경계를 제공하지만 MyISAM 같은 nontransactional engine은 같은 방식으로 되돌릴 수 없다. 하나의 트랜잭션에서 두 종류의 테이블을 함께 변경하면 source의 논리적 트랜잭션과 복제 이벤트의 안전한 GTID 경계를 일치시키기 어렵다.

대표적인 위험 구조는 다음과 같다. 이 코드는 실행 예제가 아니라 금지 패턴을 보여주는 구조다.

```text
START TRANSACTION;
UPDATE innodb_orders
   SET status = 'PAID'
 WHERE order_id = 1001;
INSERT INTO myisam_audit(order_id, message)
VALUES (1001, 'paid');
COMMIT;
```

MySQL 8.0.46의 실제 강제 조건에서 InnoDB 테이블을 변경한 뒤 같은 트랜잭션에서 MyISAM 테이블을 변경하면 다음 오류가 발생했다.

```text
ERROR 1785 (HY000): Statement violates GTID consistency: Updates to non-transactional tables can only be done in either autocommitted statements or single-statement transactions, and never in the same statement as updates to transactional tables.
```

### 6.1 우선 대응은 InnoDB 전환이다

가장 안정적인 해결은 영속 업무 테이블을 InnoDB로 통일하는 것이다. nontransactional 테이블 변경을 별도 autocommit 문장으로 분리하면 GTID 오류를 피할 수 있는 경우가 있지만, 원래 하나로 보이던 업무 작업의 원자성은 사라진다. 주문 변경은 성공하고 audit 기록은 실패하는 부분 완료 상태를 애플리케이션이 감당해야 한다.

현재 스키마의 engine 분포는 다음처럼 점검할 수 있다. 예제는 진단 결과를 안정적으로 확인하기 위해 샘플 InnoDB와 MyISAM 테이블을 만든 뒤 정리한다.

```sql
DROP TABLE IF EXISTS gtid_engine_innodb;
DROP TABLE IF EXISTS gtid_engine_myisam;

CREATE TABLE gtid_engine_innodb (
    id INT NOT NULL PRIMARY KEY
) ENGINE=InnoDB;

CREATE TABLE gtid_engine_myisam (
    id INT NOT NULL PRIMARY KEY
) ENGINE=MyISAM;

SELECT ENGINE, COUNT(*) AS table_count
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
  AND TABLE_NAME IN ('gtid_engine_innodb', 'gtid_engine_myisam')
GROUP BY ENGINE
ORDER BY ENGINE;

DROP TABLE gtid_engine_innodb;
DROP TABLE gtid_engine_myisam;
```

실행 결과(MySQL 8.0.x):

```text
mysql> DROP TABLE IF EXISTS gtid_engine_innodb;

Query OK, 0 rows affected (0.00 sec)

mysql> DROP TABLE IF EXISTS gtid_engine_myisam;

Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE gtid_engine_innodb (
    ->     id INT NOT NULL PRIMARY KEY
    -> ) ENGINE=InnoDB;

Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE gtid_engine_myisam (
    ->     id INT NOT NULL PRIMARY KEY
    -> ) ENGINE=MyISAM;

Query OK, 0 rows affected (0.00 sec)

mysql> SELECT ENGINE, COUNT(*) AS table_count
    -> FROM information_schema.TABLES
    -> WHERE TABLE_SCHEMA = DATABASE()
    ->   AND TABLE_NAME IN ('gtid_engine_innodb', 'gtid_engine_myisam')
    -> GROUP BY ENGINE
    -> ORDER BY ENGINE;

+--------+-------------+
| ENGINE | table_count |
+--------+-------------+
| InnoDB |           1 |
| MyISAM |           1 |
+--------+-------------+
2 rows in set (0.00 sec)

mysql> DROP TABLE gtid_engine_innodb;

Query OK, 0 rows affected (0.00 sec)

mysql> DROP TABLE gtid_engine_myisam;

Query OK, 0 rows affected (0.01 sec)
```

실제 운영 점검에서는 `TABLE_NAME IN (...)` 조건을 제거하고 업무 schema를 명시한다. 결과에 MyISAM, MEMORY 등 nontransactional engine이 있다면 단순 존재 여부뿐 아니라 다음을 조사한다.

- 해당 테이블에 쓰는 코드 경로
- InnoDB 테이블과 같은 트랜잭션에서 변경되는지
- trigger 또는 stored program을 통해 간접 변경되는지
- 장애 시 재처리와 중복 방지 방식
- InnoDB 전환 시 잠금, 디스크, 복구 시간 영향

모든 nontransactional 테이블이 즉시 위반을 일으키는 것은 아니다. 그러나 GTID 전환 작업에서는 “어떤 트랜잭션이 어떤 engine을 함께 변경하는가”까지 확인해야 한다.

## 7. 임시 테이블 DDL과 `binlog_format`

임시 테이블 제약은 binary log format과 MySQL 버전에 따라 해석해야 한다.

### 7.1 `STATEMENT` 형식

`enforce_gtid_consistency=ON`이고 `binlog_format=STATEMENT`이면 `CREATE TEMPORARY TABLE`과 `DROP TEMPORARY TABLE`을 다음 문맥에서 사용할 수 없다.

- 명시적 트랜잭션 내부
- procedure 내부의 transactional context
- function 또는 trigger처럼 실행 문맥이 트랜잭션에 결합되는 위치

트랜잭션 밖에서 `autocommit=1`인 독립 문장으로 실행하는 것은 가능하다. 실제 MySQL 8.0.46 강제 조건에서 트랜잭션 안의 생성은 다음처럼 거부되었다.

```text
ERROR 3748 (HY000): Statement violates GTID consistency: CREATE TEMPORARY TABLE and DROP TEMPORARY TABLE are not allowed inside a transaction or inside a procedure in a transactional context when @@session.binlog_format=STATEMENT.
```

### 7.2 `ROW`와 `MIXED` 형식

MySQL 8.0.13부터 `ROW` 또는 `MIXED`에서는 GTID를 사용하는 트랜잭션·stored program 안에서도 임시 테이블 생성과 삭제가 허용된다. 임시 테이블만 대상으로 하는 작업은 binary log에 기록되지 않고, 영속 테이블의 행 변경만 row event로 복제되는 방식이기 때문이다.

그렇다고 format 변경을 즉석 우회책으로 사용해서는 안 된다. 열린 임시 테이블이 있는 세션의 format 변경에는 별도 제약이 있으며, `MIXED`는 문장마다 실제 logging format이 달라질 수 있다. 운영 표준은 보통 `ROW`를 기준으로 정하고, 임시 테이블 수명과 transaction 경계를 코드에서 명확히 관리하는 편이 낫다.

### 7.3 실무 대응 순서

1. 임시 테이블을 만드는 stored procedure와 배치 코드를 검색한다.
2. `START TRANSACTION` 이전에 생성하고 `COMMIT` 이후 삭제할 수 있는지 검토한다.
3. 업무상 필요한 경우 `ROW` 기반 표준과 대상 버전을 확인한다.
4. 임시 테이블의 데이터가 replica에 그대로 존재할 것이라고 가정한 로직이 없는지 확인한다.
5. connection pool에서 임시 테이블이 세션에 남아 다음 요청을 오염시키지 않는지 점검한다.

## 8. `log_bin=OFF` 테스트가 만드는 착시

`enforce_gtid_consistency`는 binary log에 기록되는 문장을 대상으로 한다. 따라서 다음 상황에서는 `ON`이어도 위반이 드러나지 않을 수 있다.

- 서버 전체의 binary logging이 꺼져 있다.
- 세션 또는 관리 작업이 binary log 기록에서 제외되었다.
- replication filter로 해당 문장이 기록되지 않는다.
- 개발 환경의 `binlog_format`이 운영과 다르다.
- 개발 버전은 최신이지만 운영에는 8.0.21 미만 노드가 남아 있다.

SQL 검증 하네스처럼 문법과 객체 존재 여부를 확인하기 위해 `skip-log-bin`으로 기동한 인스턴스는 예제의 정상 실행을 확인할 수 있지만, GTID 강제 오류까지 증명하지는 못한다. 이 글에서는 일반 SQL 블록을 표준 하네스로 검증하고, 오류 경계는 별도의 `log_bin=ON`, GTID 강제 인스턴스에서 추가로 확인했다.

`SET SESSION sql_log_bin=0`을 애플리케이션 우회책으로 사용해서는 안 된다. 해당 변경은 replica로 전달되지 않아 데이터 차이를 만들 수 있고 높은 권한도 필요하다. 복구나 프로비저닝 절차에서 의도적으로 사용할 때는 데이터 상태와 GTID 집합을 별도로 보증하는 승인된 runbook이 필요하다.

## 9. 안전한 GTID 전환 절차

기존 binary log replication 환경에서 GTID를 온라인으로 켤 때는 즉시 `gtid_mode=ON`으로 바꾸는 것이 아니라 호환성 조사와 단계적 mode 전환을 수행한다.

### 9.1 사전 조사

먼저 topology 전체에서 다음 상태를 같은 시점에 수집한다.

```text
SELECT @@GLOBAL.log_bin;
SELECT @@GLOBAL.binlog_format;
SELECT @@GLOBAL.gtid_mode;
SELECT @@GLOBAL.enforce_gtid_consistency;
SELECT @@GLOBAL.log_replica_updates;
```

이 명령들은 운영 실행 후보이며 topology의 각 서버에서 읽기 전용으로 수행한다. 이름이 다른 구버전에서는 `log_slave_updates`가 보일 수 있으므로 대상 minor version의 변수명을 확인한다.

함께 점검할 항목은 다음과 같다.

- source, replica, 승격 후보의 MySQL minor version
- InnoDB 이외 영속 테이블과 쓰기 경로
- CTAS를 사용하는 배치와 스키마 배포 도구
- temporary table을 사용하는 stored program
- replica에서 실행하는 로컬 maintenance write
- binary log filter와 channel별 복제 설정
- 장기 트랜잭션과 배포 freeze 가능 시간

### 9.2 `WARN` 관측

모든 관련 서버에서 `enforce_gtid_consistency=WARN`을 적용하고 충분한 업무 주기를 관측한다. 경고가 나오면 문장, 호출 주체, 배치 이름, engine, transaction context를 연결해 수정한다.

```text
SET GLOBAL enforce_gtid_consistency = WARN;
```

이 명령을 실행했다는 사실만으로 재시작 후에도 유지되는 것은 아니다. MySQL 구성 파일 또는 관리형 서비스 parameter group의 영속 설정을 함께 관리한다. `SET PERSIST` 사용 여부는 조직의 구성 관리 원칙에 맞춘다.

### 9.3 강제와 mode 전환

위반 코드를 제거한 뒤 topology 전체에서 `enforce_gtid_consistency=ON`을 먼저 강제한다. 이후 공식 온라인 전환 순서에 따라 `gtid_mode`를 `OFF_PERMISSIVE`, `ON_PERMISSIVE`, `ON`으로 단계적으로 이동한다. 각 단계에서 anonymous transaction이 남아 있는지 확인하고, 모든 채널과 서버가 다음 단계 조건을 충족한 뒤 진행한다.

```text
SET GLOBAL enforce_gtid_consistency = ON;
SET GLOBAL gtid_mode = OFF_PERMISSIVE;
SET GLOBAL gtid_mode = ON_PERMISSIVE;
-- anonymous transaction 소진 확인
SET GLOBAL gtid_mode = ON;
```

위 순서는 개념적 runbook이다. 실제 변경 전에는 현재 topology, multi-source channel, 버전별 요구 조건, rollback 지점을 포함한 별도 변경 계획을 승인받아야 한다. 일부 노드만 먼저 강제하거나 mode를 건너뛰면 복제 연결과 승격 가능성이 불일치할 수 있다.

## 10. 애플리케이션과 배포 도구의 대응 패턴

### 10.1 CTAS 생성 작업

| 조건 | 권장 대응 |
|---|---|
| MySQL 8.0.21 이상 + InnoDB atomic DDL | staging에서 GTID 강제 조건으로 CTAS를 검증한다. |
| 구버전 노드가 topology에 존재 | `CREATE TABLE ... LIKE`와 `INSERT ... SELECT`로 분리한다. |
| non-atomic engine 대상 | InnoDB로 설계를 바꾸거나 DDL/DML을 분리한다. |
| 대량 데이터 복사 | replication lag, redo/binlog 증가, metadata lock, 디스크 여유를 함께 검토한다. |

CTAS가 GTID-safe하다는 것과 운영에 가볍다는 것은 다른 문제다. 대량 CTAS는 긴 실행 시간, 큰 binary log, replica apply 지연, 공간 급증을 만들 수 있다.

### 10.2 mixed-engine 업무 트랜잭션

가능하면 영속 테이블을 InnoDB로 통일한다. 분리가 불가피하면 outbox table처럼 InnoDB 트랜잭션 안에서 후속 작업 의도를 기록하고, 별도 worker가 idempotent하게 외부 또는 legacy 저장소를 갱신하는 구조를 검토한다. 단순히 `COMMIT` 앞뒤로 문장을 옮기면 실패 시 부분 완료를 복구할 책임이 애플리케이션으로 이동한다.

### 10.3 임시 테이블 기반 배치

임시 테이블 DDL을 transaction 밖으로 이동하거나, MySQL 8.0.13 이상과 `ROW` 기반 조건을 명시한다. stored procedure 내부에서 암묵적으로 transaction context가 만들어지는지도 확인한다. session 재사용 환경에서는 `DROP TEMPORARY TABLE` 누락을 장애 요인으로 취급한다.

### 10.4 오류 처리

`ERROR 1785`, `ERROR 1786`, `ERROR 3748` 같은 GTID consistency 오류를 무조건 재시도해서는 안 된다. 같은 문장을 같은 조건으로 다시 실행하면 계속 실패한다. 애플리케이션은 이를 transient lock 오류와 구분하고, 코드 또는 실행 문맥 수정이 필요한 비재시도 오류로 분류하는 편이 안전하다.

## 11. Aurora MySQL에서의 해석

Aurora MySQL cluster 내부의 writer-reader 데이터 복제는 Aurora의 분산 스토리지 계층이 담당한다. 일반 MySQL source-replica channel을 그대로 사용해 cluster 내부 reader를 유지하는 구조가 아니다. 따라서 GTID consistency는 주로 다음 경계에서 중요하다.

- 외부 MySQL과 Aurora 사이의 binary log replication
- Aurora cluster 간 binary log 기반 연계
- migration 또는 CDC 도구가 binary log를 소비하는 구성
- Aurora 밖의 replica나 downstream 시스템으로 변경을 전달하는 구성

Aurora MySQL 3는 MySQL 8.0 호환 계열이지만, 호환 minor version과 지원 기능은 Aurora engine version에 종속된다. `CREATE TABLE ... SELECT` 허용 여부를 “Aurora MySQL 3이므로 항상 최신 Community MySQL과 같다”고 가정하지 말고 다음을 실제 cluster에서 확인한다.

- `SELECT VERSION()`과 Aurora engine version
- `SHOW VARIABLES`로 본 `gtid_mode`, `enforce_gtid_consistency`, `log_bin`, `binlog_format`
- 대상 table engine과 atomic DDL 동작
- DB cluster parameter group의 적용 범위와 재시작 필요 여부
- 외부 replication 또는 CDC의 GTID 요구 조건

Aurora의 storage replication이 정상이라는 사실은 외부 binary log 소비자가 안전하다는 뜻이 아니다. 외부 연계가 있다면 binlog 보존 시간, writer failover 뒤의 연결 재수립, GTID 집합, downstream apply 상태를 별도로 관측한다.

## 12. 장애와 오해

### 12.1 “8.0에서는 CTAS가 모두 허용된다”

잘못된 해석이다. 8.0.21부터 **atomic DDL을 지원하는 engine**에 대해 제한이 완화되었다. non-atomic engine과 구버전 혼재 topology는 별도 대응이 필요하다.

### 12.2 “`enforce_gtid_consistency=ON`이면 GTID도 켜져 있다”

두 변수는 역할이 다르다. 전자는 unsafe statement 강제, 후자는 트랜잭션의 GTID 사용 mode다. 항상 두 값을 함께 확인한다.

### 12.3 “개발 서버에서 성공했으므로 운영에서도 성공한다”

개발 서버의 `log_bin=OFF`, 다른 `binlog_format`, 다른 minor version 때문에 강제 검사가 생략될 수 있다. 운영과 같은 binary logging 조건의 staging 검증이 필요하다.

### 12.4 “`WARN`에서 경고가 없었으므로 즉시 `ON`으로 바꿔도 된다”

관측 기간에 실행되지 않은 배치가 있을 수 있고, 로그 수집이 경고를 버렸을 수도 있다. 코드 검색, engine 조사, stored program 조사와 함께 판단한다.

### 12.5 “오류가 나면 binary logging만 끄면 된다”

복제되지 않는 변경과 GTID/data 불일치를 만들 수 있는 위험한 우회다. unsafe statement의 구조를 수정해야 한다.

### 12.6 “GTID-safe면 장애에도 안전하다”

GTID-safe는 복제 식별과 logging 경계의 안전성을 뜻한다. 대량 CTAS의 공간 부족, metadata lock, replica lag, 부분 완료 runbook 누락까지 해결하지는 않는다.

## 13. 적용 체크리스트

### 설정과 버전

- [ ] 모든 source, replica, 승격 후보의 정확한 MySQL minor version을 확인했다.
- [ ] `log_bin`, `binlog_format`, `gtid_mode`, `enforce_gtid_consistency`를 같은 시점에 수집했다.
- [ ] 재시작 후 유지될 구성 파일 또는 parameter group 값을 확인했다.
- [ ] binary log filter 때문에 검사에서 빠지는 쓰기 경로가 없는지 확인했다.

### SQL과 schema

- [ ] 코드, 배치, 배포 스크립트에서 `CREATE TABLE ... SELECT`를 검색했다.
- [ ] CTAS 대상 engine이 atomic DDL을 지원하는지 확인했다.
- [ ] 8.0.21 미만 노드가 남아 있으면 CTAS를 DDL과 DML로 분리했다.
- [ ] InnoDB 이외 영속 테이블과 mixed-engine transaction을 조사했다.
- [ ] temporary table DDL의 transaction context와 `binlog_format`을 확인했다.
- [ ] trigger, function, procedure의 간접 변경도 조사했다.

### 관측과 전환

- [ ] `WARN` 기간이 정기·비정기 배치를 포함할 만큼 충분했다.
- [ ] error log 경고가 중앙 수집·검색되는지 검증했다.
- [ ] GTID consistency 오류를 비재시도 오류로 분류했다.
- [ ] topology 전체를 같은 단계로 전환하는 순서와 rollback 기준이 있다.
- [ ] `OFF_PERMISSIVE`와 `ON_PERMISSIVE` 단계의 확인 항목을 정의했다.

### 성능과 복구

- [ ] 대량 CTAS 또는 `INSERT ... SELECT`의 binlog 양과 replica lag를 추정했다.
- [ ] metadata lock과 디스크 여유를 확인했다.
- [ ] 두 단계 복사에서 입력 실패 후 남는 빈 테이블의 처리 기준이 있다.
- [ ] Aurora 외부 replication/CDC가 있으면 writer failover 이후 재연결과 GTID 상태를 시험했다.

## 14. 결론

`enforce_gtid_consistency`는 GTID 기능의 부가 옵션이 아니라 “한 논리 트랜잭션과 한 GTID”의 대응 관계를 지키는 경계다. 이 변수의 효과를 이해하려면 문장 모양만 볼 것이 아니라 binary logging 여부, `binlog_format`, storage engine의 transaction·atomic DDL 능력, MySQL minor version을 함께 봐야 한다.

`CREATE TABLE ... SELECT`는 MySQL 8.0.21을 경계로 동작이 달라졌지만 최신 버전에서도 engine 조건이 남아 있다. mixed-engine 변경과 `STATEMENT` 형식의 임시 테이블 DDL은 별도의 위험 축이다. 운영 전환은 `WARN` 관측, 코드와 schema 조사, `ON` 강제, 단계적 `gtid_mode` 변경 순서로 진행해야 한다.

다음 단계에서는 GTID를 실제 replication channel의 자동 위치 지정에 연결해 `SOURCE_AUTO_POSITION`, source 교체, errant transaction 탐지와 복구 판단을 다룰 수 있다.
