데이터베이스 락과 교착 상태: 잠금 순서·2단계 로킹·직렬 가능성


1장. 두 요청이 모두 멈췄다#

재고 테이블에 두 상품이 있다고 하겠습니다.

상품 1
재고 100

상품 2
재고 100

두 개의 트랜잭션이 거의 동시에 실행됩니다.

트랜잭션 A는 상품 1을 먼저 수정하고 상품 2를 수정하려고 합니다.

트랜잭션 B는 반대입니다.

트랜잭션 A
상품 1 → 상품 2

트랜잭션 B
상품 2 → 상품 1

A가 먼저 상품 1을 수정합니다.

B가 먼저 상품 2를 수정합니다.

그다음 A는 상품 2를 수정하려고 합니다.

하지만 상품 2는 B가 사용 중입니다.

A는 기다립니다.

B도 상품 1을 수정하려고 합니다.

상품 1은 A가 사용 중입니다.

B도 기다립니다.

결과는 다음과 같습니다.

A
→ B가 가진 자원을 기다림

B
→ A가 가진 자원을 기다림

둘 다 상대방이 먼저 끝나야 진행할 수 있습니다.

하지만 어느 쪽도 끝날 수 없습니다.

이것이 교착 상태, 즉 데드락입니다.


2장. 잠금은 동시 변경을 안전하게 조정하는 장치다#

여러 트랜잭션이 동시에 같은 데이터를 읽고 쓸 때 아무런 조정이 없다면 서로의 변경을 덮어쓰거나 잘못된 중간 상태를 읽을 수 있습니다.

잠금은 이런 충돌을 제어하는 대표적인 방법입니다.

개념적으로는 다음과 같습니다.

트랜잭션 A
데이터 X 사용
↓
X에 잠금

트랜잭션 B
X 사용 요청
↓
잠금과 충돌하면 대기

잠금이 있다고 해서 무조건 문제가 생기는 것은 아닙니다.

정상적인 대기는 매우 흔합니다.

문제는 대기가 순환할 때입니다.


3장. 읽기와 쓰기는 모두 같은 충돌을 만드는 것이 아니다#

트랜잭션 T1과 T2가 데이터 X를 사용한다고 하겠습니다.

R₁(X)는 T1이 X를 읽는다는 뜻입니다.

W₁(X)는 T1이 X를 쓴다는 뜻입니다.

대표적인 충돌 관계는 다음과 같습니다.

T1 T2 충돌
R₁(X) R₂(X) 아니오
R₁(X) W₂(X) 예
W₁(X) R₂(X) 예
W₁(X) W₂(X) 예
W₁(X) R₂(Y) 아니오

핵심 규칙은 간단합니다.

서로 다른 트랜잭션이 같은 데이터 항목을 사용하고, 둘 중 하나 이상이 쓰기라면 순서가 중요하다.


4장. 두 번의 읽기는 일반적인 충돌이 아니다#

T1과 T2가 동시에 X를 읽습니다.

T1
R(X)

T2
R(X)

두 트랜잭션 모두 데이터를 바꾸지 않습니다.

따라서 전통적인 공유락 모델에서는 동시에 읽는 것이 가능합니다.

하지만 T1이 쓰려고 한다면 상황이 달라집니다.

T1
W(X)

T2
R(X)

T2가 어떤 값을 읽어야 하는지 결정해야 합니다.

이때 잠금이나 MVCC 같은 동시성 제어가 개입합니다.


5장. 공유락과 배타락의 기본 개념#

전통적인 잠금 설명에서는 대표적으로 두 종류를 사용합니다.

공유락#

Shared Lock, S Lock입니다.

주로 읽기를 위한 잠금으로 설명합니다.

여러 트랜잭션이 동시에 공유락을 가질 수 있습니다.

T1 → S(X)

T2 → S(X)

허용

배타락#

Exclusive Lock, X Lock입니다.

쓰기를 위해 사용하는 잠금으로 설명합니다.

배타락은 다른 공유락이나 배타락과 충돌합니다.


6장. 기본적인 S·X 락 호환성을 보면#

현재 잠금 새 S 요청 새 X 요청
S 허용 대기
X 대기 대기

쉽게 정리하면:

읽기 + 읽기
→ 함께 가능

읽기 + 쓰기
→ 충돌

쓰기 + 쓰기
→ 충돌

입니다.

하지만 이 표는 기본 개념 모델입니다.

실제 DBMS는 더 다양한 잠금 모드와 MVCC 구조를 사용합니다.


7장. PostgreSQL의 일반 SELECT를 단순한 S락으로 이해하면 안 된다#

전통적인 잠금 이론을 공부한 뒤 다음처럼 생각하기 쉽습니다.

SELECT가 실행되면 행에 공유락을 걸어서 UPDATE를 막는다.

PostgreSQL의 일반 SELECT를 이렇게 단순하게 이해하면 안 됩니다.

