icon

안동민 개발노트

10장 : 트랜잭션

트랜잭션 상태와 스케줄


트랜잭션은 BEGIN부터 COMMIT 또는 ROLLBACK까지의 여정에서 여러 상태를 거칩니다. 마치 택배가 접수 → 집하 → 배송 중 → 배달 완료 또는 접수 → 집하 → 분실 → 반환의 경로를 밟듯이, 트랜잭션도 정해진 상태 전이 경로를 따릅니다. 그리고 여러 트랜잭션이 동시에 실행될 때, 그 연산들의 실행 순서를 스케줄(Schedule)이라 합니다. 스케줄이 안전한지 판단하는 기준이 직렬 가능성(Serializability)이고, 이것이 동시성 제어의 이론적 토대입니다.

이 절에서는 트랜잭션의 상태 전이를 상세히 살펴보고, 직렬 스케줄과 비직렬 스케줄의 차이, 충돌 직렬 가능성의 판단 방법인 선행 그래프, 그리고 회복 가능 스케줄까지 깊이 있게 다룹니다.


트랜잭션 상태

트랜잭션은 생성부터 종료까지 다섯 가지 상태를 거칩니다. 각 상태는 명확한 의미를 가지며, 상태 전이는 단방향입니다. 한번 커밋된 트랜잭션은 다시 활동 상태로 돌아갈 수 없고, 한번 철회된 트랜잭션도 마찬가지입니다.

상태 전이 다이어그램

트랜잭션 상태 전이
                  ┌──────────┐
                  │   활동   │ ← SQL 실행 중
                  │ (Active) │
                  └────┬─────┘

              ┌────────┴────────┐
              │                 │
              ▼                 ▼
      ┌──────────────┐     ┌──────────┐
      │  부분 커밋   │     │   실패   │
      │(Part. Commit)│     │ (Failed) │
      └──────┬───────┘     └────┬─────┘
             │                  │
             ▼                  ▼
      ┌──────────────┐     ┌──────────┐
      │    커밋      │     │   철회   │
      │ (Committed)  │     │(Aborted) │
      └──────────────┘     └──────────┘

각 상태의 의미

활동 (Active)

트랜잭션이 시작되어 SQL 연산을 실행하고 있는 상태입니다. BEGIN TRANSACTION 또는 첫 번째 SQL 문이 실행된 순간부터 이 상태에 진입합니다. 이 상태에서는 SELECT, INSERT, UPDATE, DELETE 등의 연산이 수행되며, 변경 사항은 아직 버퍼(메모리)에만 존재합니다. 디스크에 영구 반영된 것이 아니므로, 장애가 발생하면 모든 변경이 사라질 수 있습니다.

부분 커밋 (Partially Committed)

트랜잭션의 마지막 연산까지 실행이 완료된 상태입니다. COMMIT 명령을 받았지만, 아직 Redo 로그가 디스크에 기록되지 않은 과도기적 상태입니다. 이 단계에서 두 가지 경로가 갈립니다.

Redo 로그가 디스크에 성공적으로 기록되면(WAL, Write-Ahead Logging) → 커밋 상태로 전이됩니다. 이 순간이 진정한 커밋 포인트(Commit Point)입니다. 만약 로그 기록 중 디스크 장애가 발생하면 → 실패 상태로 전이되고, ROLLBACK이 진행됩니다.

부분 커밋의 의미
BEGIN;
UPDATE accounts SET balance = balance - 50000 WHERE id = 'A';
UPDATE accounts SET balance = balance + 50000 WHERE id = 'B';
COMMIT;  ← 이 시점에서 "부분 커밋" 상태

[Redo 로그 디스크 기록 시도]
  ├── 성공 → "커밋" 상태 (영구 반영 보장)
  └── 실패 → "실패" 상태 → ROLLBACK → "철회" 상태

부분 커밋과 커밋이 구분되는 이유는 DBMS의 내부 동작 때문입니다. 사용자가 COMMIT을 입력하면 DBMS는 즉시 트랜잭션이 확정되었습니다라고 응답하지 않습니다. 먼저 변경 사항의 로그를 디스크에 기록하고, 그 기록이 성공해야 비로소 커밋이 확정됩니다.

커밋 (Committed)

