한 줄 요약: InnoDB 기본 격리 수준은 REPEATABLE READ이며, MVCC consistent read로 Non-Repeatable Read와 Phantom Read를 모두 방지함. 다만 INSERT ... SELECT 같은 복합 DML은 source table에도 shared next-key lock을 넓게 잡을 수 있어, 필요한 경우 해당 세션이나 다음 트랜잭션만 READ COMMITTED로 낮춰 우회하기도 함.


개요

트랜잭션 격리 수준(isolation level)은 동시에 실행 중인 여러 트랜잭션이 서로의 변경 사항을 얼마나 볼 수 있는지를 결정함. SQL:1992 표준은 4가지 수준을 정의하고, 각 수준은 허용하는 이상 현상의 종류로 구분됨.

MySQL InnoDB는 이 4가지를 모두 지원하되, REPEATABLE READ에서 MVCC와 Next-Key Lock을 조합해 SQL 표준보다 강한 격리를 제공함.

이 글은 각 격리 수준의 동작 방식과 이상 현상 방지 범위를 정리하고, InnoDB REPEATABLE READ의 특수성을 직접 테스트로 확인함. 실무에서 종종 문제가 되는 INSERT ... SELECT의 source table locking과 세션·트랜잭션 단위 우회 방법도 함께 정리함.


결론

격리 수준별 이상 현상 허용 범위

격리 수준Dirty ReadNon-Repeatable ReadPhantom Read비고
READ UNCOMMITTED발생 가능발생 가능발생 가능실무 사용 드묾
READ COMMITTED방지발생 가능발생 가능Oracle 기본값, MySQL에서 lock 경합 완화용으로 검토
REPEATABLE READ방지방지방지 (InnoDB 한정)InnoDB 기본값
SERIALIZABLE방지방지방지동시성 비용 큼

InnoDB REPEATABLE READ는 SQL 표준상 Phantom Read를 허용해도 되는 수준이지만, MVCC consistent read와 Next-Key Lock으로 실질적으로 방지함.

운영 시 기억할 것

  • 기본값 그대로 쓰는 경우가 대부분임. REPEATABLE READ는 대부분의 OLTP 워크로드에 충분함.
  • READ COMMITTED로 낮추면 일반적인 locking read·UPDATE·DELETE에서 gap lock이 비활성화되어 deadlock 빈도가 줄어들 수 있음. 단, foreign key 제약 확인·duplicate-key 확인 예외와 Non-Repeatable Read 허용을 명시적으로 수용해야 함.
  • INSERT ... SELECT는 REPEATABLE READ에서 대상 테이블뿐 아니라 source table의 검색 row에도 shared next-key lock을 잡을 수 있음. 배치성 복사 쿼리가 OLTP source table을 막는 원인이 되면 해당 세션 또는 다음 트랜잭션만 READ COMMITTED로 낮추는 방식을 검토함.
  • SERIALIZABLE은 autocommit=0 상태에서 모든 SELECTSELECT ... FOR SHARE로 변환함. 동시성이 크게 낮아지므로 일반 OLTP에는 맞지 않음.
  • 격리 수준 변경은 SET SESSION TRANSACTION ISOLATION LEVEL ... 또는 SET SESSION transaction_isolation = '...' 으로 이후 트랜잭션에 적용 가능함. SET TRANSACTION ISOLATION LEVEL ... 은 다음 트랜잭션 1회에만 적용됨.

이상 현상 세 가지

격리 수준을 이해하려면 각 수준이 방지하려는 이상 현상(anomaly)을 먼저 파악해야 함.

Dirty Read

커밋되지 않은 데이터를 다른 트랜잭션이 읽는 현상임. T2가 변경 중인 값을 T1이 읽었는데, T2가 나중에 롤백하면 T1은 존재하지 않는 데이터를 읽은 셈이 됨.

sequenceDiagram
    participant T1
    participant T2
    T2->>DB: UPDATE balance = 1500 (미커밋)
    T1->>DB: SELECT balance → 1500 (커밋 전 값 읽음)
    T2->>DB: ROLLBACK
    Note over T1: T1이 읽은 1500은 존재하지 않았던 값임

