낙관적 병행제어는 데이터베이스 환경에서 여러 트랜잭션이 동시에 실행될 때, 트랜잭션 간의 데이터 충돌이 거의 발생하지 않을 것이라고 '낙관적'으로 가정하는 동시성 제어 기법입니다.
데이터를 읽거나 수정할 때 자원에 미리 락(Lock)을 걸지 않고 자유롭게 연산을 수행한 뒤, 최종적으로 변경사항을 실제 데이터베이스에 반영(Commit)하기 직전에 충돌 여부를 검사하는 것이 핵심입니다.

작동 원리 (3단계)
낙관적 병행제어는 일반적으로 다음 세 가지 단계를 거쳐 트랜잭션을 처리합니다.
- 1. 읽기 단계 (Read Phase): 트랜잭션이 실제 데이터베이스의 데이터가 아닌, 각자의 개인 작업 공간(Local Workspace)으로 데이터를 복사해 옵니다.
- 모든 읽기와 쓰기 작업은 이 복사본을 대상으로만 이루어지며, 실제 데이터베이스에는 어떠한 락도 걸지 않습니다.

- 2. 검증 단계 (Validation Phase): 트랜잭션의 연산이 모두 끝나고 결과를 실제 데이터베이스에 반영(Commit)하기 직전에 수행됩니다.
- 트랜잭션이 시작된 시점부터 지금까지, 자신이 읽고 수정한 데이터가 다른 트랜잭션에 의해 변경되지 않았는지, 직렬성(Serializability)이 유지되는지 검사합니다.
- 3. 쓰기 단계 (Write Phase)
- 검증 성공: 충돌이 없다면, 개인 작업 공간에서 변경된 내용을 실제 데이터베이스에 영구적으로 반영합니다.
- 검증 실패: 충돌이 감지되었다면, 진행했던 트랜잭션을 모두 취소(Rollback)하고 처음부터 다시 시작합니다.