트랜잭션의 모든 변경이 영구적으로 반영된 상태입니다. Redo 로그가 디스크에 기록 완료됨으로써 보장됩니다. 이 상태에 도달하면 시스템 장애가 발생해도 변경 사항은 복구됩니다. DBMS가 재시작되면 Redo 로그를 읽어서 변경 사항을 다시 적용(Redo)하기 때문입니다.

실패 (Failed)

트랜잭션 실행 중 오류가 발생한 상태입니다. 오류의 원인은 다양합니다.

실패 원인들
* 제약 조건 위반 — UNIQUE 중복, FK 참조 무결성 위반
* 데이터 타입 오류 — 문자열을 숫자 컬럼에 넣으려 함
* 교착 상태 (Deadlock) — DBMS가 하나의 트랜잭션을 희생자로 선택
* 디스크 공간 부족 — 테이블스페이스 또는 Redo 로그 공간 부족
* 타임아웃 — 락 대기 시간 초과
* 사용자 취소 — 애플리케이션에서 명시적 ROLLBACK

실패 상태에 진입하면 더 이상 SQL 연산을 실행할 수 없으며, ROLLBACK만 가능합니다.

철회 (Aborted)

ROLLBACK이 완료되어 트랜잭션 시작 전 상태로 복원된 상태입니다. Undo 로그를 사용하여 모든 변경을 역순으로 되돌립니다. 철회 후에는 두 가지 선택이 가능합니다.

철회 후 선택
철회 완료 후:
  ├── 재시도 — 같은 트랜잭션을 다시 시작 (일시적 오류의 경우)
  └── 포기 — 트랜잭션을 완전히 종료 (영구적 오류의 경우)

교착 상태로 인한 실패라면 재시도가 적절하고, 비즈니스 규칙 위반이라면 포기가 적절합니다.

상태 전이 규칙 요약

현재 상태전이 조건다음 상태
활동마지막 연산 완료 + COMMIT 요청부분 커밋
활동오류 발생실패
부분 커밋Redo 로그 디스크 기록 성공커밋
부분 커밋Redo 로그 기록 실패실패
실패ROLLBACK 완료철회
커밋종료 (최종 상태)
철회종료 (최종 상태)

중요한 것은 상태 전이가 단방향이라는 점입니다. 커밋된 트랜잭션을 철회할 수 없고, 철회된 트랜잭션을 커밋할 수 없습니다. 이것이 트랜잭션의 지속성(Durability)을 보장하는 핵심 원리입니다.


스케줄의 개념

여러 트랜잭션이 동시에 실행될 때, 각 트랜잭션의 연산들이 어떤 순서로 실행되는지를 나타낸 것이 스케줄(Schedule)입니다. 예를 들어 T1이 {R(A), W(A), R(B), W(B)}을 실행하고, T2가 {R(A), W(A)}를 실행한다면, 이 6개 연산의 실행 순서가 하나의 스케줄입니다.

스케줄에서 중요한 제약은 각 트랜잭션 내부의 연산 순서는 변경할 수 없다는 것입니다. T1이 R(A) 후에 W(A)를 실행하도록 정의되어 있다면, 어떤 스케줄에서도 T1의 R(A)는 반드시 T1의 W(A)보다 먼저 실행되어야 합니다. 하지만 T1의 연산들 사이에 T2의 연산이 끼어들 수 있습니다.


직렬 스케줄 (Serial Schedule)

직렬 스케줄은 트랜잭션들이 하나씩 순차적으로 실행되는 스케줄입니다. 한 트랜잭션이 완전히 끝나야 다음 트랜잭션이 시작됩니다.

직렬 스케줄 예시
T1과 T2가 순차 실행 (순서 1: T1 → T2):

시간 →
T1: [R(A)  W(A)  R(B)  W(B)  COMMIT]
T2:                                   [R(A)  W(A)  R(B)  W(B)  COMMIT]

순서 2: T2 → T1:
T2: [R(A)  W(A)  R(B)  W(B)  COMMIT]
T1:                                   [R(A)  W(A)  R(B)  W(B)  COMMIT]

n개의 트랜잭션이 있으면 n!개의 직렬 스케줄이 가능합니다. T1, T2 두 개라면 2! = 2개(T1→T2, T2→T1), T1, T2, T3 세 개라면 3! = 6개의 직렬 스케줄이 존재합니다.

