Contents
see List잠금 문제는 쿼리 속도 문제와 다르다
PostgreSQL에서 화면 응답이 갑자기 느려졌는데 CPU와 디스크 사용률이 낮다면, 오래 실행되는 쿼리보다 잠금 대기를 먼저 의심할 수 있습니다. 여러 요청이 같은 행이나 테이블의 변경 권한을 기다리면 실제 작업 시간은 짧아도 사용자는 계속 대기합니다. 특히 주문 상태 변경, 재고 차감, 배치 정산, 관리자 일괄 수정처럼 같은 데이터를 동시에 갱신하는 업무에서 자주 발생합니다.
잠금은 데이터 정합성을 지키기 위한 정상 동작입니다. 해결의 목표는 잠금을 없애는 것이 아니라, 트랜잭션이 필요한 데이터만 짧게 점유하고 대기가 장애로 번지기 전에 제어하는 것입니다. 애플리케이션 코드, SQL 작성 방식, 운영 관측 지점을 함께 설계해야 재발을 줄일 수 있습니다.
먼저 대기 중인 세션과 원인을 확인하기
운영 중에는 현재 실행 중인 SQL만 보지 말고, 어떤 세션이 무엇을 기다리고 있는지 확인해야 합니다. 아래 조회는 활성 세션의 대기 이벤트와 실행 시간을 보여 줍니다. 실행 시간이 긴 세션, 대기 이벤트가 Lock인 세션, 같은 SQL이 반복되는 세션을 함께 확인하세요.
SELECT pid, usename, application_name, state, wait_event_type, wait_event, now() - query_start AS running_for, query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY query_start;대기 세션만 확인하면 차단한 주체를 놓치기 쉽습니다. pg_blocking_pids 함수로 차단 PID를 연결해 보면, 긴 트랜잭션 하나가 여러 요청을 막고 있는지 빠르게 파악할 수 있습니다. 이 결과를 장애 시간대의 애플리케이션 로그, 배치 실행 기록과 대조하면 원인 작업을 좁힐 수 있습니다.
SELECT a.pid AS waiting_pid, pg_blocking_pids(a.pid) AS blocking_pids, now() - a.query_start AS waiting_for, a.query AS waiting_query
FROM pg_stat_activity a
WHERE cardinality(pg_blocking_pids(a.pid)) > 0;트랜잭션 범위를 작게 유지하는 방법
가장 흔한 실수는 트랜잭션을 시작한 뒤 외부 API 호출, 파일 처리, 사용자 입력 대기, 복잡한 계산까지 같은 연결에서 처리하는 것입니다. 그 사이 변경된 행의 잠금은 계속 유지됩니다. 데이터베이스 트랜잭션 안에는 검증이 끝난 최소한의 읽기와 쓰기만 넣고, 외부 통신과 긴 계산은 트랜잭션 전후로 분리하세요.
예를 들어 재고를 차감한 뒤 결제 서비스에 요청해야 한다면, 결제 응답을 기다린 상태로 재고 행을 잠그지 않는 구조가 필요합니다. 결제 결과를 먼저 검증할 수 있는 흐름인지 검토하고, 반드시 원자성이 필요하면 짧은 상태 전이와 재시도 가능한 후속 작업으로 나누는 방식을 고려합니다. 하나의 거대한 트랜잭션으로 모든 시스템을 묶으면 잠금 시간과 장애 전파 범위가 함께 커집니다.
실패를 빨리 반환하도록 시간 제한 설정하기
lock_timeout은 잠금을 얻기까지 기다리는 최대 시간입니다. 무제한 대기는 웹 요청 스레드를 고갈시키고 커넥션 풀을 막을 수 있습니다. 사용자 요청에는 짧은 lock_timeout을 적용하고, 시간 초과가 발생하면 무조건 재시도하지 말고 충돌 안내 또는 제한된 횟수의 지연 재시도를 선택해야 합니다. statement_timeout은 SQL 전체 실행 시간을 제한하므로 lock_timeout과 목적이 다릅니다.
BEGIN;
SET LOCAL lock_timeout = '2s';
SET LOCAL statement_timeout = '10s';
UPDATE inventory
SET quantity = quantity - 1, updated_at = now()
WHERE product_id = 42 AND quantity > 0;
COMMIT;SET LOCAL은 현재 트랜잭션에만 적용되므로 커넥션 풀 환경에서 다른 요청에 설정이 남는 위험을 줄입니다. 갱신 결과 행 수가 0이면 재고 부족 또는 경쟁 조건을 애플리케이션이 명확히 처리해야 합니다. 시간 제한 값은 업무별로 달라야 합니다. 즉시 응답해야 하는 주문 API와 오래 실행될 수 있는 야간 집계에 같은 값을 적용하지 마세요.
일관된 잠금 순서와 격리 수준 점검
두 작업이 여러 행을 갱신할 때 잠그는 순서가 다르면 교착 상태가 생길 수 있습니다. 예를 들어 주문 A가 상품 1 다음 상품 2를 갱신하고, 주문 B가 상품 2 다음 상품 1을 갱신하면 서로 상대 잠금을 기다리게 됩니다. PostgreSQL은 교착 상태를 감지해 한 트랜잭션을 취소하지만, 서비스는 해당 오류를 처리할 준비가 되어 있어야 합니다.
- 여러 행을 갱신할 때는 ID 오름차순 등 공통된 순서를 정합니다.
- 트랜잭션 시작 전에 필요한 입력값과 권한을 검증합니다.
- 사용자 요청에서 대량 UPDATE나 DDL을 직접 수행하지 않습니다.
- 교착 상태와 lock timeout 오류는 로그에 SQL 식별자, 요청 종류, 재시도 여부를 남깁니다.
- 읽기와 쓰기 경합이 많은 테이블은 인덱스와 WHERE 조건을 점검해 불필요한 행 탐색을 줄입니다.
SELECT FOR UPDATE는 필요한 경우에만 사용해야 합니다. 재고처럼 읽은 값에 따라 뒤이어 반드시 갱신해야 하는 경우에는 유용하지만, 단순 조회까지 잠그면 동시 처리량이 떨어집니다. 또한 기본 격리 수준에서 해결 가능한 문제를 무조건 높은 격리 수준으로 바꾸면 직렬화 실패와 재시도 부담이 증가할 수 있습니다. 업무의 정합성 조건을 문서화한 뒤 필요한 범위에서만 적용하는 것이 안전합니다.
운영 체크리스트
- 대기 세션과 차단 세션을 함께 조회할 수 있는 점검 SQL을 준비합니다.
- 외부 호출과 긴 계산이 트랜잭션 내부에 들어가지 않았는지 검토합니다.
- 웹 요청에는 업무에 맞는 lock_timeout과 statement_timeout을 적용합니다.
- 여러 행을 갱신하는 코드의 잠금 순서를 통일합니다.
- 교착 상태와 시간 초과를 관측하고, 안전한 재시도 기준을 테스트합니다.
잠금 대기는 데이터베이스가 고장 났다는 신호라기보다 동시성 설계가 드러나는 지점입니다. 짧은 트랜잭션, 명확한 시간 제한, 재현 가능한 관측 쿼리를 갖추면 장애 대응과 일상적인 성능 개선을 함께 진행할 수 있습니다.
database
| No | 작성일 | Title |
|---|---|---|
| 2188 | 2026. 02. 11. | PostgreSQL 17 신기능과 성능 최적화 |
| 2187 | 2026. 02. 11. | Supabase Edge Functions 실전 활용 가이드 |
| 2121 | 2026. 02. 11. | Time-Series DB 비교: TimescaleDB vs InfluxDB vs QuestDB |
| 2120 | 2026. 02. 11. | Supabase Edge Functions와 실시간 구독 활용 |
| 2119 | 2026. 02. 11. | NewSQL 데이터베이스: CockroachDB vs TiDB 비교 |
| 2118 | 2026. 02. 11. | SQL 성능 최적화: 실행 계획 분석부터 인덱스 설계까지 |
| 2117 | 2026. 02. 11. | 데이터베이스 마이그레이션 전략과 도구 비교 |
| 2116 | 2026. 02. 11. | DuckDB: 분석용 인메모리 DB의 부상 |
| 2115 | 2026. 02. 11. | Redis 8.0과 실시간 데이터 처리 패턴 |
| 2114 | 2026. 02. 11. | PostgreSQL 17 신기능 완전 정리 |