구체적인 예시
위키백과와 같은 온라인 공동 문서 편집 시스템을 상상해 보면 이해가 쉽습니다.
- 사용자 A와 사용자 B가 동시에 '인공지능'이라는 문서를 열어 수정을 시작합니다. (락이 없으므로 둘 다 지연 없이 문서를 열람하고 편집할 수 있습니다. - 읽기 단계)
- 사용자 A가 먼저 내용 수정을 마치고 '저장' 버튼을 누릅니다. 시스템은 A가 문서를 열었을 때의 상태와 현재 상태가 동일함을 확인하고 저장을 완료합니다. (검증 성공 및 쓰기 완료)
- 잠시 후, 사용자 B가 내용 수정을 마치고 '저장' 버튼을 누릅니다.
- 이때 시스템은 B가 처음에 문서를 열었을 때의 버전과, 현재 서버에 저장된 버전(A가 이미 수정한 버전)이 다르다는 것을 감지합니다. (검증 실패)
- 시스템은 사용자 B의 저장을 거부하고, "다른 사용자에 의해 문서가 먼저 변경되었습니다"라는 경고와 함께 병합을 요구하거나 작업을 취소시킵니다. (롤백)
비관적 병행제어(PCC)와의 비교
낙관적 병행제어는 락을 적극적으로 사용하는 비관적 병행제어(Pessimistic Concurrency Control)와 대비됩니다.
| 구분 | 낙관적 병행제어 (Optimistic) | 비관적 병행제어 (Pessimistic) |
| 기본 가정 | 충돌이 거의 발생하지 않을 것이다 | 충돌이 자주 발생할 것이다 |
| 락(Lock) 사용 | 사용하지 않음 (커밋 시점에만 검증) | 데이터를 읽거나 수정할 때 즉시 락 사용 |
| 교착상태(Deadlock) | 발생하지 않음 | 발생 가능성 있음 (데드락 방지 로직 필요) |
| 주요 문제점 | 충돌 발생 시 롤백 및 재시작 비용이 큼 | 락으로 인한 대기 시간 증가 및 동시성 저하 |
| 적합한 환경 | 데이터 읽기 비중이 압도적으로 높은 시스템 | 데이터 수정이 빈번하고 충돌 가능성이 높은 시스템 |
장단점 요약
- 장점: 데이터를 읽고 쓰는 과정에서 락을 획득하고 해제하는 오버헤드가 없기 때문에, 데이터 충돌이 적은 환경에서는 시스템의 동시 처리 능력을 극대화할 수 있습니다. 또한 구조적으로 교착상태(Deadlock)가 발생하지 않습니다.
- 단점: 실제 충돌이 발생했을 때 트랜잭션을 완전히 롤백하고 재수행해야 하므로, 데이터 변경이 잦은 환경에서는 오히려 시스템 자원 낭비와 성능 저하를 초래할 수 있습니다.
(문제) 아래 트랜젝션 스케줄을 낙관적병행제어 기법으로 생성 가능한가?
| 시간 흐름 | T1 | T2 |
| t1 | read(x) | |
| t2 | read(x) x = x - 50 read(y) y = y + 50 |
|
| t3 | read(y) display(x+y) |
|
| t4 | write(x) write(y) |
1. 스케줄의 직렬 가능성 (Conflict Serializability) 확인
먼저 해당 스케줄이 직렬 가능한지(Serializable) 확인해야 합니다.
- 충돌 연산 분석
- T_1의 read(x)와 T_2의 write(x) 충돌 → T_1이 먼저 수행됨 (T_1 → T_2)
- T_1의 read(y)와 T_2의 write(y) 충돌 → T_1이 먼저 수행됨 (T_1 → T_2)
- 우선순위 그래프(Precedence Graph)
- T_1에서 T_2로 향하는 단방향 간선만 존재하며 사이클(Cycle)이 발생하지 않습니다.
- 결론
- 이 스케줄은 충돌직렬가능(Conflict Serializable)하며, 논리적으로 직렬 스케줄 T_1 → T_2와 동일한 결과를 보장합니다.
2. 낙관적 병행 제어 (OCC) 적용 가능 여부
OCC는 모든 트랜잭션이 읽기(Read) → 검증(Validation) → 쓰기(Write)의 3단계를 거칩니다. 스케줄의 검증 통과 여부를 확인합니다.
- 특징 파악: 주어진 스케줄에서 T_2의 쓰기 연산(write(x), write(y))이 맨 마지막으로 지연되어 있습니다. 이는 전형적인 OCC의 동작 방식입니다.
- T_1의 검증: T_1은 display(x+y) 수행 후 검증 단계에 진입합니다.
- T_1은 쓰기 연산이 없는 읽기 전용 트랜잭션이므로 다른 트랜잭션에 영향을 주지 않으며 무조건 검증을 통과하고 Commit 됩니다.
- T_2의 검증: T_2는 쓰기 단계(Write Phase) 직전인 스케줄 후반부에 검증을 받습니다.
- 검증 규칙에 따라, T_2의 읽기 단계 도중 Commit 된 T_1의 WriteSet을 확인합니다.
- T_1의 WriteSet은 공집합(∮)이므로, T_2의 ReadSet과 충돌(WriteSet(T_1) ∩ ReadSet(T_2) ≠ ∮ )이 전혀 발생하지 않습니다.
- 따라서 T_2 역시 성공적으로 검증을 통과하여 최종 쓰기를 수행합니다.
- 결론: 검증 단계에서 어떠한 충돌이나 롤백 조건도 발생하지 않으므로 OCC로도 생성 가능한 스케줄입니다.

