2026년 07월 18일 | DBMS Error 가이드
이 글에서 다루는 내용
55P03 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
55P03 lock not available 는?
PostgreSQL 에러 코드 55P03 (lock not available)은 NOWAIT 옵션을 사용하여 락(Lock)을 획득하려 했지만, 해당 리소스가 이미 다른 트랜잭션에 의해 잠겨 있어 즉시 락을 획득할 수 없을 때 발생합니다. 일반적인 락 대기와 달리, NOWAIT 모드에서는 대기하지 않고 즉시 에러를 반환하는 것이 핵심 특징입니다. 주로 SELECT ... FOR UPDATE NOWAIT, LOCK TABLE ... NOWAIT, 또는 SELECT ... FOR SHARE NOWAIT 구문을 사용하는 고동시성 환경에서 자주 마주치게 됩니다.
주요 발생 원인
1. NOWAIT 옵션과 동시 트랜잭션 충돌
NOWAIT 키워드는 락을 즉시 획득하지 못하면 기다리지 않고 바로 에러를 발생시키도록 설계된 옵션입니다. 두 개 이상의 트랜잭션이 동시에 동일한 행 또는 테이블에 접근하려 할 때, 먼저 락을 획득한 트랜잭션이 커밋 또는 롤백되기 전까지 나머지 트랜잭션은 55P03 에러를 받게 됩니다. 특히 고빈도 주문 처리 시스템이나 재고 관리 시스템처럼 특정 행에 대한 경쟁이 극심한 환경에서 빈번하게 발생합니다.
2. 장기 실행 트랜잭션(Long-Running Transaction)에 의한 락 점유
배치 작업이나 분석 쿼리처럼 오랜 시간 동안 트랜잭션을 열어 두는 경우, 해당 트랜잭션이 특정 테이블이나 행에 대한 락을 오랫동안 보유하게 됩니다. 이 상태에서 다른 트랜잭션이 NOWAIT 옵션으로 동일 리소스에 접근을 시도하면 락을 즉시 획득하지 못해 에러가 발생합니다. 이런 케이스는 모니터링 없이 방치되면 애플리케이션 전체의 처리량(Throughput)을 심각하게 저하시킬 수 있습니다.
3. DDL 작업과 DML 작업 간의 락 충돌
ALTER TABLE, DROP INDEX, TRUNCATE와 같은 DDL 작업은 테이블 수준의 강한 배타적 락(AccessExclusiveLock)을 요구합니다. 이 DDL 작업이 진행 중인 상황에서 해당 테이블에 NOWAIT 옵션을 포함한 DML 쿼리가 실행되거나, 반대로 DML이 테이블을 점유한 상황에서 DDL이 NOWAIT으로 실행될 경우 55P03 에러가 발생합니다. 운영 중인 테이블에 온라인 스키마 변경을 시도할 때 특히 위험하며, 배포 자동화 파이프라인에서도 자주 나타나는 원인 중 하나입니다.
해결 방법
원인 1 해결: NOWAIT 대신 SKIP LOCKED 또는 재시도 로직 사용
NOWAIT 대신 SKIP LOCKED를 사용하면 락이 걸린 행을 건너뛰고 나머지 행을 처리할 수 있어 큐(Queue) 패턴 구현에 매우 유용합니다.
-- SKIP LOCKED를 활용한 작업 큐 처리 패턴
BEGIN;
SELECT id, payload
FROM job_queue
WHERE status = 'pending'
ORDER BY created_at
LIMIT 10
FOR UPDATE SKIP LOCKED;
-- 가져온 행에 대해 처리 후 상태 업데이트
UPDATE job_queue
SET status = 'processing', started_at = NOW()
WHERE id = ANY(ARRAY[/* 위에서 가져온 id 목록 */]);
COMMIT;
애플리케이션 레벨에서는 재시도(Retry) 로직을 구현하여 55P03 에러 발생 시 일정 횟수만큼 재시도하도록 처리하는 것이 권장됩니다.
-- 재시도가 필요한 경우를 위한 예외 처리 구조 (PL/pgSQL)
DO $$
DECLARE
retry_count INT := 0;
max_retries INT := 3;
success BOOLEAN := FALSE;
BEGIN
WHILE retry_count < max_retries AND NOT success LOOP
BEGIN
-- 락 획득 시도
PERFORM id FROM orders
WHERE order_id = 12345
FOR UPDATE NOWAIT;
-- 락 획득 성공 시 비즈니스 로직 수행
UPDATE orders
SET status = 'confirmed'
WHERE order_id = 12345;
success := TRUE;
EXCEPTION
WHEN lock_not_available THEN
retry_count := retry_count + 1;
RAISE NOTICE '락 획득 실패, 재시도 중... (% / %)', retry_count, max_retries;
PERFORM pg_sleep(0.1 * retry_count); -- 지수 백오프
END;
END LOOP;
IF NOT success THEN
RAISE EXCEPTION '최대 재시도 횟수 초과: 락 획득 실패';
END IF;
END;
$$;
원인 2 해결: 장기 실행 트랜잭션 감지 및 종료
현재 어떤 트랜잭션이 락을 보유하고 있는지 확인하고, 필요 시 해당 프로세스를 종료합니다.
-- 현재 락을 보유 중인 트랜잭션 조회
SELECT
pid,
usename,
application_name,
state,
wait_event_type,
wait_event,
now() - pg_stat_activity.query_start AS duration,
query
FROM pg_stat_activity
WHERE state != 'idle'
AND now() - pg_stat_activity.query_start > INTERVAL '5 minutes'
ORDER BY duration DESC;
-- 락 충돌 관계를 상세히 파악하는 쿼리
SELECT
blocked_locks.pid AS blocked_pid,
blocked_activity.usename AS blocked_user,
blocking_locks.pid AS blocking_pid,
blocking_activity.usename AS blocking_user,
blocked_activity.query AS blocked_statement,
blocking_activity.query AS blocking_statement,
now() - blocking_activity.query_start AS blocking_duration
FROM pg_catalog.pg_locks AS blocked_locks
JOIN pg_catalog.pg_stat_activity AS blocked_activity
ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks AS 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_catalog.pg_stat_activity AS blocking_activity
ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;
-- 특정 PID의 트랜잭션을 강제 종료 (주의하여 사용)
SELECT pg_terminate_backend(/* blocking_pid 값 입력 */);
원인 3 해결: DDL 작업 시 lock_timeout 설정
DDL 작업 전에 lock_timeout을 설정하면 락 대기 시간을 제한하여 시스템 전체가 멈추는 상황을 방지할 수 있습니다.
-- DDL 실행 전 타임아웃 설정
SET lock_timeout = '3s';
SET statement_timeout = '30s';
-- 이제 DDL 실행 시도
ALTER TABLE orders ADD COLUMN discount_rate NUMERIC(5,2) DEFAULT 0;
-- 작업 후 설정 복원
RESET lock_timeout;
RESET statement_timeout;
-- pg_try_advisory_lock을 활용한 비차단 방식 확인
SELECT pg_try_advisory_lock(12345);
-- TRUE 반환 시 락 획득 성공, FALSE 반환 시 다른 세션이 보유 중
예방 방법
1. lock_timeout과 statement_timeout을 세션 또는 역할(Role) 레벨로 기본 설정
운영 환경에서 무한정 락 대기 상태에 빠지는 것을 원천적으로 방지하려면, 데이터베이스 역할 또는 애플리케이션 커넥션 레벨에서 lock_timeout을 반드시 설정해야 합니다. NOWAIT과 달리 lock_timeout은 지정된 시간 동안은 대기하되 초과 시 에러를 발생시키므로, 짧은 락 충돌은 자연스럽게 해소될 기회를 줍니다.
-- 특정 역할에 기본 타임아웃 설정
ALTER ROLE app_user SET lock_timeout = '5s';
ALTER ROLE app_user SET statement_timeout = '30s';
-- 현재 설정값 확인
SHOW lock_timeout;
SHOW statement_timeout;
2. 트랜잭션을 짧고 명확하게 유지하고, 락 순서를 일관되게 통일
트랜잭션 내에서 락을 획득하는 순서를 애플리케이션 전체에서 일관되게 유지하면 데드락(Deadlock)과 락 경합을 대폭 줄일 수 있습니다. 또한 트랜잭션 내에서 불필요한 외부 API 호출이나 사용자 입력 대기 등을 제거하여 트랜잭션 지속 시간을 최소화해야 합니다. pg_stat_activity와 pg_locks 뷰를 주기적으로 모니터링하거나, auto_explain과 log_lock_waits = on 설정을 통해 락 대기 상황을 로그로 기록하는 습관을 들이는 것이 중요합니다.
-- postgresql.conf 또는 ALTER SYSTEM으로 락 대기 로깅 활성화
ALTER SYSTEM SET log_lock_waits = on;
ALTER SYSTEM SET deadlock_timeout = '1s'; -- 이 시간 초과 시 로그 기록
SELECT pg_reload_conf();
관련 에러
- 40P01 (deadlock_detected): 두 트랜잭션이 서로 상대방의 락을 기다리는 교착 상태. 55P03과 유사하게 락 관리 부재에서 비롯되며,
deadlock_timeout설정으로 감지 주기를 조정할 수 있습니다. - 55006 (object_in_use): 데이터베이스나 테이블스페이스가 현재 다른 세션에 의해 사용 중일 때 발생하며, DDL 작업 중 나타날 수 있습니다.
- 57014 (query_canceled):
statement_timeout또는lock_timeout초과로 쿼리가 취소될 때 발생하며, 55P03의 예방 수단으로 설정한 타임아웃에 의해 트리거될 수 있습니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.