MySQL에서 동시성 문제가 생기면 “잠금 문제” 또는 “격리 수준 문제”라고 말하기 쉽습니다. 하지만 그 말만으로는 원인을 충분히 설명하지 못합니다. 실제로는 어떤 트랜잭션이, 어떤 쿼리로, 어떤 인덱스 경로를 지나가며, 무엇을 얼마나 오래 잠갔는지까지 내려가야 합니다.

격리 수준은 이름으로 외우는 개념이 아니라 운영 중 대기와 정합성 문제로 드러나는 선택입니다. 그래서 이 글에서는 격리 수준 표를 다시 외우기보다, 실제 쿼리가 어떤 인덱스 경로를 지나고 어떤 잠금을 얼마나 오래 잡는지 추적하는 쪽에 초점을 맞춥니다. 같은 REPEATABLE READ라도 point lookup인지 range scan인지, 일반 SELECT인지 locking read인지에 따라 운영에서 보이는 증상은 완전히 달라집니다.

트랜잭션과 잠금은 같은 말이 아닙니다

트랜잭션은 작업의 완전성을 보장합니다. 여러 작업이 모두 반영되거나, 문제가 생기면 모두 되돌아가야 할 때 필요합니다. 반면 잠금은 여러 세션이 같은 자원을 동시에 바꾸지 않도록 접근 순서를 제어합니다.

MySQL 잠금 계층
MySQL 잠금 계층

둘은 함께 등장하지만 역할이 다릅니다.

구분주요 목적문제가 되는 경우
트랜잭션작업 단위를 원자적으로 처리트랜잭션이 너무 길어져 lock과 connection을 오래 붙잡음
잠금동시 접근 순서 제어잠금 범위가 예상보다 넓거나 대기 시간이 길어짐
격리 수준동시에 실행되는 트랜잭션이 서로의 변경을 어떻게 볼지 결정필요 이상으로 강하거나, 필요한 정합성을 보장하지 못함

트랜잭션을 사용한다고 해서 잠금 범위가 자동으로 적절해지지는 않습니다. 반대로 격리 수준을 높인다고 해서 모든 정합성 문제가 해결되는 것도 아닙니다. 어떤 현상이 발생했는지 먼저 고정해야 합니다.

MySQL에는 층위가 다른 잠금이 있습니다

MySQL에서 잠금이라고 부르는 것에는 여러 층위가 있습니다. 글로벌 락이나 메타데이터 락은 서버 또는 테이블 정의 변경과 관련된 더 큰 범위의 잠금입니다. 반면 InnoDB의 레코드 락, 갭 락, 넥스트 키 락은 인덱스와 트랜잭션 처리 과정에서 등장합니다.

잠금범위자주 보이는 상황
Global Lock서버 수준전체 백업, 전역 읽기 잠금
Metadata Lock테이블 메타데이터DDL과 DML이 동시에 실행될 때
Record Lock인덱스 레코드특정 row update/delete
Gap Lock인덱스 레코드 사이의 간격범위 조건에서 phantom 방지
Next-Key Lockrecord lock + gap lockREPEATABLE READ의 범위 locking read

실제 서비스에서 자주 문제가 되는 것은 “테이블이 잠겼다”보다 “생각보다 넓은 인덱스 범위가 잠겼다”에 가깝습니다.

InnoDB는 레코드보다 인덱스 경로를 기준으로 잠급니다

InnoDB는 수정할 row를 찾기 위해 사용한 인덱스 레코드에 잠금을 겁니다. 그래서 최종적으로 바뀐 row 수보다, 그 row를 찾기 위해 어떤 인덱스 경로를 지나갔는지가 먼저입니다.

InnoDB 잠금 범위와 인덱스 경로
InnoDB 잠금 범위와 인덱스 경로

예를 들어 애플리케이션은 특정 사용자 한 명의 주문 상태만 바꾼다고 생각할 수 있습니다.

sql
UPDATE orders
SET status = 'CANCELLED'
WHERE user_id = 10
  AND status = 'WAITING'
  AND created_at < '2026-01-01';

하지만 인덱스가 status 하나뿐이라면 InnoDB는 WAITING 상태의 넓은 구간을 훑어야 할 수 있습니다. 최종 수정 row는 적어도, 탐색 과정에서 지나가는 후보가 많아지고 잠금 범위도 넓어질 수 있습니다.

반대로 (user_id, status, created_at)처럼 조건에 맞는 복합 인덱스가 있다면 탐색 범위가 줄고, 잠금 영향도 함께 줄어들 수 있습니다.

격리 수준은 어떤 읽기를 허용할지 정합니다

격리 수준은 여러 트랜잭션이 동시에 실행될 때 서로의 변경을 어디까지 볼 수 있는지 정합니다.