💡 x = x-50과 write(x)가 무엇이 다른가?
두 연산은 데이터베이스 트랜잭션 이론에서 명확히 구분되는 완전히 다른 연산입니다.
데이터베이스 트랜잭션의 데이터 처리 과정은 크게 '메모리(지역 작업 공간)에서의 연산'과 '실제 데이터베이스(디스크/버퍼)로의 기록'으로 나뉩니다.
- x = x - 50 (지역 연산, Local Computation)
- 이것은 트랜잭션이 자신의 지역 작업 공간(Local Workspace, 메모리)으로 읽어온(read(x)) 데이터 x의 값을 단순히 가공(연산)하는 과정입니다.
- 이 시점에서는 해당 트랜잭션 내부에서만 변경된 값을 알고 있을 뿐, 실제 데이터베이스에는 어떠한 물리적인 변화도 일어나지 않습니다. 따라서 다른 트랜잭션은 아직 이 변경 사항을 볼 수 없습니다.
- write(x) (데이터베이스 쓰기, Database Write)
- 지역 작업 공간에서 계산이 완료된 최종 결과값을 실제 데이터베이스(또는 시스템의 공유 버퍼)에 기록하는 핵심 연산입니다.
- 이 연산이 수행되어야 비로소 변경된 값이 시스템에 반영되며, 병행 제어에서 말하는 '쓰기 충돌(Write Conflict)'이나 '락(Lock)' 검증의 실질적인 기준점이 됩니다.
💡x = x-50과 write(x)를 왜 굳이 분리해서 표기했는가?
문제의 스케줄 표를 보면 T_2에서 x = x - 50과 같은 내부 연산은 중간에 수행하면서, 실제 데이터베이스에 반영하는 write(x)와 write(y)는 트랜잭션의 맨 마지막으로 미루어 배치했습니다. 이것은 바로 앞서 설명해 드린 낙관적 병행제어(OCC)의 핵심 동작 방식을 시각적으로 보여주기 위한 출제자의 의도입니다.
- OCC의 읽기 단계(Read Phase): 실제 DB에 락을 걸지 않고, 데이터를 자신의 메모리로 가져와서 x = x - 50 연산을 자유롭게 수행합니다.
- OCC의 쓰기 단계(Write Phase): 모든 연산이 끝나고 직렬성 검증(Validation)을 무사히 통과한 직후에야, 맨 마지막에 몰아서 write(x)와 write(y)를 실행하여 실제 DB에 반영합니다.
즉, 이 두 연산을 명확히 구분해야만 OCC 기법의 특징인 '지연된 쓰기(Deferred Write)' 구조를 간파하고 올바른 정답을 도출할 수 있습니다.
💡write(x)와 commit은 다른 것인가?
데이터베이스 트랜잭션 이론에서 두 가지의 차이는 다음과 같이 정의됩니다.
1. write(x): 데이터베이스에 변경 사항을 기록하는 '개별 연산'
- 상태의 미확정: 지역 메모리에서 계산을 마친 값을 데이터베이스의 공유 버퍼(Buffer) 또는 임시 로그에 기록하는 행위입니다. write(x)가 수행되었다고 해서 해당 변경 사항이 시스템에 최종적으로 확정된 것은 아닙니다.
- 취소 가능성(Rollback): T_2가 write(x)를 수행한 직후, 이어지는 write(y) 과정에서 오류가 발생하거나 시스템 장애가 일어나면, 앞서 수행한 write(x)의 결과는 모두 취소(Rollback)되어 이전 상태로 되돌아갑니다.
- 즉, write(x)는 트랜잭션이 목적을 달성하기 위해 수행하는 과정 중 하나일 뿐입니다.
2. Commit: 트랜잭션의 성공을 선언하는 '최종 확정'
- 영속성(Durability) 보장: 트랜잭션 내에 포함된 모든 연산(예: read(x), x=x-50, read(y), y=y+50, write(x), write(y))이 아무런 논리적/물리적 오류 없이 완료되었음을 선언합니다. Commit이 완료된 시점부터 데이터베이스는 어떠한 장애가 발생하더라도 이 변경 사항을 영구적으로 보존할 책임을 집니다(ACID 특성 중 영속성).
- 가시성(Visibility) 확보: 일반적으로 트랜잭션이 Commit을 완료해야만 변경된 x와 y의 값을 다른 트랜잭션들이 안정적으로 읽어갈 수 있습니다.
요약 비교 및 스케줄 상의 의미
| 구분 | write(x) | Commit |
| 의미 | 변경된 데이터를 DB 버퍼에 반영하는 연산 | 전체 트랜잭션의 성공적 종료를 알리는 제어어 |
| 번복 가능 여부 | 장애 및 충돌 발생 시 취소(Rollback) 가능 | 완료 이후에는 절대 취소 불가 |
| 비유 | 서류에 수정할 내용을 펜으로 적어두는 행위 | 작성된 서류에 최종 결재 도장을 찍는 행위 |
제시된 문제의 스케줄에서 T_2는 가장 마지막에 write(x)와 write(y)를 차례대로 수행합니다. 이 두 번의 쓰기 연산이 모두 정상적으로 버퍼에 기록되고 나면, 표에는 생략되어 있지만 내부적으로 Commit 연산이 호출되어야 비로소 T_2 트랜잭션이 완전히 종료되는 것입니다.
특히 낙관적 병행 제어(OCC) 환경에서는 검증(Validation) 단계를 통과한 직후에만 write(x), write(y)를 수행할 자격이 주어지며, 이 쓰기 단계가 끝나면 즉시 Commit 처리됩니다.
💡display(x+y)는 무엇을 의미하는가?
제시된 트랜잭션 스케줄에서 display(x+y)는 데이터베이스 내부의 상태를 변경하는 연산이 아니라, 트랜잭션이 읽어온 데이터를 사용자 화면이나 애플리케이션에 출력(Print)해 주는 '응용 프로그램 수준의 단순 처리'를 의미합니다.
1. 데이터베이스 관점에서의 display
- 물리적 DB 접근 없음: write(x)가 데이터베이스 버퍼나 디스크에 값을 기록하는 핵심 연산인 반면, display(x+y)는 메모리(로컬 작업 공간)에 이미 읽어둔 x와 y의 값을 더해서 보여주기만 할 뿐 데이터베이스에 어떠한 물리적 변화도 일으키지 않습니다.
- 읽기 전용(Read-only)의 증명: 스케줄 상 T_1은 read(x), read(y)를 수행한 뒤 display(x+y)를 끝으로 더 이상의 연산이 없습니다. 즉, T_1이 데이터를 수정하지 않는 '읽기 전용 트랜잭션'임을 시각적으로 확정해 주기 위해 이 기호를 삽입한 것입니다.
2. 병행제어(2PLP, OCC) 판별 시 display의 역할
이 단순한 출력 연산 하나가 앞서 정답을 도출하는 데 결정적인 단서로 작용합니다.
- 2단계 로킹 규약(2PLP) 적용 시:
- T_1은 데이터를 읽고 출력만 하므로 x와 y에 대해 공유 잠금(S-lock)만 획득합니다.
- display(x+y)가 완료되면 T_1의 모든 작업이 끝난 것이므로, 쥐고 있던 S-lock을 즉시 해제(Unlock)합니다. 덕분에 대기하고 있던 T_2가 독점 잠금(X-lock)을 획득하고 성공적으로 write를 수행할 수 있게 됩니다. (교착상태 회피)
- 낙관적 병행 제어(OCC) 적용 시:
- T_1은 display 연산만 수행하고 쓰기(Write)를 하지 않았으므로, 변경 사항을 모아두는 집합인 WriteSet이 공집합(∮)이 됩니다.
- 따라서 T_2가 마지막에 검증(Validation)을 받을 때, 먼저 끝난 T_1과 충돌할 위험이 원천적으로 차단됨을 논리적으로 보장해 줍니다.
💡 핵심 요약
read는 DB에서 데이터를 가져오는 것, x=x-50은 가져온 데이터를 내 책상(메모리)에서 조작하는 것, display는 그 결과를 눈으로 확인하는 것, 그리고 write는 최종 결과를 다시 DB라는 공용 문서고에 덮어쓰는 행위로 엄격하게 구분하셔야 합니다.
'DB' 카테고리의 다른 글
| 비연쇄적 스케줄(Cascadeless Schedule) (0) | 2026.02.15 |
|---|---|
| 2PLP (2-Phase Locking Protocol) (0) | 2026.02.15 |
| 데이터베이스 동시성제어 (0) | 2026.02.15 |
| [병행제어] 타임스탬프 순서 규약 (Timestamp Ordering Protocol) (0) | 2026.02.15 |
| 인덱스 분포도 (1) | 2026.02.14 |