
MVCC는 “무엇이 보이느냐(가시성)”를, 락은 “누가 먼저 쓰느냐(경합)”을 다룬다.
행 락과 대기 전략(NOWAIT / SKIP LOCKED)을 통해 “기다릴지 / 포기할지 / 건너뛸지”를 설계한다.
타임아웃·데드락 표준으로 “영원 대기”를 방지하며, 인덱스·FK가 락의 범위를 결정한다.

트랜잭션 락은 데이터의 일관성(Consistency)과 격리성(Isolation)을 보장하기 위한 경합 제어 도구다.
DB는 동시에 여러 사용자가 같은 데이터를 수정하려 할 때
“누가 먼저 접근할 것인가”를 결정하기 위해 락을 건다.
즉, 한 트랜잭션이 데이터를 수정 중이면
다른 트랜잭션은 그 데이터에 접근할 수 없도록 “잠금(Lock)”을 설정한다.
모든 RDBMS는 이 락을 통해 읽기·쓰기 충돌이 발생했을 때의 처리 방식을 정한다.
이때 시스템은 보통 아래 세 가지 중 하나로 대응한다.
핵심 문장:
락은 여러 트랜잭션이 동시에 실행되더라도
데이터를 순서 있게, 일관되게 유지하기 위한 장치다.
| 요소 | 설명 | 예시 |
|---|---|---|
| 가시성 (MVCC 이전 세대 의미) | COMMIT된 데이터만 보임 | 트랜잭션 격리 수준에 따라 Dirty Read 방지 |
| 경합 (Concurrency Control) | 쓰기 충돌 시 순서를 보장 | X락 선점 시 다른 트랜잭션 대기 |
| 대기 전략 | 충돌 시 시스템이 취할 태도 | WAIT, NOWAIT, SKIP |
| 타임아웃 / 데드락 | 무한 대기 방지 메커니즘 | Lock Timeout, Deadlock Detection |
| 락 범위 결정 | SQL 조건에 따라 락 범위 결정 | WHERE 조건 인덱스 유무가 결정적 |
| FK 연쇄 잠금 | 참조 무결성 검증 중 부모/자식 동시 락 | UPDATE parent → child FK check |
인덱스가 없는 WHERE 조건으로 UPDATE를 실행하면
DB는 어떤 행이 바뀔지 알 수 없으므로 테이블 전체를 스캔하며 락을 걸게 된다.
UPDATE row1 → X락 획득UPDATE row1 → 대기 상태(WAIT)핵심 문장:
일반적인 트랜잭션 락은 “대기”를 허용하는 구조다.
즉, 동시에 같은 데이터를 수정하려 하면
시스템이 락 큐(Lock Queue)를 만들어 “순서대로 처리”한다.
PostgreSQL은 같은 문제를 MVCC(Multi-Version Concurrency Control) 구조로 재해석했다.
즉, “락 중심 제어” 대신 “버전 기반 가시성”으로 읽기와 쓰기를 분리한다.
PostgreSQL은 각 행(row)에 xmin, xmax를 기록한다.

트랜잭션은 Snapshot을 통해 “보이는 버전만 읽기” 때문에
SELECT는 락을 걸지 않는다.
즉, 읽기-쓰기 간 충돌이 사라지고 “쓰기 간” 락만 남는다.
결과
- 읽기는 “버전 분리”
- 쓰기는 “락 경쟁”
PostgreSQL은 쓰기 충돌만 Row-Level Lock으로 제어한다.

