PostgreSQL 55P03 오류 원인과 해결 방법 완벽 가이드

55P03
2026년 09월 21일 | DBMS Error 가이드

이 글에서 다루는 내용

55P03 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.

55P03 lock not available 는?

PostgreSQL 에러 코드 55P03 lock_not_availableNOWAIT 옵션을 사용한 잠금 획득 시도에서 즉시 잠금을 얻을 수 없을 때 발생합니다. 일반적인 락 대기와 달리, NOWAIT 또는 lock_timeout 설정을 통해 “기다리지 않겠다”고 명시한 경우 다른 트랜잭션이 이미 해당 리소스를 점유하고 있으면 즉각적으로 이 에러가 발생합니다. 운영 환경에서는 배치 작업, 스키마 변경(ALTER TABLE, VACUUM FULL), 또는 고동시성 OLTP 시스템에서 빈번하게 목격됩니다.


주요 발생 원인

1. NOWAIT 옵션 사용 중 다른 세션의 잠금 충돌

SELECT ... FOR UPDATE NOWAIT 또는 LOCK TABLE ... NOWAIT 구문을 사용할 때, 대상 행이나 테이블을 다른 세션이 이미 잠그고 있으면 즉시 에러가 발생합니다. 이는 의도적으로 설계된 동작이지만, 애플리케이션 레벨에서 재시도 로직 없이 사용하면 치명적인 오류로 이어집니다. 특히 결제, 재고 차감 등 동시 접근이 많은 테이블에서 자주 발생합니다.

2. lock_timeout 설정으로 인한 자동 에러 발생

SET lock_timeout = '3s'처럼 타임아웃을 설정한 상태에서 지정 시간 내에 잠금을 획득하지 못하면 55P03 에러가 발생합니다. DBA나 개발자가 데드락 방지 목적으로 lock_timeout을 설정하는 경우가 많은데, 이 값이 너무 짧으면 정상적인 트랜잭션도 실패하게 됩니다. 특히 장기 실행 트랜잭션이 존재하는 환경에서 더욱 빈번하게 발생합니다.

3. DDL 작업(ALTER TABLE, VACUUM FULL 등) 중 잠금 경합

스키마 변경 작업(ALTER TABLE ADD COLUMN, VACUUM FULL, REINDEX 등)은 AccessExclusiveLock을 요구하기 때문에, 해당 테이블에 활성 쿼리가 존재하는 상황에서 NOWAIT를 사용하거나 lock_timeout이 짧으면 즉시 실패합니다. 운영 중 스키마 변경은 항상 잠금 경합 위험이 높으며, 특히 트래픽이 많은 시간대에 이러한 작업을 수행하면 에러가 반복됩니다.


해결 방법

원인 1 해결: NOWAIT 사용 시 재시도 로직 추가

애플리케이션 레벨에서 55P03 에러를 캐치하고 일정 횟수 재시도하는 패턴을 구현합니다.

-- 잘못된 예: 재시도 없이 NOWAIT 사용
BEGIN;
SELECT * FROM orders WHERE order_id = 12345 FOR UPDATE NOWAIT;
-- 다른 세션이 잠근 경우 즉시 에러 발생
COMMIT;

-- 올바른 예: 재시도 로직을 포함한 함수 예시
CREATE OR REPLACE FUNCTION try_lock_order(p_order_id INT, p_max_retry INT DEFAULT 3)
RETURNS BOOLEAN AS $$
DECLARE
    v_retry INT := 0;
BEGIN
    LOOP
        BEGIN
            PERFORM * FROM orders WHERE order_id = p_order_id FOR UPDATE NOWAIT;
            RETURN TRUE; -- 잠금 성공
        EXCEPTION
            WHEN lock_not_available THEN
                v_retry := v_retry + 1;
                IF v_retry >= p_max_retry THEN
                    RAISE WARNING '잠금 획득 실패: order_id=%, 재시도 횟수 초과', p_order_id;
                    RETURN FALSE;
                END IF;
                -- 100ms 대기 후 재시도
                PERFORM pg_sleep(0.1);
        END;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

-- 사용 예
SELECT try_lock_order(12345, 5);

원인 2 해결: lock_timeout 값 적절히 조정

-- 현재 lock_timeout 확인
SHOW lock_timeout;

-- 세션 레벨에서 타임아웃 조정 (너무 짧지 않게 설정)
SET lock_timeout = '10s';