PostgreSQL은 MVCC를 사용하므로 일반 SELECT는 다른 트랜잭션의 미확정 행 변경을 단순히 S락으로 막기보다 자신에게 보이는 행 버전을 읽을 수 있습니다.

반면 다음과 같은 SQL은 다릅니다.

SELECT *
FROM account
WHERE account_id = 1
FOR UPDATE;

이 경우 변경 대상 행에 명시적인 잠금 의도가 있습니다.


8장. SELECT FOR UPDATE는 “읽은 뒤 바꿀 행”을 보호할 때 사용한다#

재고를 읽고 복잡한 업무 계산을 한 뒤 변경해야 한다고 하겠습니다.

BEGIN;

SELECT stock
FROM product
WHERE product_id = 1
FOR UPDATE;

이제 해당 행에 대해 경쟁하는 다른 변경 작업은 잠금 해제를 기다릴 수 있습니다.

업무 조건을 확인한 뒤 수정합니다.

UPDATE product
SET stock = stock - 1
WHERE product_id = 1;

COMMIT;

다만 잠금을 사용한다고 모든 동시성 문제가 자동으로 해결되는 것은 아닙니다.

무엇을 잠그는지가 중요합니다.


9장. 잠금 대기와 교착 상태는 다르다#

트랜잭션 A가 상품 1을 수정 중입니다.

트랜잭션 B가 상품 1을 수정하려고 합니다.

A
상품 1 잠금

B
상품 1 요청
↓
대기

이것만으로는 교착 상태가 아닙니다.

A가 COMMIT하면 잠금이 해제됩니다.

B는 이어서 실행할 수 있습니다.

즉:

A
↓
끝남
↓
B 진행

이 가능합니다.


10장. 교착 상태에는 순환이 필요하다#

다음 상황을 보겠습니다.

A
상품 1 잠금
↓
상품 2 기다림

B
상품 2 잠금
↓
상품 1 기다림

대기 관계를 그리면:

flowchart LR
    A["트랜잭션 A"] -->|"상품 2 기다림"| B["트랜잭션 B"]
    B -->|"상품 1 기다림"| A

순환이 만들어졌습니다.

A → B → A

이 순환이 교착 상태의 핵심입니다.


11장. PostgreSQL에서 실제 교착 상태를 만들어 보자#

실습 테이블을 만듭니다.

CREATE TABLE lock_demo (
    id integer PRIMARY KEY,
    value integer NOT NULL
);

INSERT INTO lock_demo
VALUES
    (1, 0),
    (2, 0);

두 개의 독립된 데이터베이스 연결을 준비합니다.

세션 A

세션 B

12장. 먼저 세션 A가 행 1을 수정한다#

세션 A:

BEGIN;

UPDATE lock_demo
SET value = value + 1
WHERE id = 1;

아직 COMMIT하지 않습니다.

세션 A는 행 1의 변경과 관련된 잠금을 유지합니다.


13장. 세션 B는 행 2를 수정한다#

세션 B:

BEGIN;

UPDATE lock_demo
SET value = value + 1
WHERE id = 2;

역시 COMMIT하지 않습니다.

현재 상태를 개념적으로 보면:

세션 A
→ 행 1 사용

세션 B
→ 행 2 사용

아직 충돌은 없습니다.


14장. 세션 A가 행 2를 수정하려고 하면 기다린다#

세션 A:

UPDATE lock_demo
SET value = value + 1
WHERE id = 2;

행 2는 세션 B가 변경 중입니다.

따라서 세션 A는 대기하게 됩니다.

A
→ B를 기다림

아직 이 시점까지는 교착 상태라고 단정할 수 없습니다.

B가 정상적으로 COMMIT하면 A가 진행할 수 있기 때문입니다.


15장. 세션 B까지 행 1을 요청하면 순환이 완성된다#

세션 B:

UPDATE lock_demo
SET value = value + 1
WHERE id = 1;

행 1은 세션 A가 사용 중입니다.

이제 B도 기다립니다.

A → B

B → A

대기 순환이 완성되었습니다.

PostgreSQL은 이 교착 상태를 감지한 뒤 트랜잭션 하나를 오류로 중단할 수 있습니다.


16장. 어떤 트랜잭션이 희생될지는 고정해서 기대하면 안 된다#

교착 상태가 발생하면 DBMS는 순환을 끊기 위해 한 트랜잭션을 중단할 수 있습니다.

예를 들어 A가 취소될 수도 있고 B가 취소될 수도 있습니다.

따라서 애플리케이션을 다음처럼 작성하면 위험합니다.

항상 두 번째 요청이 실패할 것이다.

교착 상태의 희생 거래를 특정 요청으로 고정해서 가정하지 않는 것이 좋습니다.


17장. 교착으로 실패한 트랜잭션은 ROLLBACK 상태를 처리해야 한다#

PostgreSQL에서 트랜잭션이 오류 상태가 되면 해당 세션에서:

