[MySQL] 트랜잭션 격리수준과 MVCC: REPEATABLE READ, 언두 로그, 팬텀 리드
![[MySQL] 트랜잭션 격리수준과 MVCC: REPEATABLE READ, 언두 로그, 팬텀 리드](https://blog.kakaocdn.net/dna/p3leD/dJMcaci1euz/AAAAAAAAAAAAAAAAAAAAAHvPFYZ2_vw8EdgbJMKzv2Fyi90Aix7Qd7DzUI8bQMQC/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1788188399&allow_ip=&allow_referer=&signature=eGK8zuK5QiQVCqI9cCrdOgWtqls%3D)

[MySQL] 트랜잭션 격리수준과 MVCC: REPEATABLE READ, 언두 로그, 팬텀 리드
격리수준은 면접 단골이라 표는 다들 외우는데, 실무에서 정작 중요한 질문은 따로 있어요. "MySQL 기본값(REPEATABLE READ)을 그냥 둬도 되나?", "읽는 동안 왜 안 막히지?", "같은 트랜잭션에서 조회했는데 왜 옛날 값이 나오지?" — 전부 MVCC를 알면 풀려요.
격리수준 4단계와 이상 현상, InnoDB가 락 없이 일관 읽기를 만드는 MVCC(언두 로그), READ COMMITTED와 REPEATABLE READ의 실질 차이, 그리고 실무 선택 기준까지 차례로 풀어요.
01. 격리수준이 거래하는 것
트랜잭션을 완전히 직렬로 돌리면 정합성은 완벽하지만 동시성이 죽어요. 격리수준은 "어느 정도의 이상 현상을 허용하고 동시성을 살릴까"의 단계별 거래예요.

- 더티 리드 — 커밋 안 된 값을 읽음. 롤백되면 "없던 값"을 읽은 셈이라 치명적이에요.
- 반복 불가 읽기 — 같은 행을 두 번 읽었는데 사이에 다른 커밋이 끼어 값이 달라짐.
- 팬텀 리드 — 같은 조건으로 두 번 조회했는데 없던 행이 나타남(INSERT가 끼어듦).
표준상 REPEATABLE READ는 팬텀을 허용하지만, InnoDB는 MVCC와 갭 락 덕분에 RR에서도 팬텀을 대부분 막아요. MySQL이 표준 표보다 한 칸 강한 이유고, 그 장치가 다음 주제예요.
02. MVCC, 락 없이 읽는 장치
순진하게 구현하면 "읽는 동안 쓰지 마" 락이 필요해요. 그러면 읽기·쓰기가 서로 줄을 서서 동시성이 박살나죠. InnoDB는 다르게 풀어요 — 버전을 여러 개 보관하고 트랜잭션마다 자기 시점의 버전을 읽게 해요. 다중 버전 동시성 제어(MVCC)예요.

행이 수정되면 이전 값이 언두 로그에 체인으로 남아요. SELECT는 "내 트랜잭션이 볼 수 있는 시점"의 버전을 이 체인에서 찾아 읽어요. 그래서:
- 누가 수정 중이어도 내 SELECT는 안 기다려요(이전 버전을 읽으면 되니까).
- 내 SELECT가 길어도 남의 쓰기를 안 막아요.
이게 "MySQL은 읽기가 안 막힌다"의 정체예요. 일반 SELECT는 락을 안 잡는 일관 읽기(consistent read)고, 락을 잡는 읽기(SELECT ... FOR UPDATE)는 별도예요 — 그건 다음 편(락)의 주제예요.
MVCC의 숨은 비용도 알아둬요. 언두 로그는 "그 버전을 볼 수도 있는 트랜잭션"이 살아 있는 한 못 지워요. 그래서 몇 시간씩 열려 있는 긴 트랜잭션 하나가 언두 로그를 무한정 키워요. 디스크가 차고 버전 체인이 길어져 조회도 느려져요. "트랜잭션은 짧게"가 매너가 아니라 성능 규칙인 이유 중 하나예요.
03. RC vs RR, 스냅샷을 언제 찍나
READ COMMITTED와 REPEATABLE READ의 실질 차이는 한 줄이에요 — 스냅샷(읽기 기준 시점)을 언제 찍느냐.

- RC — SELECT마다 새 스냅샷. 항상 "방금 커밋된" 최신을 봐요. 같은 트랜잭션 안에서 같은 쿼리 결과가 달라질 수 있어요(반복 불가 읽기 허용).
- RR — 트랜잭션의 첫 SELECT에서 스냅샷을 고정. 내내 같은 시점을 봐요. "분명 다른 데서 커밋했는데 내 트랜잭션에선 안 보여요"는 버그가 아니라 RR의 정상 동작이에요.
실무 기준은 단순해요. MySQL은 RR 기본을 그대로 쓰는 게 보통이에요. 생태계(복제, 잠금 동작)가 RR 기준으로 다져져 있고, 트랜잭션 내 일관성은 대부분의 비즈니스 로직에 이로워요. RC로 낮추는 건 갭 락 경합을 줄이려는 등 구체적 이유가 있을 때의 선택이에요 — "왜"가 있어야 해요.
04. 흔한 오해 정리
"격리수준이 있으면 갱신 분실도 막아주죠?"
아니요. A·B가 같은 값을 읽고 각자 계산해 쓰는 갱신 분실은 RR에서도 생겨요. MVCC는 "읽기의 일관성"을 줄 뿐, read-modify-write 충돌은 낙관적 락(@Version)·비관적 락(FOR UPDATE)·원자적 UPDATE로 따로 막아야 해요.
"RR이면 같은 걸 두 번 읽으면 늘 같다면서요? 그런데 달라졌어요"
자기 트랜잭션이 수정한 건 보여요. 그리고 SELECT ... FOR UPDATE는 스냅샷이 아니라 최신 커밋 값을 읽어요(락을 잡아야 하니까요). 일반 SELECT와 락 읽기가 다른 시점을 볼 수 있다는 건 꽤 미묘한 함정이에요.
"SERIALIZABLE이 제일 안전하니 그걸 쓰면 되죠?"
모든 SELECT가 공유 락을 잡게 돼서 동시성이 크게 떨어져요. 일반 서비스에서 전역 SERIALIZABLE은 사실상 안 써요. 정합성이 극도로 중요한 특정 구간만 락(FOR UPDATE)으로 좁게 보호하는 게 현실적이에요.
05. 자주 만나는 문제
다른 트랜잭션이 커밋했는데 조회가 안 돼요
RR의 스냅샷 고정 — 정상이에요. 최신을 봐야 하면 트랜잭션을 새로 시작하거나, 그 로직만 락 읽기로 가져가요.
언두 로그(또는 ibdata)가 계속 커져요
장수 트랜잭션을 찾아요. information_schema.innodb_trx에서 오래 열린 트랜잭션을 확인하고, 배치·관리 쿼리가 트랜잭션을 안 닫고 있는 경우가 단골이에요.
같은 조건 SELECT인데 두 번째에 행이 늘었어요 (RC)
RC에서는 정상이에요(팬텀·반복불가 허용). 트랜잭션 안 일관성이 필요하면 RR로, 특정 구간만이면 락으로 풀어요.
RR을 그대로 쓰되, 셋만 기억하기
격리수준은 이상 현상과 동시성을 맞바꾸는 거래이고, MySQL 기본 RR은 첫 SELECT에 스냅샷을 고정해 트랜잭션 내 일관성을 줘요. 그걸 락 없이 가능하게 하는 게 언두 로그 기반 MVCC고요. 실무에서 들고 갈 건 세 줄이에요.
- 격리수준은 → RR 기본을 그대로. 낮출 땐 "왜"가 있을 때만.
- 트랜잭션은 → 짧게. 장수 트랜잭션이 언두 로그를 키워요.
- 갱신 분실은 → 격리수준이 아니라 락으로. MVCC는 읽기 일관성만 줘요.
그 "락"이 바로 다음 주제예요. RR의 팬텀 방지를 떠받치는 레코드 락·갭 락·넥스트키 락의 실체로 들어갑니다.
출처: MySQL — Transaction Isolation Levels · MySQL — InnoDB Multi-Versioning
'백엔드 > 데이터 & DB' 카테고리의 다른 글
| [MySQL] 데드락 원인과 해결: SHOW ENGINE INNODB STATUS 읽는 법, 재시도 전략 (0) | 2026.07.24 |
|---|---|
| [MySQL] InnoDB 락 정리: 레코드 락, 갭 락, 넥스트키 락, SELECT FOR UPDATE (0) | 2026.07.23 |
| [MySQL] EXPLAIN 실행계획 읽는 법: type, Extra, rows 그리고 EXPLAIN ANALYZE (0) | 2026.07.21 |
| [MySQL] 복합 인덱스 설계: 컬럼 순서, 커버링 인덱스, 인덱스 못 타는 조건 (0) | 2026.07.20 |
| [MySQL] 인덱스 동작 원리: B+Tree, 클러스터드 인덱스 vs 세컨더리 인덱스 (1) | 2026.07.17 |
📚 같이 보면 좋은
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."