Non-Repeatable Read

같은 트랜잭션에서 같은 row를 두 번 읽었을 때 값이 달라지는 현상임. T1이 첫 번째 읽기를 마친 뒤, T2가 해당 row를 수정·커밋하고, T1이 다시 읽으면 다른 값이 나옴.

sequenceDiagram
    participant T1
    participant T2
    T1->>DB: SELECT balance → 1000
    T2->>DB: UPDATE balance = 1500
    T2->>DB: COMMIT
    T1->>DB: SELECT balance → 1500 (달라짐)

Phantom Read

같은 조건으로 범위 SELECT를 두 번 실행했을 때 결과 행 수가 달라지는 현상임. row 값이 변하는 Non-Repeatable Read와 달리, 행 자체가 추가되거나 사라짐.

sequenceDiagram
    participant T1
    participant T2
    T1->>DB: SELECT * WHERE amount > 100 → 3 rows
    T2->>DB: INSERT amount = 200
    T2->>DB: COMMIT
    T1->>DB: SELECT * WHERE amount > 100 → 4 rows (행 수 달라짐)

4가지 격리 수준 상세

READ UNCOMMITTED

가장 낮은 격리 수준임. SELECT가 잠금 없이 동작하고, 다른 트랜잭션이 커밋하지 않은 변경 사항도 읽을 수 있음.

Dirty Read, Non-Repeatable Read, Phantom Read 모두 발생 가능함. 실무에서 사용 이유가 거의 없으며, 특수한 집계 근사치 조회 외에는 선택하지 않음.

공식 문서 기준으로 READ UNCOMMITTEDSELECT는 nonlocking 방식으로 동작하지만 일관된 읽기를 보장하지 않으며, dirty read가 발생할 수 있음.


READ COMMITTED

Dirty Read는 방지하지만 Non-Repeatable Read는 허용하는 수준임. Oracle의 기본 격리 수준이기도 함.

핵심 동작: 같은 트랜잭션 안에서도 SELECT를 실행할 때마다 새 snapshot을 생성함. 따라서 T2가 커밋한 변경 사항을 T1의 이후 SELECT에서 볼 수 있음.

공식 문서 기준으로 READ COMMITTED의 consistent read는 같은 트랜잭션 안에서도 매번 새 snapshot을 생성함.

추가 특성:

  • Gap lock이 비활성화됨 (외래 키 검사·중복 키 검사 제외)
  • Semi-consistent read: WHERE 조건에 맞지 않는 row의 잠금을 즉시 해제하여 deadlock 빈도를 줄임
  • 바이너리 로그는 row-based logging만 지원함 (binlog_format=ROW 또는 MIXED 필요)
  • INSERT ... SELECT에서 source table S를 consistent read로 읽어 source row에 lock을 걸지 않음

REPEATABLE READ (InnoDB 기본값)

Non-Repeatable Read를 방지하고, InnoDB에서는 Phantom Read도 대부분 방지하는 수준임.

핵심 동작: 트랜잭션에서 첫 번째 SELECT(consistent read)가 실행될 때 snapshot을 생성하고, 이후 같은 트랜잭션의 모든 consistent read는 그 snapshot을 재사용함.

공식 문서 기준으로 REPEATABLE READ의 consistent read는 같은 트랜잭션 안에서 첫 read가 만든 snapshot을 계속 재사용함.

SQL 표준 REPEATABLE READ와의 차이: SQL 표준에서 REPEATABLE READ는 Phantom Read를 허용해도 됨. 그러나 InnoDB는 두 가지 메커니즘으로 실질적으로 방지함.

읽기 유형메커니즘Phantom Read 방지
Consistent read (일반 SELECT)MVCC snapshot방지됨
Current read (FOR UPDATE, FOR SHARE, UPDATE, DELETE)Next-Key Lock방지됨