ROLLBACK;

하여 트랜잭션을 종료해야 할 수 있습니다.

그다음 필요한 업무를 다시 시작합니다.

ROLLBACK
↓
새 BEGIN
↓
업무 재검사
↓
재실행

마지막 SQL만 이어서 실행하는 방식은 안전하지 않을 수 있습니다.


18장. 왜 마지막 UPDATE만 재실행하면 안 될까#

A의 업무가 다음과 같았다고 하겠습니다.

1. 상품 1 재고 읽기
2. 상품 1 변경
3. 상품 2 재고 읽기
4. 상품 2 변경

3단계나 4단계에서 교착 상태로 트랜잭션 전체가 취소됐습니다.

그렇다면 4단계 UPDATE만 다시 실행해서는 안 됩니다.

1단계에서 읽었던 값은 이미 다른 트랜잭션 때문에 바뀌었을 수 있습니다.

따라서:

트랜잭션 전체의 업무 판단부터 다시 수행해야 한다.

는 원칙이 중요합니다.


19장. 동일한 잠금 순서를 사용하면 대표적인 교착을 줄일 수 있다#

트랜잭션 A:

상품 1
↓
상품 2

트랜잭션 B:

상품 2
↓
상품 1

순서가 반대라서 교착 가능성이 생겼습니다.

모든 코드에서 다음 규칙을 사용한다고 하겠습니다.

여러 상품을 잠글 때 상품ID가 작은 순서부터 잠근다.

그러면 둘 다:

상품 1
↓
상품 2

순서로 접근합니다.


20장. 잠금 순서를 통일하면 어떻게 달라질까#

A가 먼저 상품 1을 잡았습니다.

B도 상품 1을 원합니다.

B는 기다립니다.

A는 상품 2까지 처리하고 COMMIT합니다.

A
상품 1
↓
상품 2
↓
COMMIT

그다음 B가 상품 1과 상품 2를 처리합니다.

순환은 없습니다.

flowchart LR
    A["A가 1 → 2 처리"] --> C["A COMMIT"]
    C --> B["B가 1 → 2 처리"]

대기는 발생할 수 있지만 교착은 피할 수 있습니다.


21장. 잠금 순서 통일이 모든 교착을 없애는 것은 아니다#

상품 행 두 개만 보면 순서 통일이 효과적입니다.

하지만 실제 트랜잭션에는 다음이 함께 포함될 수 있습니다.

주문

상품 재고

쿠폰

포인트

결제 상태

외래키 검사

한 코드 경로에서는:

주문 → 재고 → 포인트

순서인데 다른 배치 프로그램에서는:

포인트 → 주문 → 재고

순서라면 다른 형태의 교착이 발생할 수 있습니다.

잠금 순서는 업무 전체 접근 경로에서 통일해야 효과가 큽니다.


22장. 짧은 트랜잭션은 잠금 대기 가능성도 줄인다#

다음 트랜잭션을 생각해 보겠습니다.

BEGIN

재고 행 잠금

외부 API 호출 5초

사용자 로그 출력

추가 계산

UPDATE

COMMIT

행을 잠근 상태에서 외부 API를 기다립니다.

그동안 다른 요청은 같은 행을 사용하지 못하고 기다릴 수 있습니다.

가능하다면 잠금을 보유하는 구간을 필요한 만큼 짧게 만드는 것이 좋습니다.


23장. 사용자 입력을 기다리는 동안 잠금을 잡고 있으면 위험하다#

예를 들어:

BEGIN

행 잠금

사용자 확인 버튼 대기

30초 후 UPDATE

COMMIT

같은 구조는 잠금을 오래 유지할 수 있습니다.

일반적으로 사용자 입력 시간과 DB 트랜잭션 시간은 분리하는 것이 좋습니다.

사용자 입력 완료
↓
짧은 DB 트랜잭션 시작
↓
현재 상태 재확인
↓
변경
↓
COMMIT

형태가 더 안전할 수 있습니다.


24장. 2단계 로킹은 잠금을 얻는 시기와 푸는 시기를 나눈다#

2PL, Two-Phase Locking은 잠금 프로토콜의 대표적인 이론 모델입니다.

두 단계가 있습니다.

확장 단계#

잠금을 획득합니다.

LOCK A

LOCK B

LOCK C

아직 새로운 잠금을 얻을 수 있습니다.

축소 단계#

잠금을 해제하기 시작합니다.

UNLOCK A

이 시점 이후에는 새로운 잠금을 얻지 않습니다.


25장. 2PL의 기본 구조를 그림으로 보면#

flowchart LR
    G1["LOCK A"] --> G2["LOCK B"]
    G2 --> G3["LOCK C"]
    G3 --> U1["UNLOCK A"]
    U1 --> U2["UNLOCK B"]
    U2 --> U3["UNLOCK C"]