직렬 스케줄의 특징은 항상 정확한 결과를 보장한다는 것입니다. 트랜잭션이 겹치지 않으므로 간섭이 발생할 수 없습니다. 하지만 동시성이 전혀 없어서 성능이 매우 나쁩니다. CPU가 I/O를 기다리는 동안에도 다른 트랜잭션을 실행할 수 없기 때문입니다.

직렬 스케줄의 성능 문제
T1: [CPU] [I/O 대기....] [CPU] [I/O 대기....] [COMMIT]
T2:                                                     [CPU] [I/O...]

→ T1의 I/O 대기 시간 동안 CPU가 놀고 있음
→ 전체 처리 시간이 길어짐

비직렬 스케줄 (Non-serial Schedule)

비직렬 스케줄은 여러 트랜잭션의 연산이 교차(인터리빙)되어 실행되는 스케줄입니다.

비직렬 스케줄 예시
T1: R(A) ──── W(A) ────── R(B) ── W(B) ── COMMIT
T2: ──── R(A) ──── W(A) ────── R(B) ── W(B) ── COMMIT

→ T1과 T2의 연산이 교차됨
→ T1이 I/O를 기다리는 사이에 T2가 CPU를 사용

비직렬 스케줄의 장점은 높은 동시성입니다. 여러 트랜잭션이 자원을 효율적으로 공유하여 전체 처리량(throughput)이 크게 향상됩니다.

비직렬 스케줄의 성능 이점
T1: [CPU] [I/O 대기....] [CPU] [I/O 대기....] [COMMIT]
T2: ───── [CPU] [I/O...] ───── [CPU] [I/O...] ──── [COMMIT]

→ T1이 I/O를 기다리는 동안 T2가 CPU를 사용
→ 전체 처리 시간 단축

하지만 비직렬 스케줄은 연산의 교차 방식에 따라 정확하지 않은 결과를 낼 수 있습니다. 다음 예시를 보겠습니다.

잘못된 비직렬 스케줄
초기값: A = 100, B = 100
T1: A에서 B로 50 이체 (A = A - 50, B = B + 50)
T2: A와 B 모두 2배 (A = A * 2, B = B * 2)

정상 결과 (T1 → T2):
  T1 후: A = 50, B = 150 (합계 200)
  T2 후: A = 100, B = 300 (합계 400)

정상 결과 (T2 → T1):
  T2 후: A = 200, B = 200 (합계 400)
  T1 후: A = 150, B = 250 (합계 400)

잘못된 비직렬 스케줄:
  T1: R(A)=100  W(A)=50                     R(B)=100  W(B)=150
  T2:                    R(A)=50  W(A)=100                      R(B)=150  W(B)=300

  결과: A = 100, B = 300 (합계 400) → 이 경우는 T1→T2와 같은 결과로 OK

다른 잘못된 비직렬 스케줄:
  T1: R(A)=100                              W(A)=50   R(B)=100  W(B)=150
  T2:           R(A)=100  W(A)=200  R(B)=100  W(B)=200

  결과: A = 50, B = 150 (합계 200) → T2의 변경이 T1에 의해 덮어쓰기됨!
  → 어떤 직렬 순서와도 다른 결과 → 데이터 불일치!

이처럼 비직렬 스케줄은 교차 방식에 따라 올바른 결과를 낼 수도 있고, 잘못된 결과를 낼 수도 있습니다. 그래서 어떤 비직렬 스케줄이 안전한가?를 판단하는 기준이 필요합니다. 그것이 바로 직렬 가능성입니다.


직렬 가능성 (Serializability)

비직렬 스케줄 S의 실행 결과가 어떤 직렬 스케줄의 결과와 동일하다면, 스케줄 S는 직렬 가능(serializable)하다고 합니다. 직렬 가능한 스케줄은 동시성의 이점을 누리면서도 정확한 결과를 보장합니다.

직렬 가능성에는 두 가지 유형이 있습니다. 충돌 직렬 가능성(Conflict Serializability)과 뷰 직렬 가능성(View Serializability)입니다.

충돌 연산 (Conflicting Operations)

