MySQL에서 deadlock found when trying to get lock 에러를 보면 많은 팀이 먼저 재시도를 붙입니다. 재시도는 필요할 수 있지만, deadlock의 핵심은 “잠깐 운이 나빴다”가 아닙니다. 대개는 둘 이상의 트랜잭션이 같은 자원을 다른 순서로 잡거나, 생각보다 넓은 범위를 잠그고 있다는 구조적 신호입니다.
그래서 deadlock 대응은 단순히 실패한 쿼리 한 줄을 보는 일보다, 동시에 실행된 흐름들이 어떤 순서로 어떤 row와 index range를 건드렸는지 복원하는 일에 가깝습니다.
실제로 프로덕션에서 하루 수백 건의 deadlock alert가 터졌을 때, 팀에서 가장 먼저 한 일은 retry를 늘리는 것이었습니다. 하지만 2주 뒤에도 빈도가 줄지 않았고, 결국 SHOW ENGINE INNODB STATUS를 꺼내서 두 배치 job의 접근 순서가 정반대라는 걸 찾아냈습니다. retry는 증상을 숨겼고, 문제는 그대로 남아 있었습니다.
이 글에서는 아래를 한 번에 정리합니다.
- deadlock이 정확히 무엇인지
- lock wait timeout과 무엇이 다른지
SHOW ENGINE INNODB STATUS에서 무엇을 봐야 하는지- 어떤 트랜잭션 순서와 범위가 deadlock을 자주 만드는지
- 재시도는 어디까지가 안전장치이고, 어디부터가 근본 원인 은폐인지
요약부터 말하면 deadlock은 “DB가 이상하다”기보다 “동시에 실행되는 쓰기 흐름의 순서와 범위가 충돌한다”는 신호입니다.
Quick answer
실무에서는 아래 순서로 보면 가장 빠릅니다.
- deadlock이 난 시점의 SQL과 요청 경로를 잡습니다.
SHOW ENGINE INNODB STATUS\G로 최신 deadlock 정보를 먼저 확인합니다.- 어떤 트랜잭션이 어떤 테이블, 어떤 index, 어떤 row 또는 range를 잡고 있었는지 비교합니다.
- 두 흐름이 같은 자원을 다른 순서로 접근하는지 확인합니다.
- 트랜잭션 안에 외부 API 호출, 과도한 루프, 넓은 range scan이 들어있는지 봅니다.
- supporting index 부족 때문에 lock 범위가 넓어진 건 아닌지 확인합니다.
- 재시도는 추가하되, idempotency와 backoff를 갖춘 안전장치로만 둡니다.
즉 deadlock 대응의 중심은 retry 추가가 아니라 충돌 구조를 재구성해서 접근 순서와 트랜잭션 범위를 줄이는 것입니다.
1. MySQL deadlock은 정확히 어떤 상태인가
deadlock은 두 개 이상 트랜잭션이 서로가 가진 잠금을 기다리면서 앞으로 진행할 수 없는 순환 대기 상태를 말합니다.
가장 단순한 예시는 이렇습니다.
-- transaction A
START TRANSACTION;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
-- transaction B
START TRANSACTION;
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
-- A tries to touch id = 2
-- B tries to touch id = 1
이 시점에는 A가 B를 기다리고, B가 다시 A를 기다립니다. 그냥 오래 기다린다고 풀리는 문제가 아니라 구조상 더 이상 진행할 수 없는 상태이기 때문에, InnoDB는 그중 하나를 골라 롤백합니다.
중요한 점은 여기서 롤백된 쪽이 항상 “잘못한 쿼리”는 아니라는 것입니다. 마지막에 실패한 쿼리는 피해자일 수 있고, 실제 원인은 서로 다른 코드 경로의 접근 순서 자체일 수 있습니다.
2. lock wait timeout과는 무엇이 다른가
둘 다 lock 문제라 헷갈리지만, 디버깅 포인트는 꽤 다릅니다.
lock wait timeout: 누군가 오래 잡고 있어서 다른 쪽이 너무 오래 기다린 경우deadlock: 서로가 서로를 기다리는 순환 구조가 생긴 경우
그래서 lock wait timeout에서 핵심 질문은 대개 이렇습니다.
- 누가 blocker였는가
- 왜 그 트랜잭션이 그렇게 오래 열려 있었는가
반대로 deadlock에서 핵심 질문은 주로 이렇습니다.
- 어떤 두 흐름이 같은 자원을 다른 순서로 접근했는가
- 어떤 index 또는 range 때문에 충돌 면적이 넓어졌는가
또 한 가지 차이는 대응 방식입니다.
- lock wait timeout은
innodb_lock_wait_timeout조정이 임시 완화책이 될 수 있습니다. - deadlock은 MySQL이 구조적 충돌을 감지하면 바로 한쪽을 죽이기 때문에, timeout 값을 올려도 근본 해결이 되지 않습니다.
즉 deadlock에 timeout 튜닝을 먼저 가져가는 건 방향이 어긋날 가능성이 큽니다.
보다 일반적인 대기 문제는 MySQL Lock Wait Timeout 가이드에서 이어서 볼 수 있습니다.
3. 운영 환경에서 deadlock이 자주 생기는 패턴
실제로는 아래 네 가지가 특히 많습니다.
같은 자원을 다른 순서로 갱신하는 경우
예를 들어 한 경로는 항상 users -> orders 순서로 잠그고, 다른 경로는 orders -> users 순서로 잠그면 순환 대기가 생기기 쉽습니다.
특히 서비스가 커지면 다음 조합에서 자주 나옵니다.
- API 요청 경로와 백그라운드 worker
- 관리자 배치와 사용자 실시간 요청
- 서로 다른 마이크로서비스 또는 job runner
트랜잭션 안에서 너무 많은 row를 만지는 경우
bulk update, 큰 batch 처리, 정렬되지 않은 ID 목록 갱신은 deadlock 가능성을 빠르게 키웁니다. 실제로 한 프로젝트에서 정산 배치가 5,000건의 주문을 한 트랜잭션에서 갱신하다가 API 서버의 실시간 주문 처리와 충돌하는 패턴이 있었습니다. 배치를 500건 단위로 쪼개고 ID를 정렬해서 넣었더니 deadlock이 거의 사라졌습니다. 트랜잭션이 길어질수록 잠금도 오래 유지되고, 충돌 가능한 조합이 늘어나기 때문입니다.
supporting index가 약해서 잠금 범위가 넓어지는 경우
애플리케이션 입장에서는 한두 row만 만진다고 생각해도, 실제 실행 계획은 더 넓은 범위를 스캔할 수 있습니다. 그러면 deadlock은 “두 쿼리가 이상해서”라기보다 예상보다 넓은 lock footprint가 겹쳐서 생깁니다.
트랜잭션 경계 안에 느린 작업이 섞인 경우
트랜잭션 안에서 외부 API 호출, 파일 I/O, 긴 비즈니스 로직, 과도한 루프가 돌면 잠금 유지 시간이 길어집니다. 그 자체가 deadlock을 만들지는 않더라도, 충돌이 순환 구조로 발전할 확률을 높입니다.
4. 장애가 났을 때 가장 먼저 봐야 할 명령
live incident에서 제일 가치가 큰 출발점은 보통 이것입니다.
SHOW ENGINE INNODB STATUS\G
SHOW FULL PROCESSLIST;
SHOW ENGINE INNODB STATUS\G 에서는 보통 LATEST DETECTED DEADLOCK 구간이 핵심입니다. 여기에서 특히 봐야 할 것은:
- 충돌에 걸린 두 트랜잭션의 SQL
- 어떤 테이블과 index가 관여했는지
- 어떤 lock mode를 기다렸는지
- MySQL이 어느 트랜잭션을 롤백했는지
SHOW FULL PROCESSLIST 는 deadlock 직후 전체 세션 상태를 빠르게 훑는 데 도움이 됩니다. 특히 아래를 같이 보세요.
- 비정상적으로 오래 살아 있는 세션
Sleep처럼 보여도 애플리케이션 쪽에서 다시 사용될 풀 연결- lock wait가 많은 시점에 몰린 worker나 batch session
deadlock이 드물게 발생해 바로 캡처가 어렵다면, 잠시 innodb_print_all_deadlocks = ON 으로 두고 MySQL error log에 deadlock 정보를 남기는 방법도 많이 씁니다.
핵심은 실패한 요청 하나만 보는 게 아니라, 그 시점에 동시에 살아 있던 다른 트랜잭션까지 같이 잡는 것입니다.
경험상 deadlock은 새벽 배치가 도는 시간대에 가장 많이 터집니다. 배치 타이밍과 사용자 트래픽이 겹치는 시점을 먼저 잡으면 원인을 훨씬 빨리 좁힐 수 있습니다.
5. root cause를 추적할 때 실제로 따라가면 좋은 순서
deadlock 로그를 봐도 처음엔 “그래서 누가 먼저 잘못한 거지?”가 잘 안 잡힙니다. 그럴 때는 아래 순서가 가장 실용적입니다.
1. 실패한 엔드포인트 또는 job부터 식별합니다
- 어떤 API였는지
- 어떤 worker였는지
- 어떤 배치였는지
- 같은 시각에 같이 돌던 다른 흐름은 무엇이었는지
deadlock은 단일 SQL보다 두 흐름의 만남이 중요해서, 요청 경로 이름이 빨리 잡혀야 합니다.
2. 트랜잭션 경계를 코드에서 다시 확인합니다
로그상으로는 짧아 보여도 실제 코드에서는 아래가 트랜잭션 안에 숨어 있을 수 있습니다.
- ORM relation loading
- 후속 검증 로직
- 외부 호출
- 여러 repository 호출
개발자가 생각한 “짧은 UPDATE 한 번”이 실제로는 긴 트랜잭션인 경우가 꽤 많습니다.
3. 두 흐름의 접근 순서를 나란히 적어봅니다
예를 들어:
- checkout flow:
inventory -> orders -> payments - admin cancel flow:
orders -> inventory -> refunds
이렇게만 써봐도 deadlock 후보가 바로 보이는 경우가 많습니다. 순서가 다르면, concurrency가 올라갔을 때 순환 대기가 생기기 쉽습니다.
4. 어떤 row 하나가 아니라 어떤 range가 잠겼는지도 확인합니다
deadlock은 꼭 같은 primary key 하나를 두고만 생기지 않습니다. range condition, secondary index, next-key lock 때문에 예상보다 넓은 영역이 겹칠 수 있습니다.
그래서 아래 질문을 같이 해야 합니다.
WHERE절을 제대로 받쳐주는 index가 있는가- 정렬과 조건 조합 때문에 넓은 스캔이 생기지 않는가
- 같은 쿼리라도 isolation level과 access path 때문에 더 큰 범위를 잠그는가
실행 계획 측면은 MySQL EXPLAIN 가이드와 MySQL 인덱스 설계 가이드가 같이 도움이 됩니다.
5. 왜 지금부터 자주 터졌는지 배포와 트래픽 맥락을 봅니다
deadlock은 코드가 오랫동안 있어도 특정 시점부터 급증할 수 있습니다.
- 동시성 증가
- 새 worker 추가
- batch 시간대 변경
- 새 정렬 조건이나 새 필터 도입
- 인덱스 누락 상태에서 데이터량 증가
즉 deadlock은 SQL 문법 문제보다 운영 맥락 변화가 기존 취약 구조를 드러낸 사건일 때가 많습니다.
6. 접근 순서를 통일하면 왜 효과가 큰가
deadlock 완화에서 가장 재현성 높게 먹히는 방법 중 하나가 접근 순서 통일입니다.
예를 들어 여러 흐름이 결국 account, invoice, ledger 세 자원을 건드린다면, 어느 경로든 항상 같은 순서로 잠그게 맞추는 편이 좋습니다.
accountinvoiceledger
bulk update에서도 비슷합니다. ID 목록을 처리할 때 순서를 통일하지 않으면 worker마다 잠그는 순서가 뒤엉켜 deadlock 확률이 올라갑니다. 그래서 실무에서는 아래 같은 습관이 꽤 중요합니다.
- 갱신 대상 ID를 정렬한 뒤 처리
- 여러 row를 만질 때 순서를 고정
- 비슷한 비즈니스 동작을 여러 서비스에서 공유한다면 locking order를 문서화
deadlock은 생각보다 자주 “대단한 튜닝”보다 순서를 맞추는 작은 합의로 크게 줄어듭니다.
7. 트랜잭션 범위와 쿼리 계획이 같이 중요하다
deadlock을 순서 문제로만 보면 절반만 본 셈입니다. 같은 순서여도 트랜잭션이 너무 길거나, index가 약해서 범위가 넓으면 충돌 자체가 자주 납니다.
특히 아래는 위험합니다.
- 트랜잭션 안에서 여러 페이지를 batch로 순회
SELECT ... FOR UPDATE범위가 생각보다 넓음- secondary index condition이 약해서 많은 row를 건드림
- 한 트랜잭션에서 너무 많은 업데이트를 몰아서 수행
그래서 deadlock 대응은 보통 두 축을 동시에 봐야 합니다.
- 순서를 통일할 수 있는가
- 범위를 줄일 수 있는가
범위를 줄이는 방법은 보통 아래에서 나옵니다.
- 더 좋은 supporting index 추가
- batch size 축소
- 긴 작업을 여러 짧은 트랜잭션으로 분리
- 외부 API 호출이나 무거운 계산을 트랜잭션 밖으로 이동
즉 deadlock은 locking order 문제이면서 동시에 transaction design 문제입니다.
8. retry는 필요하지만, 근본 해결로 착각하면 안 된다
deadlock은 MySQL이 한 트랜잭션을 희생시키며 끝내기 때문에, 애플리케이션에서 retry를 두는 건 현실적인 선택입니다. 다만 전제가 있습니다.
- 작업이 idempotent 해야 합니다.
- 중복 실행 시 부작용이 없어야 합니다.
- 무한 재시도 말고 소수 재시도와 backoff를 둬야 합니다.
- retry 비율과 deadlock 발생률을 모니터링해야 합니다.
retry가 유용한 이유는 사용자에게 일시적 충돌을 덜 느끼게 하기 때문입니다. 하지만 retry만으로 버티면:
- 근본 충돌 구조는 그대로 남고
- 트래픽이 늘수록 다시 터지고
- 심하면 재시도 자체가 부하를 더 키웁니다
그래서 retry는 airbag 이고, root fix는 접근 순서와 트랜잭션 범위 재설계입니다.
9. 자주 하는 실수
1. deadlock을 랜덤한 DB 일시 장애로만 보는 것
물론 deadlock은 동시성 환경에서 어느 정도 자연스럽게 생길 수 있습니다. 하지만 반복된다면 거의 항상 구조적 원인이 있습니다.
2. 롤백된 마지막 쿼리만 튜닝하는 것
실패한 쿼리는 피해자일 수 있습니다. 함께 충돌한 다른 트랜잭션과 순서를 보지 않으면 반쪽짜리 수정이 됩니다.
3. innodb_lock_wait_timeout 또는 커넥션 수치로 해결하려는 것
deadlock은 timeout을 늘린다고 해결되지 않습니다. 커넥션을 늘려도 충돌 구조는 그대로입니다.
4. 재시도를 넣었으니 끝났다고 생각하는 것
재시도는 필요할 수 있지만, deadlock 빈도가 계속 높다면 그건 사용자 경험과 인프라 비용을 같이 갉아먹습니다.
FAQ
Q. deadlock은 완전히 없어져야 하나요?
동시성 높은 시스템에서는 드물게 남을 수 있습니다. 목표는 “0건 집착”보다, 반복 패턴을 줄이고 중요 경로에서 체감 실패율을 낮추는 것입니다.
Q. 가장 먼저 바꿔야 할 것은 무엇인가요?
대부분은 최근 deadlock 로그 두세 건만 비교해도 공통된 접근 순서 차이 또는 과도한 트랜잭션 범위가 보입니다. 먼저 그 공통 패턴을 찾는 게 좋습니다.
Q. deadlock이 나면 트랜잭션을 없애야 하나요?
아닙니다. 트랜잭션이 문제라기보다 설계와 범위가 문제일 가능성이 큽니다. 트랜잭션은 유지하되, 짧고 예측 가능하게 만드는 쪽이 일반적으로 맞습니다.
Read Next
- 트랜잭션 경계와 롤백, 커밋 관점은 MySQL Transaction 가이드를 함께 보면 좋습니다.
- 잠금 범위를 키우는 실행 계획과 인덱스 문제는 MySQL EXPLAIN 가이드와 MySQL 인덱스 설계 가이드가 연결됩니다.
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- 미들웨어 트러블슈팅 가이드: Redis, RabbitMQ, Kafka 중 어디부터 볼까 Redis, RabbitMQ, Kafka가 함께 있는 시스템에서 지금 보이는 장애가 어느 계층에 더 가까운지, 첫 10분 안에 무엇을 확인하고 어떤 글로 들어가야 하는지 정리한 실전 허브 가이드입니다.
- Kubernetes CrashLoopBackOff: 먼저 볼 것들 startup failure, probe, config, resource limit 관점에서 CrashLoopBackOff를 어떻게 나눠서 봐야 하는지 정리한 가이드입니다.
- Astro 기술 블로그 SEO 체크리스트: 트래픽 기다리기 전에 먼저 고칠 것 Astro 기술 블로그를 위한 실전 SEO 체크리스트입니다. 배포 호스트 확인, robots.txt, sitemap, canonical, hreflang, 구조화 데이터, 페이지별 메타데이터, noindex 판단, 검증 명령까지 우선순위대로 정리합니다.
- 다국어 블로그 canonical과 hreflang 설정 가이드: 무엇을 확인하고 어디서 깨질까 다국어 블로그에서 canonical과 hreflang을 어떻게 설정해야 하는지 실전 기준으로 정리합니다. self-canonical, 상호 연결되는 hreflang 묶음, x-default, 카테고리 페이지, 최종 렌더 HTML 점검, 한 언어 버전이 다른 언어 버전을 눌러버리는 실수까지 다룹니다.
- OpenAI Codex CLI 설치 가이드: 설치, 인증, 첫 작업까지 OpenAI Codex CLI를 실전 기준으로 설치하는 방법을 정리했다. 설치, 로그인, 첫 실행, Windows 주의점, 첫 작업을 어떻게 시작하면 좋은지까지 다룬다.