왼쪽은 잠금 획득 구간입니다.

오른쪽은 잠금 해제 구간입니다.

한 번 해제를 시작하면 다시 새 잠금을 얻지 않는 것이 핵심입니다.


26장. 2PL은 충돌 직렬 가능성을 보장하기 위한 대표적 방법이다#

여러 트랜잭션이 2PL 규칙을 따른다면 충돌 직렬 가능 스케줄을 만들 수 있습니다.

하지만 여기서 주의해야 할 점이 있습니다.

기본 2PL이 곧 모든 회복 문제까지 해결하는 것은 아니다.

기본 2PL에서는 특정 잠금을 COMMIT 전에 해제할 수 있습니다.

그 결과 다른 트랜잭션이 미확정 변경에 의존하는 문제가 생길 수 있습니다.


27장. Strict 2PL은 배타락을 트랜잭션 끝까지 유지한다#

엄격한 2단계 로킹에서는 일반적으로 X락을 COMMIT이나 ROLLBACK까지 유지합니다.

개념적으로:

LOCK-X A

WRITE A

...

COMMIT

UNLOCK A

처럼 볼 수 있습니다.

이렇게 하면 다른 트랜잭션이 미확정 쓰기에 접근하는 문제를 줄일 수 있고 연쇄 철회를 막는 데 유리합니다.


28장. Strong Strict 2PL은 S와 X 잠금을 모두 끝까지 유지한다#

강한 2단계 로킹 모델에서는 공유락과 배타락을 모두 트랜잭션 종료까지 유지합니다.

정리하면 다음과 같습니다.

방식 끝까지 유지하는 잠금 주요 의미
기본 2PL 반드시 모두는 아님 충돌 직렬 가능성
Strict 2PL X락 미확정 쓰기 노출 방지에 유리
Strong Strict 2PL S락과 X락 잠금 해제가 트랜잭션 끝에 집중

제품 구현은 더 복잡할 수 있으므로 이 표는 이론적 구분으로 이해하는 것이 좋습니다.


29장. 직렬 가능성이란 동시에 실행했지만 순차 실행과 같다는 뜻이다#

두 트랜잭션이 있다고 하겠습니다.

T1

T2

완전히 직렬로 실행하면 두 가지 순서만 있습니다.

T1 → T2

또는:

T2 → T1

동시 실행된 스케줄이 이 둘 중 하나와 동일한 효과를 갖는다면 직렬 가능하다고 볼 수 있습니다.


30장. 충돌 직렬 가능성은 선행 그래프로 판단할 수 있다#

다음 스케줄을 보겠습니다.

R₁(A)
W₁(A)
R₂(A)
W₂(B)
C₁
C₂

A에서:

W₁(A)
↓
R₂(A)

라는 충돌이 있습니다.

T1의 쓰기가 먼저이므로 간선을 만듭니다.

T1 → T2

B는 T2만 사용하므로 추가 간선이 없습니다.


31장. 그래프에 순환이 없으면 충돌 직렬 가능하다#

앞의 선행 그래프는 다음과 같습니다.

flowchart LR
    T1["T1"] --> T2["T2"]

순환이 없습니다.

따라서:

T1
↓
T2

라는 직렬 순서와 충돌 동등하게 설명할 수 있습니다.


32장. 순환이 있으면 충돌 직렬 가능하지 않다#

이번에는 다음 실행 순서를 보겠습니다.

W₁(A)
R₂(A)
W₂(B)
R₁(B)
C₁
C₂

A에서는:

W₁(A)
↓
R₂(A)

이므로:

T1 → T2

간선이 생깁니다.

B에서는:

W₂(B)
↓
R₁(B)

이므로:

T2 → T1

도 생깁니다.

그래프는:

flowchart LR
    T1["T1"] --> T2["T2"]
    T2 --> T1

입니다.

순환이 있으므로 충돌 직렬 가능하지 않습니다.


33장. 선행 그래프와 교착 대기 그래프를 혼동하면 안 된다#

둘 다 화살표를 사용하기 때문에 헷갈리기 쉽습니다.

충돌 선행 그래프#

목적:

이 실행 결과를
어떤 직렬 순서와 동등하게 설명할 수 있는가?

간선:

충돌 연산의 실행 순서

대기 그래프#

목적:

현재 누가 누구를 기다리고 있는가?

간선:

잠금 대기 관계

둘은 서로 다른 그래프입니다.


34장. 직렬 가능하다고 회복 가능성까지 보장되는 것은 아니다#

다음 스케줄을 보겠습니다.

W₁(X)

R₂(X)

C₂

A₁

T2는 T1이 쓴 값을 읽었습니다.

그런데 T2가 먼저 COMMIT했습니다.

이후 T1이 ABORT합니다.

문제는 T2가 결국 취소된 데이터를 읽고 이미 확정됐다는 것입니다.

따라서 회복할 수 없는 문제가 생길 수 있습니다.