| 전략 | 의미 | 명령어 |
|---|---|---|
| WAIT | 기본 모드 — 락 해제까지 대기 | FOR UPDATE |
| NOWAIT | 대기하지 않고 즉시 예외 발생 | FOR UPDATE NOWAIT |
| SKIP LOCKED | 잠긴 행을 건너뛰고 다음 행 조회 | FOR UPDATE SKIP LOCKED |
설명: 기본 모드. 잠긴 행이 풀릴 때까지 대기한다.
적용 시점: 결제, 재고, 송금 등 정합성이 중요한 업무
UPDATE accounts
SET balance = balance - 1000
WHERE id = 1
FOR UPDATE;
이미 다른 트랜잭션이 같은 계좌를 수정 중이라면 순서대로 대기 후 실행된다.
비즈니스 예시:
A 사용자가 결제 중일 때 B가 결제를 시도하면
B는 A의 트랜잭션이 끝날 때까지 대기한다.
→ 이중 결제나 중복 승인 방지에 유용하다.
설명: 잠긴 행이 있으면 즉시 실패 (ERROR: could not obtain lock)
적용 시점: 빠른 응답이 필요한 API, 포인트 적립, 예약 등
UPDATE points
SET amount = amount + 100
WHERE user_id = 42
FOR UPDATE NOWAIT;
락이 걸려 있으면 바로 예외 발생 → 재시도 큐에 등록
비즈니스 예시:
포인트 적립 중 다른 요청이 이미 처리 중이라면
“처리 중입니다” 메시지를 띄우고 종료.
→ 서버 부하를 줄이고 재시도 큐에서 나중에 처리한다.
설명: 잠긴 행을 건너뛰고 처리 가능한 행만 선택
적용 시점: 주문 큐, 배치, 멀티 워커 시스템
SELECT * FROM orders
WHERE status = 'READY'
FOR UPDATE SKIP LOCKED
LIMIT 10;
이미 다른 워커가 처리 중인 주문은 건너뛰고, 남은 주문만 선택한다.
비즈니스 예시:
여러 워커(worker)가 동시에 주문 큐를 가져갈 때,
이미 잠긴 주문은 건너뛰고 처리 가능한 주문부터 가져간다.
→ 병렬성을 높이고 락 경합을 최소화한다.
| 전략 | 대기 여부 | 사용 시점 | 장점 | 단점 | 대표 SQL |
|---|---|---|---|---|---|
| WAIT | 대기 | 결제, 송금 등 정합성 업무 | 순서 보장, 정확성 | 대기 발생 가능 | FOR UPDATE |
| NOWAIT | 즉시 실패 | API, 예약, 포인트 등 | 빠른 실패, 부하 감소 | 재시도 필요 | FOR UPDATE NOWAIT |
| SKIP LOCKED | 건너뜀 | 큐, 배치, 비동기 처리 | 병렬 효율, 무한 대기 없음 | 일부 지연 처리 | FOR UPDATE SKIP LOCKED |
| 항목 | PostgreSQL 구현 |
|---|---|
| Lock Timeout | SET lock_timeout = '3s'; |
| Deadlock Detection | Wait-for Graph 분석 (순환 대기 탐지) |
| 표준 대응 방식 | 실패 → 재시도 (Transaction Retry Loop) |
PostgreSQL은 “영원 대기”를 허용하지 않는다.
충돌 시 항상 실패 후 재시도가 정석 패턴이다.
| 조건 | 결과 |
|---|---|
| 인덱스 있음 | 특정 행만 Row Lock (최소 범위) |
| 인덱스 없음 | 테이블 전체 Scan → 광범위 락 발생 |
| FK 제약 | 부모-자식 간 Transaction Lock 연쇄 |
정리:
인덱스가 MVCC의 효율을 결정한다.
잘못된 인덱스 설계는 불필요한 블로킹을 유발한다.
| 구분 | 일반 트랜잭션 락 | PostgreSQL 트랜잭션 락 |
|---|---|---|
| 읽기-쓰기 충돌 | 락 기반 대기 | MVCC 기반 비차단(Read only) |
| 쓰기 충돌 | 락 큐 대기 | Row Lock (Tuple-level) |
| 대기 전략 | 내부 엔진 설정 | SQL 레벨 제어 (NOWAIT, SKIP) |
| 락 범위 | 조건 및 인덱스 의존 | 인덱스·FK 구조 직접 영향 |
| 데드락 처리 | 강제 롤백 | Wait-for Graph 탐지 + Fail & Retry |
| 철학 | “모두 기다려라” | “읽기는 분리, 쓰기만 질서” |
| 관점 | 일반 트랜잭션 락 | PostgreSQL 트랜잭션 락 |
|---|---|---|
| 핵심 질문 | “누가 먼저 접근하느냐” | “무엇이 보이느냐” |
| 중심 개념 | 대기 기반 순서 제어 | 버전 기반 가시성 분리 |
| 충돌 대응 | WAIT → DEADLOCK → ROLLBACK | NOWAIT / SKIP LOCKED + Retry |
| 락 범위 | SQL 범위 (테이블/페이지 단위) | 인덱스·FK 기반 행 단위 |
| 대표 전략 | Lock Queue 중심 | MVCC + Tuple Lock 하이브리드 |
정리 문장:
일반적인 트랜잭션 락은 ‘누가 먼저 쓰느냐’의 싸움이지만,
PostgreSQL의 트랜잭션 락은 ‘무엇이 보이느냐’를 분리한 뒤
‘누가 먼저 쓰느냐’를 최소 범위에서만 결정한다.
PostgreSQL은 단순히 트랜잭션 동작을 통제하는 수준을 넘어,
WAIT / NOWAIT / SKIP LOCKED 세 가지 대기 정책을
비즈니스 특성에 맞게 설계할 수 있도록 SQL 레벨에서 개방했다.
즉, “트랜잭션 제어”는 더 이상 엔진의 몫이 아니라
시스템 설계자의 선택 영역이다.
“지금까지는 데이터베이스가 동시성을 어떻게 제어하는지,
즉 MVCC와 트랜잭션 락이 ‘보이는 세계(가시성)’와 ‘쓰는 세계(경합)’를 분리하는 방식을 살펴봤습니다.
다음 편에서는 비관적 락 vs 낙관적 락 두 가지 접근 방식을 비교하며,
내가 직접 구현 중인 “좋아요 기능”에 어떤 방식이 더 적합한지
실제 프로젝트 수준에서 분석하고 적용해보겠습니다.