본문 바로가기
소프트웨어개발

PostgreSQL deadlock detected 해결: 40P01·잠금 순서·트랜잭션 재시도

by 아빠띠띠뽀 2026. 9. 30.

PostgreSQL 트랜잭션 두 개가 잠금을 교차 획득해 교착 상태에 빠지는 흐름과 해결 방향을 나타낸 일러스트
AI로 직접 제작한 개념 일러스트입니다. 교차 잠금으로 생긴 교착 상태를 일관된 잠금 순서와 재시도로 해결하는 흐름을 보여줍니다.

PostgreSQL 로그에 deadlock detected가 나타나면 데이터베이스가 멈춘 것이 아니라 서로의 잠금을 기다리는 트랜잭션 중 하나를 중단해 순환을 끊은 상황입니다. 애플리케이션에는 보통 SQLSTATE 40P01로 전달됩니다. 무작정 max_connections를 올리거나 모든 세션을 종료하기 전에, 어떤 순서로 잠금을 잡았는지와 재시도 범위를 확인하세요. 이 글은 두 세션의 간단한 예와 운영 환경에서 증거를 모으는 방법을 설명합니다.

1. 두 트랜잭션이 서로 기다리는 구조

테스트 데이터베이스에 accounts(id, balance)라는 테이블과 id 1, 2가 있다고 가정합니다. 세션 A가 1번 행을 수정하고 세션 B가 2번 행을 수정한 뒤, 서로 상대가 잡은 행을 수정하려고 하면 대기 순환이 만들어집니다. 아래는 개념을 보여 주는 예시이며 운영 데이터베이스에서 그대로 실행하지 마세요.

-- 세션 A                         -- 세션 B
BEGIN;                            BEGIN;
UPDATE accounts SET balance=balance WHERE id=1;
                                  UPDATE accounts SET balance=balance WHERE id=2;
UPDATE accounts SET balance=balance WHERE id=2; -- 대기
                                  UPDATE accounts SET balance=balance WHERE id=1; -- 교착

PostgreSQL은 교착을 감지하면 한 트랜잭션을 중단합니다. 다른 트랜잭션은 진행할 수 있지만 중단된 쪽의 작업은 전체 트랜잭션 관점에서 다시 판단해야 합니다. 공식 잠금 문서는 여러 객체의 잠금을 일관된 순서로 잡는 것을 핵심 예방책으로 설명합니다.

2. 로그와 현재 대기를 함께 모으세요

실패한 요청의 시각, SQLSTATE, 트랜잭션에 포함된 SQL 순서, 대상 행을 로그에서 확인합니다. 현재도 기다리는 세션이 있다면 다음 읽기 전용 쿼리로 누가 잠금 대기 중인지 살펴볼 수 있습니다. 민감한 SQL이나 값이 로그에 남을 수 있으니 운영 환경의 로그 접근 범위를 지키세요.

SELECT pid, state, wait_event_type, wait_event,
       pg_blocking_pids(pid) AS blocking_pids,
       left(query, 160) AS query_preview
FROM pg_stat_activity
WHERE datname = current_database()
  AND wait_event_type = 'Lock';

pg_stat_activity와 pg_blocking_pids()는 현재 대기를 분석하는 데 도움이 되지만 이미 끝난 교착의 전체 순서를 복원하지는 못합니다. 그때는 서버 로그와 애플리케이션 추적 정보를 함께 봐야 합니다. PostgreSQL 활동 모니터링 문서에서 뷰와 함수의 의미를 확인하세요.

3. 비슷해 보이는 장애를 구분하는 표

증상 의미 먼저 볼 증거
deadlock detected / 40P01 잠금 대기 순환을 감지해 한 트랜잭션 중단 충돌한 SQL과 잠금 획득 순서
단순 잠금 대기 다른 트랜잭션이 잠금을 풀 때까지 대기 blocking_pids, 오래 열린 트랜잭션
lock timeout 설정된 대기 시간 한도를 넘음 lock_timeout 값과 대기 대상
too many connections 접속 슬롯 부족 연결 수, 풀 설정, 장기 세션

마지막 증상은 잠금 교착과 별개의 문제입니다. 연결 고갈을 조사 중이라면 기존 PostgreSQL too many connections 점검 글의 연결 풀과 세션 수 절차를 참고하세요. 서로 다른 오류를 한 번에 해결하려고 설정값을 올리는 것은 원인 분석을 어렵게 합니다.

4. 잠금 순서와 트랜잭션 길이를 수정하세요

여러 행 또는 테이블을 바꾸는 모든 코드 경로가 동일한 순서로 잠금을 얻도록 설계합니다. 예를 들어 계좌 1과 2를 옮기는 작업이라면 요청 방향과 무관하게 작은 id부터 처리하는 방식을 검토합니다. 트랜잭션 중 사용자 입력, 외부 API 응답, 긴 파일 작업을 기다리지 않도록 범위를 줄이세요. 인덱스와 실행 계획을 확인해 예상보다 넓은 행 집합을 오래 잠그고 있지 않은지도 검토할 가치가 있습니다.

코드가 여러 서비스에 흩어져 있다면 각 서비스의 SQL 실행 순서를 표로 적어 비교합니다. 데이터베이스 설정을 바꾸기 전에 같은 자원을 서로 다른 순서로 수정하는 경로를 찾아내는 편이 재발 방지에 직접적입니다.

5. 재시도는 트랜잭션 전체를 대상으로 제한하세요

교착은 정상적인 동시성 환경에서도 드물게 발생할 수 있으므로 완전 제거만 목표로 삼지 않습니다. SQLSTATE 40P01을 구분해 짧은 지연과 최대 횟수가 있는 재시도를 설계하되, 실패한 SQL 한 줄만 다시 보내서는 앞선 트랜잭션 상태가 복구되지 않습니다. 트랜잭션 전체를 처음부터 시작하고 외부 결제·메일 발송 같은 부수 효과는 중복되지 않게 분리해야 합니다. 재시도 횟수와 실패율도 운영 지표로 남기세요.

공식 잠금 설정 문서에 따르면 deadlock_timeout은 교착 검사를 시작하기까지 기다리는 시간입니다. 이를 줄이는 것은 탐지 시점과 검사 비용에 영향을 주며 교착의 원인을 고치지 않습니다. log_lock_waits와 함께 조사할 때에도 운영 부하와 로그량을 검토해야 합니다.

6. 자주 묻는 질문

PostgreSQL을 재시작하면 해결되나요? 현재 세션은 사라질 수 있지만 잠금 획득 순서가 그대로라면 재발합니다. 먼저 충돌하는 트랜잭션 경로를 고치세요.

40P01이면 무조건 재시도해도 되나요? 전체 트랜잭션을 재시작할 수 있고 외부 부수 효과가 중복되지 않는 경우에만 제한된 재시도를 적용하세요.

deadlock_timeout을 늘리면 교착이 없어지나요? 아닙니다. 감지·보고 시점이 달라질 뿐 대기 순환을 만든 SQL 순서는 남습니다.

자료 확인: 2026년 9월 30일. PostgreSQL 버전과 권한에 따라 모니터링 정보의 범위가 달라질 수 있습니다.