35장. 회복 가능 스케줄은 커밋 순서까지 본다#

T2가 T1이 쓴 값을 읽었다면:

T1 COMMIT
↓
T2 COMMIT

순서가 필요합니다.

예를 들어:

W₁(X)
R₂(X)
C₁
C₂

라면 커밋 순서 측면에서는 회복 가능합니다.

하지만 T2가 T1의 미확정 값을 이미 읽었다는 사실은 남습니다.

T1이 중간에 실패했다면 T2도 영향을 받을 수 있습니다.


36장. 연쇄 철회는 하나의 실패가 다른 트랜잭션까지 번지는 현상이다#

T1이 미확정 값을 썼습니다.

W₁(X)

T2가 그 값을 읽습니다.

R₂(X)

그런데 T1이 ROLLBACK합니다.

T2는 잘못된 값을 이용했습니다.

따라서 T2도 취소해야 할 수 있습니다.

T1 실패
↓
T2도 철회

이것이 연쇄 철회입니다.

엄격한 잠금 방식은 이런 문제를 방지하는 데 도움이 됩니다.


37장. 직렬 가능성과 회복 가능성은 서로 다른 질문이다#

정리하면:

직렬 가능성
→ 동시 실행의 논리적 순서가 안전한가?

회복 가능성
→ 미확정 데이터에 의존한 거래를 안전하게 복구할 수 있는가?

같은 개념이 아닙니다.

따라서:

직렬 가능하니까 완전히 안전하다.

라고 단정해서는 안 됩니다.


38장. 교착 상태의 고전적인 네 조건#

교착 상태를 설명할 때 흔히 네 조건을 사용합니다.

상호 배제#

한 자원을 동시에 여러 트랜잭션이 자유롭게 사용할 수 없습니다.

점유 대기#

이미 자원을 보유한 상태에서 다른 자원을 기다립니다.

비선점#

다른 트랜잭션이 가진 자원을 강제로 빼앗지 않습니다.

순환 대기#

트랜잭션 간 대기가 원형으로 연결됩니다.

교착 상태는 이러한 조건이 함께 형성될 때 발생할 수 있습니다.


39장. 타임아웃은 교착을 예방하는 기술과는 다르다#

트랜잭션이 5초 이상 기다리면 중단한다고 하겠습니다.

5초 대기
↓
오류

이렇게 하면 무한히 기다리는 문제는 줄일 수 있습니다.

하지만 다음과 같은 교착 자체는 이미 발생했을 수 있습니다.

A → B → A

타임아웃은 대기를 끊는 방법입니다.

잠금 순서를 통일하는 것은 순환 가능성을 줄이는 방법입니다.

둘은 역할이 다릅니다.


40장. 긴 정상 작업도 타임아웃에 걸릴 수 있다#

배치 작업이 정상적으로 10초 동안 행을 잠근다고 하겠습니다.

온라인 트랜잭션의 잠금 대기 제한이 5초라면 온라인 요청은 오류가 날 수 있습니다.

이것은 반드시 교착 상태라는 뜻은 아닙니다.

B
↓
A 기다림
↓
5초 후 타임아웃

A가 아무것도 기다리지 않고 정상 진행 중일 수 있습니다.

따라서:

대기 시간이 길다
=
교착

은 아닙니다.


41장. 대기 사슬을 봐야 교착인지 알 수 있다#

다음 상황을 생각해 보겠습니다.

T1 → T2
T2 → T3

T3가 아무것도 기다리지 않고 정상 실행 중이라면 T3가 끝난 뒤 T2와 T1도 진행할 수 있습니다.

교착은 아닙니다.

하지만:

T1 → T2
T2 → T3
T3 → T1

이라면 순환입니다.

교착 상태입니다.


42장. PostgreSQL에서는 잠금 현황을 조회할 수 있다#

PostgreSQL에서는 pg_locks와 활동 정보를 이용해 잠금 상태를 분석할 수 있습니다.

예를 들어 현재 잠금 정보를 확인하는 출발점은:

SELECT *
FROM pg_locks;

입니다.

실제 운영 분석에서는 세션 활동 정보와 함께 보아:

누가 기다리는가?

누가 잠금을 보유하는가?

어떤 SQL이 실행 중인가?

를 연결해야 합니다.


43장. 차단 세션 하나가 많은 요청을 줄 세울 수 있다#

트랜잭션 A가 중요한 행을 오래 잠근다고 하겠습니다.

B가 A를 기다립니다.

C가 B 이후 같은 자원을 기다립니다.

D도 기다립니다.

flowchart LR
    D["D"] --> C["C"]
    C --> B["B"]
    B --> A["A"]

실제 사용자 입장에서는 여러 요청이 동시에 느려진 것처럼 보일 수 있습니다.

하지만 근본 원인은 가장 앞의 긴 트랜잭션 A일 수 있습니다.


