락과 동시성 — SELECT FOR UPDATE
행 락·테이블 락·데드락 진단까지. 동시 요청이 만든 충돌 풀기.
트랜잭션 두 개가 같은 데이터를 동시에 만지면? PostgreSQL 은 락으로 정리합니다. 행 단위·테이블 단위·advisory 락까지 종류가 다양 — 15편은 매일 만나는 락 패턴과 데드락 진단, 그리고 pg_locks/pg_stat_activity 로 락 상태 보는 법까지.
락의 두 축 — 무엇을 / 얼마나 강하게
| 대상 | 락 종류 | 발생 |
|---|---|---|
| 테이블 락 | ACCESS SHARE ~ ACCESS EXCLUSIVE (8단계) | SELECT·DDL·VACUUM 등 |
| 행 락 | FOR KEY SHARE / SHARE / NO KEY UPDATE / UPDATE | UPDATE·DELETE·SELECT FOR UPDATE |
| advisory 락 | 애플리케이션이 명시적 | pg_advisory_lock(123) |
SELECT FOR UPDATE — 가장 자주 쓰는 행 락
BEGIN;
SELECT stock FROM inventory WHERE id = 5 FOR UPDATE;
-- 다른 TX 가 이 행을 UPDATE·DELETE·FOR UPDATE 시도하면 대기
IF stock > 0 THEN
UPDATE inventory SET stock = stock - 1 WHERE id = 5;
END IF;
COMMIT;
왜 필요한가. "재고 확인 → 차감" 사이에 다른 트랜잭션이 같은 일 하면 재고가 음수로 갑니다 (race). FOR UPDATE 가 그 사이를 차단.
FOR SHARE · FOR KEY SHARE — 가벼운 락
-- FOR SHARE: 읽기만 하지만 "이 행 바꾸지 마"
SELECT * FROM users WHERE id = 5 FOR SHARE;
-- 다른 TX 는 SELECT FOR SHARE 는 OK, UPDATE 는 대기
-- FOR KEY SHARE (가장 약함): FK 참조 시 자동 사용
-- 다른 TX 의 UPDATE 는 허용, DELETE 만 차단
NOWAIT · SKIP LOCKED — 락 대기 회피
-- 이미 락 걸려있으면 즉시 에러
SELECT * FROM jobs WHERE id = 1 FOR UPDATE NOWAIT;
-- ERROR: could not obtain lock on row
-- 잠긴 행은 건너뛰기 — 작업 큐 패턴!
BEGIN;
SELECT * FROM jobs
WHERE status = 'pending'
ORDER BY created_at
LIMIT 1
FOR UPDATE SKIP LOCKED;
-- 다른 워커가 잠근 행은 보이지 않음 → 자연스러운 분산 큐
-- 작업 처리...
UPDATE jobs SET status = 'done' WHERE id = $1;
COMMIT;
SKIP LOCKED 의 가치. 멀티 워커 작업 큐를 Redis 없이 PostgreSQL 만으로 구현 가능. 워커 N 개가 같은 테이블 폴링해도 서로 다른 행을 처리. pg_queue·river 같은 라이브러리들의 핵심.
테이블 락 — DDL 의 함정
-- 컬럼 추가는 ACCESS EXCLUSIVE 락 (모든 SELECT 도 막힘)
ALTER TABLE big_table ADD COLUMN new_col INT;
-- 큰 테이블이면 수 분 동안 서비스 정지
-- 안전한 패턴
ALTER TABLE big_table ADD COLUMN new_col INT NULL; -- NULL 허용 → 즉시
-- 백필은 별도 배치로
-- NOT NULL 은 나중에 (PG 12+ 는 DEFAULT 와 함께면 빠름)
-- LOCK 직접
BEGIN;
LOCK TABLE big_table IN ACCESS EXCLUSIVE MODE;
-- ...
COMMIT;
데드락 — A 가 B 를 기다리고 B 가 A 를 기다림
-- TX1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
-- (잠시 후 B 도 UPDATE 하려는데)
UPDATE accounts SET balance = balance + 100 WHERE id = 'B'; -- TX2 가 B 락 보유 → 대기
-- TX2 (동시)
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 'B';
UPDATE accounts SET balance = balance + 100 WHERE id = 'A'; -- TX1 이 A 락 보유 → 대기
-- 1초 뒤 PostgreSQL 이 감지하고 한 쪽을 abort:
-- ERROR: deadlock detected
데드락 회피 — 일관된 순서. 여러 행에 락 걸 때 항상 같은 순서로(예: id 오름차순). 그러면 데드락이 생길 수가 없습니다. SELECT ... FOR UPDATE ORDER BY id 패턴.
락 상태 보기 — pg_locks · pg_stat_activity
-- 현재 활성 락
SELECT pid, relation::regclass, mode, granted
FROM pg_locks
WHERE NOT granted; -- 대기 중인 것만
-- 누가 무엇을 하는 중인지
SELECT pid, usename, state, query_start, query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY query_start;
-- 락 대기 체인 (블로커 추적)
SELECT
blocked.pid AS blocked_pid,
blocked_act.query AS blocked_query,
blocking.pid AS blocking_pid,
blocking_act.query AS blocking_query
FROM pg_locks blocked
JOIN pg_stat_activity blocked_act ON blocked_act.pid = blocked.pid
JOIN pg_locks blocking ON blocking.locktype = blocked.locktype
AND blocking.granted = true
AND blocking.pid != blocked.pid
JOIN pg_stat_activity blocking_act ON blocking_act.pid = blocking.pid
WHERE NOT blocked.granted;
-- 강제 종료 (긴급)
SELECT pg_cancel_backend(pid); -- 부드럽게
SELECT pg_terminate_backend(pid);-- 강제 (마지막 수단)
advisory 락 — 애플리케이션이 만드는 락
-- 행이 아니라 임의의 숫자 키에 락
SELECT pg_advisory_lock(123); -- 세션 종료까지
-- ... 임계 구역 ...
SELECT pg_advisory_unlock(123);
-- 트랜잭션 단위
SELECT pg_advisory_xact_lock(123);
-- 트랜잭션 끝나면 자동 해제
-- 시도 (블록 안 함)
SELECT pg_try_advisory_lock(123); -- true 면 획득, false 면 못 함
-- 사용 예 — "이 사용자의 결제는 한 번에 하나만"
SELECT pg_advisory_xact_lock(hashtext('payment-user-' || user_id));
advisory 락의 가치. 행이 아직 없는 작업·여러 테이블에 걸친 작업·"이 이름의 cron 은 한 번에 하나만" 같은 패턴. Redis 락 대체로 쓰는 경우도 많습니다.
락 통계 — 락 통한 병목 찾기
-- 락 대기 시간 (확장 필요)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT query, calls, total_exec_time,
(blk_read_time + blk_write_time) AS io_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC LIMIT 10;
-- 락 대기 통계 (PG 14+)
SELECT wait_event_type, wait_event, count(*)
FROM pg_stat_activity
WHERE wait_event_type = 'Lock'
GROUP BY wait_event_type, wait_event;
한 줄 가이드
- "읽고 → 검사 → 쓰기" 패턴 → SELECT FOR UPDATE.
- 작업 큐 → SELECT FOR UPDATE SKIP LOCKED.
- 여러 행 락 → 일관된 순서로 ORDER BY (데드락 회피).
- DDL은 짧고 작게 — 큰 테이블에 ALTER 직격 금지.
- 락 상황 모니터링 → pg_stat_activity + pg_locks 쿼리 즐겨찾기.
- 임의 키 락 → pg_advisory_xact_lock.
16편 — 인덱스 (B-Tree·GIN·BRIN)
인덱스 종류 5가지와 multi-column·partial·covering 의 선택.
📚 PostgreSQL 배우기 교재
이전: 14편 트랜잭션 · 현재: 15편 (중급) · 다음 → 16편 인덱스 · 진행: 15/24
이전: 14편 트랜잭션 · 현재: 15편 (중급) · 다음 → 16편 인덱스 · 진행: 15/24