두 연산이 충돌한다는 것은 다음 세 조건을 모두 만족하는 경우입니다.

충돌 조건
1. 서로 다른 트랜잭션에 속한다 (Ti ≠ Tj)
2. 같은 데이터 항목에 접근한다 (같은 A에 접근)
3. 둘 중 하나 이상이 쓰기(Write)이다

충돌하는 쌍:
  * R_i(A) vs W_j(A) — 읽기-쓰기 충돌
  * W_i(A) vs R_j(A) — 쓰기-읽기 충돌
  * W_i(A) vs W_j(A) — 쓰기-쓰기 충돌

충돌하지 않는 쌍:
  * R_i(A) vs R_j(A) — 읽기-읽기 (충돌 아님)
  * R_i(A) vs W_j(B) — 다른 데이터 항목 (충돌 아님)
  * W_i(A) vs W_i(A) — 같은 트랜잭션 (충돌 아님)

충돌하는 연산의 순서를 바꾸면 결과가 달라질 수 있지만, 충돌하지 않는 연산의 순서는 바꿔도 결과가 같습니다.

충돌 동등 (Conflict Equivalent)

두 스케줄 S1과 S2가 충돌 동등하다는 것은, S1에서 충돌하지 않는 연산들의 순서를 바꿔서 S2를 만들 수 있다는 뜻입니다. 다시 말해, 충돌하는 연산의 상대적 순서가 두 스케줄에서 동일합니다.

충돌 직렬 가능성 (Conflict Serializability)

비직렬 스케줄 S가 어떤 직렬 스케줄 S'와 충돌 동등하면, S는 충돌 직렬 가능합니다.

충돌 직렬 가능성을 판단하는 실용적인 방법이 선행 그래프(Precedence Graph)입니다.

선행 그래프 (Precedence Graph)

선행 그래프는 트랜잭션 간의 충돌 관계를 방향 그래프로 표현한 것입니다.

선행 그래프 구성 방법
1. 각 트랜잭션을 노드로 표현
2. Ti의 연산이 Tj의 충돌 연산보다 먼저 실행되면 Ti → Tj 간선 추가
3. 그래프에 사이클이 없으면 → 충돌 직렬 가능 ✓
4. 그래프에 사이클이 있으면 → 충돌 직렬 불가능 ✗

구체적인 예시로 살펴보겠습니다.

예시 1: 충돌 직렬 가능한 스케줄
스케줄 S:
  T1: R(A)  W(A)              R(B)  W(B)
  T2:             R(A)  W(A)              R(B)  W(B)

충돌 쌍 분석:
  * T1.W(A) → T2.R(A): T1이 A를 먼저 쓰고 T2가 A를 읽음 → T1 → T2
  * T1.W(A) → T2.W(A): T1이 A를 먼저 쓰고 T2가 A를 씀 → T1 → T2
  * T1.W(B) → T2.R(B): T1이 B를 먼저 쓰고 T2가 B를 읽음 → T1 → T2
  * T1.W(B) → T2.W(B): T1이 B를 먼저 쓰고 T2가 B를 씀 → T1 → T2

선행 그래프:
  T1 ──→ T2  (모든 간선이 T1 → T2 방향)

사이클 없음 → 충돌 직렬 가능 ✓
→ 직렬 순서: T1 → T2
예시 2: 충돌 직렬 불가능한 스케줄
스케줄 S:
  T1: R(A)        W(A)  R(B)        W(B)
  T2:       R(B)              W(B)        R(A)  W(A)

충돌 쌍 분석:
  * T1.W(A) → T2.R(A): T1 → T2 (A에 대해)
  * T2.W(B) → T1.R(B)? 아니요, T1.R(B)가 T2.W(B)보다 먼저
  * T1.R(B) vs T2.W(B): T1이 B를 먼저 읽고 T2가 B를 씀 → T1 → T2
  * T2.R(B) vs T1.W(B): T2가 B를 먼저 읽고 T1이 B를 씀 → T2 → T1

선행 그래프:
  T1 ──→ T2  (A에 대한 충돌)
  T2 ──→ T1  (B에 대한 충돌)

사이클 있음 (T1 → T2 → T1) → 충돌 직렬 불가능 ✗

선행 그래프의 실무적 의미