44장. 교착을 찾는 것과 긴 차단을 찾는 것은 운영상 모두 중요하다#

교착 상태는 DBMS가 감지해 한쪽을 중단할 수 있습니다.

반면 긴 차단은 순환이 없어 자동 교착 감지가 개입하지 않을 수 있습니다.

예:

A가 2분 동안 잠금 유지

B·C·D가 계속 대기

따라서 모니터링에서는 교착 오류뿐 아니라 장시간 열린 트랜잭션과 대기 세션도 확인해야 합니다.


45장. 의향락은 상위 객체와 하위 객체 잠금을 조정하는 개념이다#

DBMS는 테이블과 행처럼 여러 계층의 자원을 관리할 수 있습니다.

어떤 트랜잭션이 테이블 안의 일부 행에 X락을 잡으려고 한다고 하겠습니다.

상위 테이블 수준에는 “하위에 배타 잠금을 사용할 예정”이라는 정보를 표현할 수 있습니다.

이런 개념이 의향락입니다.

대표적으로:

IS
Intent Shared

IX
Intent Exclusive

등이 있습니다.


46장. IX는 테이블 전체에 배타락을 걸었다는 뜻이 아니다#

IX를 보고:

이 트랜잭션이 테이블 전체를 배타 잠금했다.

라고 이해하면 안 됩니다.

의미는:

이 테이블의 하위 객체에 배타 잠금을 잡거나 잡으려는 의도가 있다.

에 가깝습니다.

잠금 계층에서 상위 객체 전체 잠금과 하위 행 잠금을 조정하는 역할입니다.


47장. 잠금 단위를 작게 잡으면 무조건 좋은 것도 아니다#

행 단위 잠금은 충돌 범위를 줄일 수 있습니다.

하지만 수십만 개의 행을 잠가야 한다면 잠금 관리 비용이 커질 수 있습니다.

반대로 테이블 전체를 잠그면 관리할 잠금 수는 줄지만 동시성이 크게 감소합니다.

따라서:

작은 잠금 단위
→ 높은 동시성 가능
→ 많은 잠금 관리 비용

큰 잠금 단위
→ 잠금 수 감소
→ 동시성 감소

의 교환관계가 있습니다.


48장. 타임스탬프 순서화는 잠금과 다른 방식으로 순서를 제어한다#

동시성 제어 이론에는 잠금 외에도 타임스탬프 순서화가 있습니다.

각 트랜잭션에 시작 순서를 나타내는 타임스탬프를 부여한다고 하겠습니다.

T1
TS = 10

T2
TS = 20

T1이 더 오래된 트랜잭션입니다.

기본적인 타임스탬프 순서화에서는 데이터 항목마다 다음 정보를 관리한다고 생각할 수 있습니다.

R-TS(X)
→ X를 성공적으로 읽은 가장 큰 트랜잭션 타임스탬프

W-TS(X)
→ X를 성공적으로 쓴 가장 큰 트랜잭션 타임스탬프

49장. 나중 트랜잭션이 먼저 읽은 값을 오래된 트랜잭션이 쓰려고 하면#

처음에는:

R-TS(X) = 0
W-TS(X) = 0

입니다.

T2가 X를 읽습니다.

TS(T2) = 20

읽기를 허용합니다.

이제:

R-TS(X) = 20

입니다.

그 뒤 오래된 T1이 X를 쓰려고 합니다.

TS(T1) = 10

그런데:

10 < R-TS(X) 20

입니다.

시간 순서에 맞지 않는 쓰기가 되므로 기본 타임스탬프 순서화에서는 T1을 철회시킬 수 있습니다.


50장. 계산 표로 보면#

순서 요청 이전 상태 (R-TS, W-TS) 판단 결과
1 R₂(X) (0, 0) 20 ≥ 0 허용
2 W₁(X) (20, 0) 10 < 20 T1 철회

이 모델에서는 기다리기보다 오래된 순서 규칙을 위반하는 작업을 중단시키는 방식으로 직렬 순서를 유지합니다.


51장. 반대로 새로운 쓰기 뒤 오래된 읽기가 들어와도 문제가 된다#

T2가 먼저 X를 씁니다.

W-TS(X) = 20

그 뒤 T1이 읽으려고 합니다.

TS(T1) = 10

조건은:

10 < 20

입니다.

오래된 T1이 더 새로운 T2가 쓴 버전을 읽게 되어 타임스탬프 순서를 위반하게 됩니다.

따라서 읽기가 거부되거나 T1이 재시작될 수 있습니다.


52장. 타임스탬프 순서화는 PostgreSQL SQL 문법이 아니다#

다음과 같은 R-TS, W-TS를 PostgreSQL에서 직접 SQL로 설정하는 것은 아닙니다.

R-TS(X)

W-TS(X)

이는 동시성 제어를 설명하기 위한 이론 모델입니다.

실제 PostgreSQL은 MVCC와 잠금, 격리 수준을 이용해 동시성을 제어합니다.

