에디블로그
Engineer's Field Notes

AI 자동화로 매일
한 편씩 쓰는
엔지니어 운영 노트

Claude Code · 자동화 파이프라인 · 사고 회고까지. 잘 굴러간 기록 + 깨진 흔적도 같이 남깁니다.

사람이 할 수 있는 일은,
AI도 할 수 있어야 합니다.
매일 한 편 쓰면서 검증 중.
— 이번 주 가장 많이 읽힌 글 TOP 3
백엔드/데이터 & DB

[MySQL] 트랜잭션 격리수준과 MVCC: REPEATABLE READ, 언두 로그, 팬텀 리드

반응형
[MySQL] 트랜잭션 격리수준과 MVCC: REPEATABLE READ, 언두 로그, 팬텀 리드

[MySQL] 트랜잭션 격리수준과 MVCC: REPEATABLE READ, 언두 로그, 팬텀 리드

격리수준은 면접 단골이라 표는 다들 외우는데, 실무에서 정작 중요한 질문은 따로 있어요. "MySQL 기본값(REPEATABLE READ)을 그냥 둬도 되나?", "읽는 동안 왜 안 막히지?", "같은 트랜잭션에서 조회했는데 왜 옛날 값이 나오지?" — 전부 MVCC를 알면 풀려요.

격리수준 4단계와 이상 현상, InnoDB가 락 없이 일관 읽기를 만드는 MVCC(언두 로그), READ COMMITTED와 REPEATABLE READ의 실질 차이, 그리고 실무 선택 기준까지 차례로 풀어요.

01. 격리수준이 거래하는 것

트랜잭션을 완전히 직렬로 돌리면 정합성은 완벽하지만 동시성이 죽어요. 격리수준은 "어느 정도의 이상 현상을 허용하고 동시성을 살릴까"의 단계별 거래예요.

격리수준과 이상 현상 매트릭스. READ UNCOMMITTED는 더티 리드와 반복 불가 읽기와 팬텀이 모두 발생하고 READ COMMITTED는 더티 리드만 방지한다. MySQL 기본인 REPEATABLE READ는 반복 불가 읽기까지 방지하고 팬텀도 InnoDB에서는 대부분 방지하며, SERIALIZABLE은 전부 방지한다
  • 더티 리드 — 커밋 안 된 값을 읽음. 롤백되면 "없던 값"을 읽은 셈이라 치명적이에요.
  • 반복 불가 읽기 — 같은 행을 두 번 읽었는데 사이에 다른 커밋이 끼어 값이 달라짐.
  • 팬텀 리드 — 같은 조건으로 두 번 조회했는데 없던 행이 나타남(INSERT가 끼어듦).

표준상 REPEATABLE READ는 팬텀을 허용하지만, InnoDB는 MVCC와 갭 락 덕분에 RR에서도 팬텀을 대부분 막아요. MySQL이 표준 표보다 한 칸 강한 이유고, 그 장치가 다음 주제예요.

02. MVCC, 락 없이 읽는 장치

순진하게 구현하면 "읽는 동안 쓰지 마" 락이 필요해요. 그러면 읽기·쓰기가 서로 줄을 서서 동시성이 박살나죠. InnoDB는 다르게 풀어요 — 버전을 여러 개 보관하고 트랜잭션마다 자기 시점의 버전을 읽게 해요. 다중 버전 동시성 제어(MVCC)예요.

InnoDB MVCC 동작. 행의 최신 값과 별개로 언두 로그에 이전 버전들이 체인으로 보관되어, 먼저 시작한 트랜잭션은 자기 시작 시점 기준 버전을 언두 체인에서 찾아 읽는다. 읽기와 쓰기가 서로 막지 않는다

행이 수정되면 이전 값이 언두 로그에 체인으로 남아요. SELECT는 "내 트랜잭션이 볼 수 있는 시점"의 버전을 이 체인에서 찾아 읽어요. 그래서:

  • 누가 수정 중이어도 내 SELECT는 안 기다려요(이전 버전을 읽으면 되니까).
  • 내 SELECT가 길어도 남의 쓰기를 안 막아요.

이게 "MySQL은 읽기가 안 막힌다"의 정체예요. 일반 SELECT는 락을 안 잡는 일관 읽기(consistent read)고, 락을 잡는 읽기(SELECT ... FOR UPDATE)는 별도예요 — 그건 다음 편(락)의 주제예요.

MVCC의 숨은 비용도 알아둬요. 언두 로그는 "그 버전을 볼 수도 있는 트랜잭션"이 살아 있는 한 못 지워요. 그래서 몇 시간씩 열려 있는 긴 트랜잭션 하나가 언두 로그를 무한정 키워요. 디스크가 차고 버전 체인이 길어져 조회도 느려져요. "트랜잭션은 짧게"가 매너가 아니라 성능 규칙인 이유 중 하나예요.

03. RC vs RR, 스냅샷을 언제 찍나

READ COMMITTED와 REPEATABLE READ의 실질 차이는 한 줄이에요 — 스냅샷(읽기 기준 시점)을 언제 찍느냐.

READ COMMITTED와 REPEATABLE READ 차이. RC는 SELECT마다 새 스냅샷을 찍어 항상 최신 커밋을 보고, MySQL 기본 RR은 첫 SELECT에 스냅샷을 고정해 트랜잭션 내내 같은 시점을 본다
  • 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

반응형

📚 같이 보면 좋은

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."