DBMS가 실시간으로 선행 그래프를 구성하여 모든 스케줄의 직렬 가능성을 검사하는 것은 비용이 큽니다. 대신 DBMS는 애초에 직렬 불가능한 스케줄이 만들어지지 않도록 예방합니다. 그 예방 기법이 바로 2단계 로킹(2PL), 타임스탬프 순서 기법, MVCC 등의 동시성 제어 프로토콜입니다.

직렬 가능성 보장 방법
방법 1: 락 기반 (2PL)
  → 트랜잭션이 필요한 자원에 락을 걸어 충돌 방지
  → 선행 그래프에 사이클이 생기지 않도록 보장

방법 2: 타임스탬프 기반
  → 각 트랜잭션에 시작 시각 기반 타임스탬프 부여
  → 충돌 시 타임스탬프 순서를 위반하는 트랜잭션을 중단

방법 3: MVCC (Multi-Version Concurrency Control)
  → 데이터의 여러 버전을 유지
  → 읽기는 과거 버전을 읽고, 쓰기만 충돌 검사

뷰 직렬 가능성 (View Serializability)

뷰 직렬 가능성은 충돌 직렬 가능성보다 더 넓은 개념입니다. 스케줄 S와 직렬 스케줄 S'에서 다음 세 조건이 모두 같으면 뷰 동등(view equivalent)하다고 합니다.

뷰 동등 조건
1. 동일한 초기 읽기: S에서 Ti가 데이터 Q의 초기값을 읽으면,
   S'에서도 Ti가 Q의 초기값을 읽어야 함

2. 동일한 읽기 소스: S에서 Ti가 Tj가 쓴 Q 값을 읽으면,
   S'에서도 Ti가 Tj가 쓴 Q 값을 읽어야 함

3. 동일한 최종 쓰기: S에서 Ti가 Q의 최종 값을 쓰면,
   S'에서도 Ti가 Q의 최종 값을 써야 함

뷰 직렬 가능한 스케줄 집합은 충돌 직렬 가능한 스케줄 집합을 포함합니다. 즉, 충돌 직렬 가능하면 반드시 뷰 직렬 가능하지만, 뷰 직렬 가능하다고 반드시 충돌 직렬 가능한 것은 아닙니다.

포함 관계
┌────────────────────────────────────────┐
│         모든 스케줄                    │
│  ┌──────────────────────────────────┐  │
│  │    뷰 직렬 가능 (View)           │  │
│  │  ┌────────────────────────────┐  │  │
│  │  │  충돌 직렬 가능 (Conflict) │  │  │
│  │  │  ┌──────────────────────┐  │  │  │
│  │  │  │    직렬 (Serial)     │  │  │  │
│  │  │  └──────────────────────┘  │  │  │
│  │  └────────────────────────────┘  │  │
│  └──────────────────────────────────┘  │
└────────────────────────────────────────┘

하지만 뷰 직렬 가능성 검사는 NP-complete 문제로 실용적이지 않습니다. 그래서 실제 DBMS에서는 충돌 직렬 가능성을 기준으로 동시성 제어를 수행합니다.


회복 가능 스케줄 (Recoverable Schedule)

직렬 가능성과는 별도로 스케줄의 회복 가능성도 중요합니다. 트랜잭션이 실패했을 때 안전하게 롤백할 수 있는 스케줄이어야 합니다.

회복 불가능한 스케줄의 문제

회복 불가능한 스케줄
T1: W(A)=100 ─────────── ROLLBACK
T2: ──────── R(A)=100 ── COMMIT

T2는 T1이 쓴 값(100)을 읽고 커밋했음
T1이 롤백하면 A=100은 없었던 값
→ T2는 존재하지 않는 값을 기반으로 커밋됨
→ T2를 롤백해야 하지만 이미 커밋됨
→ 회복 불가능!

이 문제가 더티 읽기(Dirty Read)에 기반한 커밋입니다. T2가 T1의 아직 커밋되지 않은 데이터를 읽고 먼저 커밋하면, T1이 롤백되었을 때 T2도 롤백해야 하지만 이미 커밋되어 돌이킬 수 없습니다.

회복 가능 조건

스케줄이 회복 가능하려면: Ti가 Tj가 쓴 데이터를 읽었다면, Tj의 커밋이 Ti의 커밋보다 먼저 이루어져야 합니다.