개념 이론과 특정 DBMS의 구현을 분리해서 이해해야 합니다.


53장. Thomas 쓰기 규칙은 오래된 쓰기를 항상 실패시키지 않는 변형이다#

기본 타임스탬프 순서화에서는 더 새로운 쓰기가 이미 존재하면 오래된 쓰기를 거부할 수 있습니다.

Thomas Write Rule은 특정 조건에서 그 오래된 쓰기가 더 이상 결과에 영향을 줄 필요가 없다면 단순히 무시할 수 있도록 확장한 개념입니다.

다만:

오래된 쓰기
→ 항상 무시

라는 뜻은 아닙니다.

읽기 순서와 데이터 의존성을 함께 봐야 합니다.


54장. 잠금 방식과 타임스탬프 방식은 충돌 처리 철학이 다르다#

잠금 기반 방식은 대체로:

충돌
↓
기다림

을 사용할 수 있습니다.

타임스탬프 순서화는:

순서 위반
↓
중단·재시작

형태로 처리할 수 있습니다.

물론 실제 시스템 구현은 더 복잡하지만 두 접근의 차이를 이해하는 데 유용합니다.


55장. 재시도에는 중복 업무 처리 문제도 따라온다#

교착으로 주문 트랜잭션이 실패했습니다.

애플리케이션이 다시 시도합니다.

그런데 첫 시도에서 외부 결제 API까지 호출했다면 어떻게 될까요?

DB 트랜잭션은 ROLLBACK됐지만 카드 결제는 이미 성공했을 수도 있습니다.

따라서 재시도 가능한 업무에서는:

DB 변경

외부 API

이벤트 발행

의 경계를 명확히 해야 합니다.

요청 ID와 멱등성 처리도 중요합니다.


56장. 교착 재시도에 무한 반복을 사용하면 안 된다#

다음 방식은 위험합니다.

오류 발생
↓
즉시 재시도
↓
오류
↓
즉시 재시도
↓
무한 반복

동일한 부하와 접근 순서가 계속 유지되면 반복해서 충돌할 수 있습니다.

실제 시스템에서는:

재시도 횟수 제한

짧은 지연

무작위 지연

오류 기록

최종 실패 처리

등을 함께 고려할 수 있습니다.


57장. 재시도 전에 상태를 다시 읽어야 한다#

첫 번째 시도에서:

재고 = 10

을 읽었습니다.

교착 상태로 트랜잭션이 중단됐습니다.

그 사이 다른 트랜잭션이 재고를 2로 줄였습니다.

재시도하면서 이전의:

재고 = 10

값을 그대로 사용하면 잘못된 판단을 할 수 있습니다.

재시도는 새로운 데이터베이스 상태에서 업무를 다시 계산하는 것이어야 합니다.


58장. 실제 교착 분석에서는 SQL뿐 아니라 접근 순서를 기록해야 한다#

단순히 다음 로그만 있으면 부족합니다.

deadlock detected

가능하면 다음도 기록하는 것이 좋습니다.

어떤 트랜잭션이었는가?

어떤 자원을 먼저 잡았는가?

다음에 무엇을 요청했는가?

얼마나 오래 잠금을 보유했는가?

어떤 SQL 경로에서 실행됐는가?

이 정보를 알아야 공통 잠금 순서나 트랜잭션 축소 같은 개선책을 찾을 수 있습니다.


59장. 교착 예방과 교착 처리 전략을 구분하면#

예방·감소#

공통 잠금 순서

짧은 트랜잭션

불필요한 잠금 범위 축소

적절한 인덱스

업무 접근 경로 통일

발생 후 처리#

교착 감지

희생 트랜잭션 ROLLBACK

전체 업무 재시도

재시도 횟수 제한

관찰과 로깅

둘 다 필요합니다.


60장. 인덱스도 잠금 시간에 영향을 줄 수 있다#

다음 UPDATE가 있다고 하겠습니다.

UPDATE orders
SET status = 'CANCELLED'
WHERE customer_id = 1001;

적절한 인덱스가 없으면 대상 행을 찾는 데 많은 데이터를 검사할 수 있습니다.

트랜잭션이 오래 실행되면 잠금을 보유하는 시간도 길어질 수 있습니다.

따라서 인덱스 설계와 잠금 문제는 완전히 별개의 영역이 아닙니다.


61장. 잘못된 대량 UPDATE는 교착보다 더 큰 차단을 만들 수도 있다#

WHERE 조건이 잘못되어 수백만 행을 수정한다고 하겠습니다.

예상 대상
100행

실제 대상
500만 행

수많은 행이 변경되면서 다른 트랜잭션의 대기가 급증할 수 있습니다.

따라서 잠금 문제를 분석할 때는:

왜 이렇게 많은 행을 변경했는가?

도 확인해야 합니다.


62장. 여러 행을 잠글 때 정렬해서 접근하는 패턴#

