이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Deadlock과 lock wait를 어떻게 구분하고 대응해야 하는가?
- deadlock 로그가 나오면 백엔드 개발자는 무엇을 봐야 하는가?
- 재시도만으로 해결하면 안 되는 deadlock은 어떤 신호를 보이는가?
개요
Lock wait는 기다림이고, deadlock은 순환 대기다. 둘 다 API 지연과 실패를 만들지만 대응은 다르다. Lock wait는 blocking transaction을 찾고 줄여야 하며, deadlock은 접근 순서와 트랜잭션 구조를 바꿔야 할 수 있다.
Deadlock은 완전히 0으로 만들기 어려운 경우도 있지만, 빈번한 deadlock은 설계 냄새다.
원리
Deadlock과 lock wait를 다루는 원리는 대기 그래프를 백엔드 코드 경로로 되돌리는 것이다. DB 로그에는 SQL과 세션이 보이지만, 실제 원인은 “어떤 API와 batch가 어떤 순서로 row를 잡았는가”, “트랜잭션 안에서 왜 오래 머물렀는가”, “조건을 지원하는 인덱스가 있었는가”에 있다.
Lock wait는 blocking transaction을 줄이고, deadlock은 순환 대기를 줄인다. 둘 다 timeout이나 재시도로 숨길 수 있지만, 빈도가 늘면 트랜잭션 경계, 접근 순서, 인덱스, batch 설계를 바꿔야 한다.
Lock Wait
다음 흐름은 lock wait를 만든다.
T1: UPDATE products SET stock = stock - 1 WHERE id = 10; -- commit 안 함
T2: UPDATE products SET stock = stock - 1 WHERE id = 10; -- T1 대기T2는 T1이 commit 또는 rollback할 때까지 기다린다. T1이 외부 API를 호출하느라 오래 걸리면 T2와 뒤 요청들이 줄줄이 대기한다.
Deadlock
Deadlock은 서로의 lock을 기다리는 순환이다.
T1: A lock 획득 -> B lock 대기
T2: B lock 획득 -> A lock 대기DB는 한쪽을 롤백시키고 오류를 반환한다. 애플리케이션은 이 오류를 잡아 재시도하거나 사용자에게 적절히 실패를 알려야 한다.
코드 원인
서로 다른 순서로 업데이트하는 코드가 대표적이다.
@Transactional
public void mergeTeam(Long sourceTeamId, Long targetTeamId) {
Team source = teamRepository.findByIdForUpdate(sourceTeamId);
Team target = teamRepository.findByIdForUpdate(targetTeamId);
target.merge(source);
}동시에 반대 방향 요청이 들어오면 위험하다. 항상 ID 오름차순으로 lock을 잡거나, 비즈니스적으로 동시에 실행되지 않게 큐잉한다.
확인 쿼리
PostgreSQL에서 대기 상태를 본다.
SELECT blocked.pid AS blocked_pid,
blocked.query AS blocked_query,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_locks blocked_locks
ON blocked_locks.pid = blocked.pid
JOIN pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid <> blocked_locks.pid
JOIN pg_stat_activity blocking
ON blocking.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted
AND blocking_locks.granted;MySQL에서는 SHOW ENGINE INNODB STATUS에서 최근 deadlock 정보를 확인할 수 있다.
SHOW ENGINE INNODB STATUS;운영에서는 직접 실행 권한이 없을 수 있으므로 인프라/DBA에게 시간대와 문제 SQL을 전달한다.
인프라/DBA에게는 “deadlock이 났습니다”만 보내지 말고 다음을 함께 전달한다.
시간대: 2026-07-01 14:10~14:18 KST
영향 API: POST /teams/{source}/merge/{target}
관련 SQL: team for update 2회, team_members update
최근 변경: batch가 팀 멤버 재배치 작업 시작
의심: API는 source->target 순서, batch는 target->source 순서로 lock
요청: deadlock log의 두 트랜잭션 SQL과 blocking 관계 확인재시도 기준
Deadlock victim은 재시도 후보지만 무조건 재시도하면 안 된다.
- 작업이 짧은가?
- 멱등성이 있는가?
- 같은 요청을 여러 번 실행해도 안전한가?
- deadlock 빈도가 낮은가?
- backoff와 최대 횟수가 있는가?
빈번한 deadlock은 재시도보다 원인 제거가 우선이다.
실전 팁
- 여러 테이블을 업데이트하는 순서를 팀 규칙으로 정한다.
- 사용자 API와 batch가 같은 데이터를 업데이트하면 시간대나 범위를 분리한다.
- deadlock 로그의 SQL 두 개를 코드 경로로 역추적한다.
- lock wait timeout과 deadlock 오류를 구분해 로깅한다.
- 재시도는 짧은 트랜잭션과 멱등 작업에만 제한한다.
- 여러 row를 잠글 때는 ID 정렬 같은 전역 순서를 강제한다.
- deadlock과 lock wait는 error code, SQLState, Spring 예외 타입을 구분해 metric으로 남긴다.
- blocking query를 종료하기 전에는 rollback 영향, 사용자 요청 실패, batch 재시작 가능성을 확인한다.
위험 신호!
- deadlock이 매일 발생하는데 재시도로만 숨긴다.
- 어떤 코드 경로가 lock을 잡는지 문서화되어 있지 않다.
- batch가 사용자 API와 반대 순서로 테이블을 업데이트한다.
- deadlock 로그를 수집하지 않는다.
- 재시도에 backoff가 없다.
- lock wait timeout을 늘려 사용자 요청을 더 오래 붙잡게 만든다.
- deadlock victim 재시도 성공률만 보고 발생 빈도 증가를 무시한다.
- deadlock 로그의 SQL을 코드 경로와 배포 이력에 연결하지 않는다.
확인 질문
- Lock wait와 deadlock의 대응 차이는 무엇인가?
- Lock wait는 blocking transaction과 긴 lock 보유를 줄이는 것이 우선이고, deadlock은 lock 획득 순서와 트랜잭션 구조를 바꾸는 것이 중요하다.
- Deadlock victim을 재시도할 때 필요한 조건은 무엇인가?
- 작업이 멱등적이고 짧으며, backoff와 최대 횟수가 있고, 중복 부작용이 없어야 한다.
- 빈번한 deadlock이 위험 신호인 이유는 무엇인가?
- 접근 순서, 인덱스, 트랜잭션 경계, batch 충돌 같은 구조적 문제가 반복되고 있을 가능성이 높기 때문이다.
- deadlock 로그를 받은 백엔드 개발자가 해야 할 일은 무엇인가?
- 두 트랜잭션의 SQL을 코드 경로로 매핑하고, lock 획득 순서, 트랜잭션 길이, 관련 인덱스, batch/API 충돌 여부를 확인해야 한다.