회복 가능한 스케줄
T1: W(A)=100 ───────── COMMIT
T2: ──────── R(A)=100 ──────── COMMIT

T2는 T1이 커밋한 후에 커밋
→ T1이 롤백할 일이 없으므로 안전
→ 회복 가능!

비연쇄적 스케줄 (Cascadeless Schedule)

회복 가능 스케줄 중에서도, 커밋되지 않은 데이터를 아예 읽지 않는 스케줄을 비연쇄적 스케줄이라 합니다. 이는 연쇄적 롤백(Cascading Rollback)을 방지합니다.

연쇄적 롤백 문제
T1: W(A) ──────────────────── ROLLBACK
T2: ──── R(A) W(B) ────────── (T1 롤백 → T2도 롤백 필요)
T3: ──────────── R(B) W(C) ── (T2 롤백 → T3도 롤백 필요)

T1의 롤백이 T2를 롤백시키고, T2의 롤백이 T3를 롤백시킴
→ 연쇄적 롤백 (Cascading Rollback)
→ 성능과 가용성에 큰 타격

비연쇄적 스케줄에서는 Ti가 Tj가 쓴 데이터를 읽으려면, Tj가 이미 커밋된 상태여야 합니다. 이렇게 하면 Tj가 롤백될 일이 없으므로 연쇄적 롤백이 발생하지 않습니다.

엄격한 스케줄 (Strict Schedule)

가장 엄격한 조건의 스케줄입니다. Ti가 쓴 데이터를 Tj가 읽거나 쓰려면, Ti가 이미 커밋 또는 롤백된 상태여야 합니다. 읽기뿐 아니라 쓰기도 제한합니다.

스케줄 유형의 포함 관계
┌──────────────────────────────────────┐
│        모든 스케줄                   │
│   ┌────────────────────────────────┐ │
│   │    회복 가능 (Recoverable)     │ │
│   │  ┌──────────────────────────┐  │ │
│   │  │  비연쇄적 (Cascadeless)  │  │ │
│   │  │  ┌────────────────────┐  │  │ │
│   │  │  │  엄격 (Strict)     │  │  │ │
│   │  │  └────────────────────┘  │  │ │
│   │  └──────────────────────────┘  │ │
│   └────────────────────────────────┘ │
└──────────────────────────────────────┘

대부분의 상용 DBMS는 엄격한 2PL(Strict 2PL)을 사용하여 엄격한 스케줄을 보장합니다.


DBMS에서의 스케줄 관리

실제 DBMS가 스케줄을 어떻게 관리하는지 정리합니다.

동시성 제어 프로토콜과 스케줄

프로토콜보장하는 직렬 가능성스케줄 유형사용 DBMS
Basic 2PL충돌 직렬 가능회복 가능이론적
Strict 2PL충돌 직렬 가능엄격MySQL InnoDB, SQL Server
Rigorous 2PL충돌 직렬 가능엄격Oracle
MVCC스냅샷 격리비연쇄적PostgreSQL, Oracle
SSI직렬 가능비연쇄적PostgreSQL (SERIALIZABLE)

격리 수준과 스케줄의 관계

격리 수준별 허용 스케줄
READ UNCOMMITTED:
  → 더티 읽기 허용 → 회복 불가능한 스케줄 가능
  → 실무에서 거의 사용하지 않음

READ COMMITTED:
  → 커밋된 데이터만 읽음 → 비연쇄적 스케줄 보장
  → Oracle, PostgreSQL 기본값

REPEATABLE READ:
  → 같은 데이터를 반복 읽어도 같은 값 → 더 강한 제약
  → MySQL InnoDB 기본값

SERIALIZABLE:
  → 직렬 가능 스케줄 보장
  → 성능 비용이 크지만 완벽한 정확성

직렬 가능성 검증 실습

선행 그래프를 직접 그려서 충돌 직렬 가능성을 판단하는 연습을 해봅시다.

실습 문제
스케줄 S: R1(A) R2(A) W1(A) R1(B) R2(B) W2(A) W1(B) W2(B)