예를 들어 계좌 101과 55를 동시에 수정해야 합니다.

요청마다 입력 순서가 다를 수 있습니다.

요청 A
101 → 55

요청 B
55 → 101

애플리케이션에서 항상 정렬해:

55 → 101

순으로 처리할 수 있습니다.

예를 들어 SQL에서도 업무에 따라 명확한 순서를 정해 잠금 대상으로 가져오는 전략을 사용할 수 있습니다.

핵심은 입력 순서가 아니라 공통 규칙입니다.


63장. 대기 그래프와 선행 그래프를 한 번 더 비교하면#

구분 대기 그래프 충돌 선행 그래프
목적 교착 상태 확인 충돌 직렬 가능성 확인
노드 트랜잭션 트랜잭션
간선 의미 상대 트랜잭션의 자원을 기다림 충돌 연산이 먼저 실행됨
순환 의미 교착 가능·발생 충돌 직렬 불가능

둘 다 순환을 찾지만 해석은 완전히 다릅니다.


64장. 동시성 제어에서 가장 먼저 물어야 할 질문#

다음 질문부터 확인하면 좋습니다.

  1. 어떤 데이터 항목을 함께 읽고 변경하는가?
  2. 두 트랜잭션이 같은 행을 사용할 가능성이 있는가?
  3. 어떤 순서로 자원을 잠그는가?
  4. 여러 코드 경로에서 잠금 순서가 동일한가?
  5. 트랜잭션은 얼마나 오래 유지되는가?
  6. 외부 API를 기다리면서 잠금을 잡고 있지 않은가?
  7. 교착이 발생하면 전체 트랜잭션을 재시도할 수 있는가?
  8. 재시도 시 데이터를 처음부터 다시 읽는가?
  9. 요청 중복을 막을 식별자가 있는가?
  10. 대기와 교착을 모니터링할 수 있는가?

65장. 핵심 정리#

데이터베이스 락은 여러 트랜잭션이 같은 데이터를 동시에 사용할 때 충돌을 조정하기 위한 수단입니다.

전통적인 기본 모델에서는 다음처럼 이해할 수 있습니다.

공유락 S
→ 여러 읽기 허용

배타락 X
→ 충돌하는 읽기·쓰기 제한

하지만 실제 DBMS는 MVCC와 다양한 잠금 모드를 함께 사용하므로 모든 SELECT가 단순한 S락으로 동작한다고 생각해서는 안 됩니다.

잠금 대기와 교착 상태도 반드시 구분해야 합니다.

A → B

만 있다면 정상적인 대기일 수 있습니다.

하지만:

A → B
B → A

처럼 대기가 순환하면 교착 상태가 됩니다.

교착을 줄이는 가장 실용적인 방법 가운데 하나는 공통된 잠금 획득 순서를 사용하는 것입니다.

항상 작은 ID부터

항상 주문 → 재고 → 포인트 순서

처럼 시스템 전체의 접근 순서를 통일하면 특정 순환을 줄일 수 있습니다.

2단계 로킹은 잠금 획득 단계와 해제 단계를 분리하는 이론적 프로토콜입니다.

확장 단계
→ 잠금 획득

축소 단계
→ 잠금 해제

이를 통해 충돌 직렬 가능성을 보장할 수 있지만 기본 2PL만으로 회복 가능성과 연쇄 철회 문제가 모두 해결되는 것은 아닙니다.

직렬 가능성은 충돌 선행 그래프로 판단할 수 있습니다.

그래프에 순환 없음
→ 충돌 직렬 가능

그래프에 순환 있음
→ 충돌 직렬 가능하지 않음

반면 현재 교착 상태를 분석할 때는 잠금 대기 그래프를 봅니다.

두 그래프는 목적이 다릅니다.

또 교착 상태로 한 트랜잭션이 취소됐다면 마지막 SQL만 다시 실행해서는 안 됩니다.

그 사이 데이터가 바뀌었을 수 있기 때문에:

ROLLBACK
↓
새 트랜잭션
↓
데이터 다시 읽기
↓
업무 규칙 재검사
↓
전체 작업 재실행

과 같은 방식으로 처리하는 것이 중요합니다.

동시성 제어의 핵심은 잠금을 최대한 많이 사용하는 것이 아닙니다.

필요한 데이터를 필요한 순서로 짧게 보호하면서, 실패했을 때 업무 전체를 안전하게 다시 실행할 수 있게 만드는 것입니다.

락을 이해할 때도 가장 중요한 것은 이름을 외우는 것이 아니라 다음을 그려 보는 것입니다.

누가 무엇을 가지고 있고, 다음에 무엇을 기다리고 있는가?

이 흐름을 그릴 수 있으면 단순한 잠금 대기와 실제 교착 상태를 구분하고, 잠금 순서와 재시도 정책까지 연결할 수 있습니다.

이 페이지의 목차