Contents
see List잠금 대기는 왜 운영 장애로 이어질까
PostgreSQL에서 여러 요청이 같은 데이터를 동시에 수정하면 데이터 정합성을 지키기 위해 잠금이 사용됩니다. 잠금 자체는 오류가 아니지만, 하나의 트랜잭션이 오래 열려 있으면 뒤따르는 UPDATE, DELETE, DDL 요청이 대기열에 쌓입니다. 웹 서비스에서는 API 응답 지연으로 시작해 커넥션 풀 고갈, 타임아웃, 재시도 폭증으로 번질 수 있습니다. 따라서 문제 발생 시 단순히 세션을 종료하기보다 누가 어떤 자원을 얼마나 오래 점유하고 있는지 먼저 파악해야 합니다.
특히 긴 트랜잭션은 사용자가 화면을 오래 보고 있는 동안 트랜잭션을 유지하거나, 외부 API 호출과 파일 처리까지 하나의 트랜잭션 안에 넣을 때 자주 발생합니다. 대량 갱신 배치가 인덱스 없이 넓은 범위를 스캔하는 경우도 잠금 유지 시간을 늘립니다. 목표는 잠금을 없애는 것이 아니라, 필요한 범위에서 짧은 시간만 보유하도록 설계하는 것입니다.
현재 대기와 차단 세션 확인하기
우선 pg_stat_activity와 pg_locks를 이용해 대기 중인 세션과 차단 세션을 연결해 봅니다. 아래 쿼리는 잠금을 기다리는 요청, 이를 막고 있는 백엔드 PID, 실행 중인 쿼리를 함께 보여 줍니다. 운영 DB에서는 결과에 개인정보나 민감한 SQL 파라미터가 포함될 수 있으므로 접근 권한과 로그 보관 정책도 함께 확인합니다.
SELECT waiting.pid AS waiting_pid, waiting.usename AS waiting_user, waiting.wait_event_type, waiting.wait_event, now() - waiting.query_start AS waiting_for, waiting.query AS waiting_query, blocker.pid AS blocking_pid, blocker.usename AS blocking_user, now() - blocker.xact_start AS blocking_transaction_age, blocker.query AS blocking_query FROM pg_stat_activity AS waiting JOIN LATERAL unnest(pg_blocking_pids(waiting.pid)) AS b(blocking_pid) ON true JOIN pg_stat_activity AS blocker ON blocker.pid = b.blocking_pid WHERE waiting.wait_event_type = 'Lock' ORDER BY waiting.query_start;blocking_transaction_age가 길고 blocker.query가 COMMIT되지 않은 작업이라면 우선 애플리케이션의 해당 요청 흐름을 조사합니다. 단순 조회처럼 보여도 트랜잭션 격리 수준, SELECT FOR UPDATE, 미완료 상태의 갱신이 원인일 수 있습니다. xact_start가 NULL이 아닌 유휴 세션은 트랜잭션을 시작한 뒤 아무 쿼리도 수행하지 않는 상태일 수 있으므로 별도로 점검합니다.
원인별로 설계를 고치는 방법
첫째, 트랜잭션 안에는 DB 일관성에 필요한 읽기와 쓰기만 둡니다. 결제사 호출, 이메일 전송, 대용량 파일 업로드, 복잡한 보고서 생성은 트랜잭션 밖으로 옮기거나 커밋 뒤 비동기 작업으로 처리합니다. 둘째, 갱신 조건에 맞는 인덱스를 마련해 불필요한 행을 오래 탐색하지 않게 합니다. 인덱스가 있어도 UPDATE 대상 행 수가 너무 많다면 작은 배치로 나누고 각 배치마다 커밋합니다.
셋째, 여러 테이블이나 행을 갱신하는 업무는 모든 코드 경로에서 같은 순서로 잠금을 획득해야 합니다. 예를 들어 주문과 재고를 함께 수정할 때 어떤 요청은 주문 후 재고를, 다른 요청은 재고 후 주문을 잡으면 교착 상태 가능성이 높아집니다. 정렬된 ID 순서로 처리하고, 재시도는 PostgreSQL의 deadlock detected 또는 serialization failure처럼 재시도가 안전한 오류에만 제한적으로 적용합니다.
타임아웃으로 무한 대기 막기
lock_timeout은 잠금을 얻기 위해 기다리는 최대 시간이고 statement_timeout은 쿼리 전체 실행 시간의 상한입니다. 둘을 함께 설정하면 장애 확산을 줄일 수 있습니다. 다만 너무 짧은 전역 값은 정상적인 배치까지 실패시킬 수 있으므로, API 요청·관리자 작업·배치 작업별 연결 또는 트랜잭션 단위로 다르게 적용하는 편이 안전합니다.
BEGIN; SET LOCAL lock_timeout = '3s'; SET LOCAL statement_timeout = '15s'; UPDATE inventory SET available_quantity = available_quantity - 1, updated_at = now() WHERE product_id = 42 AND available_quantity > 0; COMMIT;위 예시는 잠금 대기가 길어질 경우 빠르게 실패시켜 애플리케이션이 사용자에게 재시도 안내 또는 대체 처리를 제공할 수 있게 합니다. 재고 차감처럼 조건부 갱신이 필요한 업무는 읽은 뒤 별도 UPDATE를 보내기보다, 조건을 UPDATE의 WHERE 절에 포함해 경쟁 구간을 줄이는 방식이 유리합니다. 결과 행 수가 0이면 재고 부족 또는 경쟁 요청 선점으로 해석하도록 애플리케이션 계약을 정합니다.
대량 작업과 큐 처리의 주의점
대량 데이터 정리나 상태 전환은 한 번에 수십만 행을 갱신하지 말고 범위를 나눕니다. 각 배치는 짧게 커밋하고, 실행 시간과 영향 행 수를 기록합니다. 여러 워커가 처리할 작업 큐에는 FOR UPDATE SKIP LOCKED를 사용할 수 있습니다. 이미 다른 워커가 잡은 행을 기다리지 않고 다음 작업을 가져오므로 병렬 처리량을 높일 수 있습니다. 그러나 실패한 작업의 재시도 횟수, 처리 중 상태의 만료 기준, 중복 실행에 안전한 작업 설계를 함께 갖춰야 합니다.
WITH next_jobs AS (SELECT id FROM job_queue WHERE status = 'ready' ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 20) UPDATE job_queue q SET status = 'running', started_at = now() FROM next_jobs WHERE q.id = next_jobs.id RETURNING q.id, q.payload;운영 체크리스트
- Lock 대기 세션과 차단 PID를 주기적으로 확인하고, 긴 트랜잭션의 발생 경로를 기록한다.
- 외부 호출과 느린 처리를 트랜잭션 밖으로 분리하고, 갱신 대상 조건에 적절한 인덱스를 둔다.
- 테이블과 행의 잠금 획득 순서를 일관되게 유지하며, 교착 상태 재시도는 멱등성이 보장된 작업에만 적용한다.
- 업무 성격에 맞춘 lock_timeout과 statement_timeout을 설정하고, 타임아웃 오류를 관측한다.
- 대량 갱신은 작은 배치로 나누고, 큐 워커는 중복 처리와 실패 복구까지 설계한다.
잠금 문제는 특정 쿼리 하나를 종료하는 것으로 끝나지 않습니다. 대기 시간, 트랜잭션 길이, 영향 행 수, 재시도 횟수를 함께 관찰하면 데이터 정합성을 유지하면서도 응답 지연을 줄이는 운영 기준을 만들 수 있습니다.
database
| No | 작성일 | Title |
|---|---|---|
| 2191 | 2026. 02. 11. | 무중단 데이터베이스 마이그레이션 전략 |
| 2190 | 2026. 02. 11. | Redis 8.0 새로운 데이터 구조와 활용법 |
| 2189 | 2026. 02. 11. | Vector Database 비교와 활용 전략 |
| 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. | 데이터베이스 마이그레이션 전략과 도구 비교 |