단계 1: 충돌 쌍 찾기

  같은 데이터 + 다른 트랜잭션 + 하나 이상 쓰기:

  (1) R2(A) < W1(A) → T2가 A를 먼저 읽고, T1이 A를 씀 → T2 → T1
  (2) R1(A) < W2(A) → T1이 A를 먼저 읽고, T2가 A를 씀 → T1 → T2
  (3) W1(A) < W2(A) → T1이 A를 먼저 쓰고, T2가 A를 씀 → T1 → T2
  (4) R2(B) < W1(B) → T2가 B를 먼저 읽고, T1이 B를 씀 → T2 → T1
  (5) R1(B) < W2(B) → T1이 B를 먼저 읽고, T2가 B를 씀 → T1 → T2
  (6) W1(B) < W2(B) → T1이 B를 먼저 쓰고, T2가 B를 씀 → T1 → T2

단계 2: 선행 그래프

  T1 ──→ T2  (간선 2, 3, 5, 6)
  T2 ──→ T1  (간선 1, 4)

단계 3: 사이클 검사

  T1 → T2 → T1 (사이클 존재!)

결론: 충돌 직렬 불가능 ✗
실습 문제 2
스케줄 S: R1(A) W1(A) R2(A) W2(A) R1(B) W1(B) R2(B) W2(B)

충돌 쌍 분석:
  (1) W1(A) < R2(A) → T1 → T2
  (2) W1(A) < W2(A) → T1 → T2
  (3) W1(B) < R2(B) → T1 → T2
  (4) W1(B) < W2(B) → T1 → T2

선행 그래프:
  T1 ──→ T2  (모든 간선이 같은 방향)

사이클 없음 → 충돌 직렬 가능 ✓
→ T1 → T2 직렬 순서와 동등

세 개 이상의 트랜잭션에서의 선행 그래프

트랜잭션이 세 개 이상일 때의 선행 그래프도 같은 원리로 분석합니다.

3개 트랜잭션 예시
스케줄 S: R1(A) R2(B) W3(A) W1(A) W2(B) R3(B)

충돌 쌍:
  R1(A) < W3(A): T1 → T3
  W3(A) < W1(A): T3 → T1
  R2(B) < R3(B): 충돌 아님 (읽기-읽기)
  W2(B) < R3(B): T2 → T3

선행 그래프:
  T1 ──→ T3 ──→ T1  (사이클!)
  T2 ──→ T3

사이클 T1 ↔ T3 존재 → 충돌 직렬 불가능 ✗
사이클 없는 3개 트랜잭션
스케줄 S: R1(A) W1(A) R2(A) W2(B) R3(B) W3(B)

충돌 쌍:
  W1(A) < R2(A): T1 → T2
  W2(B) < R3(B): T2 → T3
  W2(B) < W3(B): T2 → T3

선행 그래프:
  T1 ──→ T2 ──→ T3

사이클 없음 → 충돌 직렬 가능 ✓
→ 직렬 순서: T1 → T2 → T3

요약 정리

개념설명
트랜잭션 상태활동 → 부분 커밋 → 커밋 (또는 실패 → 철회)
직렬 스케줄트랜잭션이 순차 실행, 안전하지만 느림
비직렬 스케줄연산이 교차 실행, 빠르지만 불안전할 수 있음
충돌 직렬 가능선행 그래프에 사이클이 없으면 안전
뷰 직렬 가능더 넓은 범위의 안전한 스케줄, 검증은 NP-complete
회복 가능 스케줄읽은 데이터의 작성자가 먼저 커밋해야 함
비연쇄적 스케줄커밋된 데이터만 읽음, 연쇄 롤백 방지
엄격한 스케줄커밋/롤백 전까지 읽기와 쓰기 모두 차단

트랜잭션의 상태 전이는 DBMS가 장애 복구를 할 때 핵심이 됩니다. 활동 또는 부분 커밋 상태에서 장애가 발생하면 Undo를 적용하고, 커밋 상태에서 아직 데이터가 디스크에 반영되지 않았으면 Redo를 적용합니다. 스케줄의 직렬 가능성은 동시성 제어의 정확성을 판단하는 이론적 기준이며, 선행 그래프는 그 판단을 위한 실용적 도구입니다.

다음 절에서는 실무에서 트랜잭션을 어떻게 관리하는지 다루겠습니다.

목차