격리 수준과 읽기 이상 현상
격리 수준과 읽기 이상 현상
격리 수준특징주의점
READ UNCOMMITTED커밋되지 않은 변경도 볼 수 있음dirty read 가능
READ COMMITTED커밋된 데이터만 읽음같은 트랜잭션 안에서 다시 읽으면 값이 바뀔 수 있음
REPEATABLE READ트랜잭션 안에서 일관된 snapshot read 제공locking read와 일반 read를 구분해야 함
SERIALIZABLE가장 강한 격리동시성 비용이 큼

높은 격리 수준이 항상 더 좋은 것은 아닙니다. 읽기 정합성은 강해질 수 있지만, 대기와 잠금 충돌 비용도 함께 커질 수 있습니다. 따라서 먼저 필요한 읽기 모델을 정해야 합니다.

일반 SELECT와 FOR UPDATE는 다르게 움직입니다

InnoDB의 REPEATABLE READ에서는 일반 SELECT가 MVCC를 통해 일관된 snapshot을 읽을 수 있습니다. 그래서 같은 트랜잭션 안에서 같은 조건을 다시 조회해도 같은 결과를 보는 것처럼 동작합니다.

sql
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1;
SELECT * FROM accounts WHERE id = 1;
COMMIT;

하지만 SELECT ... FOR UPDATE는 다릅니다. 이 쿼리는 과거 snapshot을 보는 것이 아니라 현재 데이터를 기준으로 잠금을 잡는 locking read입니다.

sql
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
COMMIT;

이 차이를 모르면 “REPEATABLE READ인데 왜 대기하지?” 또는 “SELECT인데 왜 잠금이 생기지?” 같은 질문에 답하기 어렵습니다.

읽기 방식읽는 기준잠금
일반 SELECTMVCC snapshot일반적으로 row lock을 잡지 않음
SELECT ... FOR UPDATE현재 데이터수정 의도로 row 또는 범위 잠금
UPDATE / DELETE현재 데이터대상 탐색 경로에 잠금

격리 수준을 이해할 때는 이 두 세계를 구분해야 합니다. snapshot read에서 안정적인 것과 locking read에서 충돌이 없는 것은 다른 문제입니다.

장애는 보통 세 가지 조건이 겹칠 때 드러납니다

격리 수준과 잠금이 실제 장애로 이어지는 순간은 대체로 비슷합니다.

  1. 트랜잭션이 오래 열려 있습니다.
  2. 인덱스가 조건에 맞지 않아 잠금 범위가 넓어집니다.
  3. current read 또는 write가 동시에 같은 범위를 지나갑니다.

예를 들어 아래 흐름을 생각할 수 있습니다.

text
1. 트랜잭션 시작
2. SELECT ... FOR UPDATE로 대상 조회 및 lock 획득
3. 외부 API 호출 또는 파일 처리
4. 추가 UPDATE 수행
5. 커밋

실제 DB 작업은 짧더라도 외부 API 호출이 트랜잭션 안에 있으면 lock은 그 시간만큼 유지됩니다. 이 상태에서 다른 요청이 같은 인덱스 범위를 지나가면 대기 시간이 길어집니다.

따라서 잠금 장애를 볼 때 격리 수준을 먼저 바꾸기보다, 트랜잭션 안에 DB 외 작업이 들어 있는지 먼저 확인하는 편이 안전합니다.

잠금 대기는 실행 계획과 함께 봐야 합니다

SHOW ENGINE INNODB STATUS, performance_schema, slow query log를 보면 어떤 세션이 기다렸는지는 알 수 있습니다. 하지만 왜 잠금 범위가 넓어졌는지는 실행 계획과 인덱스를 같이 봐야 합니다.

잠금 대기를 확인할 때는 다음 순서가 유용합니다.

격리 수준은 이 다음에 조정하는 편이 낫습니다. 실제로는 인덱스와 트랜잭션 길이만 정리해도 잠금 충돌이 줄어드는 경우가 많기 때문입니다.

해결책은 격리 수준 변경만이 아닙니다

동시성 문제를 만나면 READ COMMITTED로 낮추거나 SERIALIZABLE로 올리는 식의 선택을 먼저 떠올릴 수 있습니다. 하지만 격리 수준 변경은 비용과 의미가 큰 결정입니다. 먼저 더 직접적인 해결책을 검토해야 합니다.

문제먼저 검토할 해결책
중복 row 생성unique constraint, idempotency key
lost updateatomic update, version column, FOR UPDATE
잠금 대기 증가트랜잭션 축소, 인덱스 개선, 작업 순서 조정
phantom 방지가 필요함locking read, unique constraint, 격리 수준 검토
긴 snapshot 유지긴 트랜잭션 제거, 배치 chunk 분리