Next-Key Lock은 index record lock과 그 앞의 gap lock을 결합한 잠금임. 범위 조건에 해당하는 gap에 대한 INSERT를 차단하여 Phantom Read를 방지함.

실무 케이스: INSERT ... SELECT가 source table을 넓게 잠그는 경우

INSERT INTO target SELECT ... FROM source WHERE ... 형태는 단순 조회처럼 보이지만 InnoDB 관점에서는 복합 DML임. 대상 테이블 target에는 insert row마다 exclusive index record lock을 잡고, 격리 수준이 READ COMMITTED가 아니면 source table source의 검색 row에도 shared next-key lock을 잡음.

개발자가 운영 OLTP 테이블을 source로 삼아 배치성 INSERT ... SELECT를 실행하면, source table의 범위 row와 gap이 예상보다 넓게 잠길 수 있음. 이때 같은 범위에 insert/update를 수행하는 온라인 트랜잭션이 대기하거나 deadlock 후보가 늘어나는 식으로 장애가 보임.

회피책은 쿼리 자체를 먼저 줄이는 것임. WHERE 조건을 좁히고 적절한 인덱스를 타게 만들며, 큰 작업은 PK 범위로 잘라 짧게 commit하는 방식이 우선임.

그래도 INSERT ... SELECT 패턴이 필요하고 source table locking만 줄이는 것이 목적이면 해당 작업 세션 또는 다음 트랜잭션만 READ COMMITTED로 낮추는 방식을 사용하기도 함. MySQL 8.4 문서는 READ COMMITTED에서 INSERT ... SELECT의 source table 검색을 consistent read로 수행해 lock을 걸지 않는다고 설명함.

-- 다음 트랜잭션 1회만 READ COMMITTED로 실행
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
 
INSERT INTO monthly_order_summary (order_id, customer_id, amount)
SELECT id, customer_id, amount
FROM orders
WHERE created_at >= '2026-05-01'
  AND created_at < '2026-06-01';
 
COMMIT;

세션 전체를 배치 전용으로 분리할 수 있으면 세션 단위 변경도 가능함. 다만 connection pool을 쓰는 애플리케이션에서는 세션 상태가 다음 요청에 재사용될 수 있으므로, 작업 후 원래 격리 수준으로 되돌리거나 next-transaction 방식으로 범위를 좁히는 것이 안전함.

-- 배치 전용 세션에서 이후 트랜잭션을 READ COMMITTED로 실행
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
 
-- 작업 종료 후 세션 기본값 복구
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SERIALIZABLE

가장 강한 격리 수준임. autocommit=0 상태에서는 모든 일반 SELECTSELECT ... FOR SHARE처럼 동작하여 공유 잠금을 획득함.

공식 문서 기준으로 SERIALIZABLEREPEATABLE READ와 비슷하지만, autocommit=0 상태에서 일반 SELECT를 암묵적으로 SELECT ... FOR SHARE처럼 처리함.

autocommit=1인 경우 각 SELECT가 독립된 트랜잭션으로 처리되므로 직렬화 없이도 일관성이 보장됨.

동시성이 크게 낮아지므로 XA 트랜잭션이나 deadlock 원인 분석 등 특수 목적에만 씀.


설정 방법

현재 격리 수준 확인

SELECT @@transaction_isolation;
+-----------------------+
| @@transaction_isolation |
+-----------------------+
| REPEATABLE-READ       |
+-----------------------+

SESSION 변경 (현재 세션의 이후 트랜잭션)

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 또는 동일하게
SET SESSION transaction_isolation = 'READ-COMMITTED';

SESSION 변경은 현재 진행 중인 트랜잭션에는 영향을 주지 않고, 현재 세션의 이후 트랜잭션에 적용됨.

GLOBAL 변경 (이후 신규 세션에만 적용)

SET GLOBAL transaction_isolation = 'READ-COMMITTED';

다음 트랜잭션에만 적용 (1회용)

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- 이 트랜잭션만 READ COMMITTED로 실행됨
COMMIT;