-- 특정 작업에만 타임아웃 적용 후 복원
BEGIN;
SET LOCAL lock_timeout = '30s';
SELECT * FROM inventory WHERE item_id = 99 FOR UPDATE;
-- ... 작업 수행 ...
COMMIT;
-- COMMIT 후 LOCAL 설정은 자동 복원됨

-- 현재 잠금 대기 중인 세션 확인 쿼리
SELECT
    pid,
    now() - pg_stat_activity.query_start AS duration,
    query,
    state,
    wait_event_type,
    wait_event
FROM pg_stat_activity
WHERE wait_event_type = 'Lock'
ORDER BY duration DESC;

원인 3 해결: DDL 작업 전 잠금 경합 확인 및 안전한 스키마 변경

-- DDL 실행 전 해당 테이블의 잠금 현황 조회
SELECT
    l.pid,
    l.granted,
    l.locktype,
    l.mode,
    a.query,
    a.state,
    now() - a.query_start AS query_duration
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE l.relation = 'your_table'::regclass
  AND l.pid <> pg_backend_pid()
ORDER BY query_duration DESC;

-- 장기 실행 트랜잭션 강제 종료 후 DDL 수행 (주의하여 사용)
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE pid <> pg_backend_pid()
  AND state = 'idle in transaction'
  AND now() - query_start > interval '10 minutes';

-- 안전한 컬럼 추가 예시 (lock_timeout 활용)
BEGIN;
SET LOCAL lock_timeout = '5s';
ALTER TABLE orders ADD COLUMN discount_rate NUMERIC(5,2) DEFAULT 0;
COMMIT;

-- pg_repack 활용 (운영 중 VACUUM FULL 대체)
-- pg_repack은 AccessExclusiveLock 없이 테이블 재구성 가능
-- 설치 후: pg_repack -t your_table -d your_database

예방 방법

1. 잠금 모니터링 및 알림 시스템 구축

장기 잠금 대기를 실시간으로 감지하는 모니터링 쿼리를 주기적으로 실행하고, Prometheus + Grafana 또는 PgBadger 같은 도구를 활용하여 잠금 경합이 임계값을 초과할 때 알림을 받도록 설정합니다. 아래 쿼리를 크론잡 또는 모니터링 에이전트에 등록해 두면 실무에서 매우 유용합니다.

-- 잠금 대기 5초 이상인 세션 조회 (모니터링용)
SELECT
    blocked.pid AS blocked_pid,
    blocked.query AS blocked_query,
    blocking.pid AS blocking_pid,
    blocking.query AS blocking_query,
    now() - blocked.query_start AS wait_duration
FROM pg_stat_activity AS blocked
JOIN pg_stat_activity AS blocking
    ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0
  AND now() - blocked.query_start > interval '5 seconds';

2. 트랜잭션 범위 최소화 및 idle in transaction 방지

트랜잭션 내에서 외부 API 호출, 대용량 파일 처리, 사용자 입력 대기 등을 포함하지 않도록 애플리케이션을 설계하여 트랜잭션이 최대한 짧게 유지되도록 합니다. idle in transaction 상태의 세션은 잠금을 계속 보유하므로, idle_in_transaction_session_timeout 파라미터를 설정하여 일정 시간 후 자동 종료되도록 구성합니다.

-- postgresql.conf 또는 세션 레벨에서 설정
SET idle_in_transaction_session_timeout = '5min';

-- 또는 특정 사용자/데이터베이스 기본값으로 설정
ALTER ROLE app_user SET idle_in_transaction_session_timeout = '5min';
ALTER DATABASE mydb SET idle_in_transaction_session_timeout = '5min';

관련 에러

  • 40P01 deadlock_detected: 두 트랜잭션이 서로의 잠금을 대기하며 교착 상태가 발생한 경우로, 55P03과 함께 잠금 관련 문제의 양대 에러입니다.
  • 55006 object_in_use: 이미 사용 중인 데이터베이스 객체에 대한 접근 시 발생하며 잠금 충돌과 유사한 맥락을 가집니다.
  • 57014 query_canceled: statement_timeout 초과로 쿼리가 취소될 때 발생하며, lock_timeout과 함께 시간 제한 관련 에러 쌍을 이룹니다.
  • 40001 serialization_failure: Serializable 격리 수준에서 직렬화 충돌 시 발생하며, 재시도 로직이 필요하다는 점에서 55P03 처리 패턴과 유사합니다.

DBMS 에러 코드 시리즈

주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.

본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.

댓글 남기기