예를 들어 중복 가입이나 중복 주문 같은 문제는 격리 수준보다 유니크 제약이 더 직접적인 방어선일 수 있습니다. count 증가 문제는 UPDATE count = count + 1 같은 원자적 update나 version 기반 낙관적 락이 더 명확할 수 있습니다.

Deadlock은 원인과 복구 경로를 함께 봐야 합니다

Deadlock은 단순히 “DB가 꼬인 상태”가 아닙니다. 두 트랜잭션이 서로가 가진 잠금을 기다리는 순환 대기 상태이고, InnoDB는 이 상황을 감지하면 보통 한쪽 트랜잭션을 희생자로 골라 rollback합니다.

문제는 애플리케이션에서 이 실패를 어떻게 다루느냐입니다. 같은 요청을 바로 재시도해도 안전한 작업인지, 이미 외부 API 호출이나 메시지 발행이 함께 일어났는지에 따라 복구 방식이 달라집니다.

확인할 것이유
deadlock에 참여한 두 쿼리서로 어떤 순서로 어떤 인덱스를 잡았는지 확인
각 트랜잭션의 작업 순서동일 자원을 서로 다른 순서로 잡는 패턴이 있는지 확인
재시도 가능 여부멱등성이 없는 작업을 단순 재시도하면 중복 부작용이 생길 수 있음
외부 부작용 위치DB rollback과 외부 API, 메시지 발행 결과가 어긋날 수 있음

Deadlock 재시도는 유용한 방어선이지만, 모든 문제를 덮는 만능 처리는 아닙니다. 재시도 전에 작업이 멱등적으로 설계되어 있는지, 트랜잭션 안에 외부 부작용이 들어 있지 않은지 확인해야 합니다.

잠금 문제를 좁히는 관측 항목

잠금 문제는 재현될 때만 보이고 지나가면 사라지는 경우가 많습니다. 그래서 운영 지표와 로그가 중요합니다.

관측 항목확인하려는 것
lock wait time대기 시간이 사용자 응답 지연과 연결되는지
deadlock log어떤 쿼리와 인덱스가 서로 기다렸는지
transaction age오래 열린 트랜잭션이 있는지
history list length긴 트랜잭션 때문에 undo purge가 밀리는지
EXPLAIN rows잠금 대상 탐색 범위가 과도한지
slow query log쿼리 실행 시간과 lock time이 함께 증가하는지

특히 deadlock은 단순히 “DB가 꼬였다”가 아닙니다. 어떤 순서로 어떤 인덱스를 잡았는지 알려주는 디버깅 자료입니다. deadlock log를 남기지 않으면 같은 문제가 반복되어도 원인을 좁히기 어렵습니다.

격리 수준보다 먼저 볼 것

MySQL에서 격리 수준과 잠금이 실제 장애로 이어지는 조건은 단순히 “격리 수준이 낮아서” 또는 “잠금이 있어서”가 아닙니다. 트랜잭션 길이, 인덱스 경로, 읽기 방식, 쓰기 범위가 함께 맞물릴 때 문제가 됩니다.

운영에서 남는 기준은 다음과 같습니다.

  • 트랜잭션은 작업 단위의 원자성을 보장하고, 잠금은 동시 접근 순서를 제어합니다.
  • InnoDB는 row 자체보다 row를 찾기 위해 사용한 인덱스 경로를 기준으로 잠금을 잡습니다.
  • 일반 SELECT의 snapshot read와 FOR UPDATE 같은 locking read는 다르게 동작합니다.
  • 높은 격리 수준이 항상 좋은 선택은 아니며, 필요한 읽기 모델에 맞춰 선택해야 합니다.
  • 잠금 대기는 격리 수준보다 트랜잭션 길이와 인덱스 설계에서 먼저 줄어드는 경우가 많습니다.
  • Deadlock 재시도는 작업의 멱등성과 외부 부작용 위치까지 확인한 뒤 적용해야 합니다.
  • 운영에서는 lock wait, deadlock log, transaction age, EXPLAIN을 함께 봐야 합니다.

그래서 격리 수준을 바꾸기 전에 먼저 확인할 질문은 더 작아야 합니다. 이 쿼리는 어떤 값을 읽고, 어떤 인덱스 경로로 찾고, 무엇을 얼마나 오래 잠그는가. deadlock 로그나 SHOW ENGINE INNODB STATUS를 볼 때도 같은 순서로 읽어야 합니다. 재시도 로직을 넣는 것은 그다음 문제입니다. 재시도 대상 작업이 멱등적인지, 외부 알림이나 결제처럼 되돌리기 어려운 부작용을 이미 냈는지까지 확인해야 격리 수준을 실제 해결 도구로 사용할 수 있습니다.