SESSION·GLOBAL 없이 실행한 SET TRANSACTION은 다음 트랜잭션 1회에만 적용되고, 트랜잭션이 이미 시작된 뒤에는 사용할 수 없음.

my.cnf에 고정

[mysqld]
transaction-isolation = REPEATABLE-READ

테스트

테스트 환경: MySQL 8.4.8

1. 기본 격리 수준 확인

SELECT @@transaction_isolation AS default_isolation_level;
+-------------------------+
| default_isolation_level |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+

MySQL 8.4.8 에서 직접 확인 — InnoDB 기본 격리 수준은 REPEATABLE-READ임.


2. Non-Repeatable Read — READ COMMITTED에서 발생

T1은 READ COMMITTED 트랜잭션에서 id=1balance를 읽고 대기함. T2가 그 사이 balance를 1000 → 1500으로 수정·커밋함. T1이 다시 읽으면 1500이 나옴.

T1 (READ COMMITTED 트랜잭션):

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT id, balance FROM accounts WHERE id = 1;
-- (T2가 커밋하는 동안 대기)
SELECT id, balance FROM accounts WHERE id = 1;  -- 두 번째 읽기
ROLLBACK;

T2 (별도 세션):

UPDATE accounts SET balance = 1500 WHERE id = 1;
COMMIT;

T1 첫 번째 SELECT 결과:

+----+---------+
| id | balance |
+----+---------+
|  1 |    1000 |
+----+---------+

T1 두 번째 SELECT 결과 (T2 커밋 이후):

+----+---------+
| id | balance |
+----+---------+
|  1 |    1500 |
+----+---------+

같은 트랜잭션에서 같은 row를 두 번 읽었는데 값이 달라짐. READ COMMITTED는 SELECT마다 새 snapshot을 생성하므로 T2의 커밋이 즉시 반영됨.


3. Non-Repeatable Read 방지 — REPEATABLE READ

동일한 시나리오에서 T1의 격리 수준만 REPEATABLE READ로 변경함.

T1 (REPEATABLE READ 트랜잭션):

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT id, balance FROM accounts WHERE id = 1;  -- snapshot 생성
-- (T2가 1500으로 커밋)
SELECT id, balance FROM accounts WHERE id = 1;  -- 두 번째 읽기
ROLLBACK;

T1 첫 번째 SELECT 결과:

+----+---------+
| id | balance |
+----+---------+
|  1 |    1000 |
+----+---------+

T1 두 번째 SELECT 결과 (T2가 1500으로 커밋한 이후):

+----+---------+
| id | balance |
+----+---------+
|  1 |    1000 |
+----+---------+

T2가 1500으로 수정·커밋했음에도 T1은 계속 1000을 읽음. 트랜잭션 첫 SELECT에서 생성된 snapshot이 그대로 유지됨.

MySQL 8.4.8 에서 직접 확인 — REPEATABLE READ에서 T1은 트랜잭션 시작 시점의 snapshot을 유지하며, T2의 커밋 이후에도 같은 값(1000)을 읽음.


4. Phantom Read 시나리오 (개념 확인)

REPEATABLE READ에서 consistent read(일반 SELECT)는 snapshot 기반이므로 phantom read가 발생하지 않음.

단, FOR UPDATEFOR SHARE 같은 current read를 사용할 때는 snapshot 밖에서 동작하므로 Next-Key Lock이 gap을 잠가 phantom read를 차단함.

-- T1: REPEATABLE READ, current read
START TRANSACTION;
SELECT * FROM accounts WHERE balance > 500 FOR UPDATE;
-- Next-Key Lock이 balance > 500 범위의 gap을 잠금
-- T2의 INSERT (balance = 800) 가 이 범위에 해당하면 대기하게 됨
COMMIT;

consistent read와 current read의 동작 차이:

읽기 유형잠금Snapshot 사용Phantom Read
SELECT (consistent read)없음트랜잭션 첫 SELECT 기준방지됨
SELECT ... FOR UPDATE (current read)X-lock + Next-Key Lock최신 커밋 기준Next-Key Lock으로 방지됨

참고