−한 편집기가 읽은 글을 다른 편집기가 먼저 고쳤다고 하자. 앞선 편집기의 저장을 뒤의 편집기가 옛 내용으로 덮어쓰면 수정이 사라진다. 데이터베이스의 재고나 예약에서도 비슷한 일이 생긴다. 각 작업만 따로 읽으면 올바른 절차라도 서로 끼어드는 순서에 따라 전체 규칙이 깨질 수 있다.
이를 다루려면 무엇을 한 번의 작업으로 보고, 어떤 데이터를 공유하며, 어느 결과를 올바른 것으로 받아들일지 정해야 한다. 한 변수의 원자적 갱신, 한 행의 버전 검사, 여러 행을 읽는 트랜잭션의 직렬가능성은 보호하는 범위가 다르다.
이 문서는 데이터베이스 동시성 제어를 중심으로 잠금·낙관적 검증·다중 버전의 계보와 관계를 설명한다. 운영체제의 mutex나 언어의 원자 연산도 공유 상태를 조정하지만, DB의 트랜잭션과 동일한 경계와 실패 모델을 갖는 것은 아니다.
1. 실행 순서를 대상으로 삼다
동시에 시작한 두 작업도 읽기와 쓰기는 어떤 순서로 섞여 실행된다. 이 순서를 스케줄(schedule)이라고 부른다. 동시성 제어 이론은 가능한 스케줄 중 어느 것을 허용해야 개별 작업이 지키던 데이터 규칙을 전체에서도 유지할 수 있는지 묻는다.
Eswaran·Gray·Lorie·Traiger의 1976년 논문은 일관성과 트랜잭션, 잠금 프로토콜을 연결하고 조건식으로 지정한 데이터 집합을 보호하는 문제를 다뤘다. 데이터가 물리적으로 놓인 행만 보호할 것인지, “이 날짜에 예약된 모든 방”처럼 논리적 조건에 해당하는 집합을 보호할 것인지가 중요한 차이였다.[각주 1]
직렬가능성은 함께 실행해 성공한 트랜잭션들의 효과가 어떤 직렬 실행과 같도록 요구한다. 이를 얻는 구현은 하나가 아니다. 충돌을 먼저 막거나, 나중에 검증하거나, 여러 버전을 읽게 하면서 의존성을 검사할 수 있다.
2. 잠금과 2단계 잠금
잠금(lock)은 충돌하는 접근을 조정하는 권한이다. 읽기와 쓰기의 종류, 보호 단위에 따라 서로 함께 허용되는 잠금과 기다려야 하는 잠금이 달라진다. DB에서는 행·테이블·키 범위 등 여러 단위가 사용된다.
2단계 잠금(two-phase locking, 2PL)은 잠금을 획득하는 단계와 해제하는 단계를 나누는 규칙이다. 어떤 잠금을 해제한 뒤에는 새 잠금을 얻지 않는다. 적절한 잠금 모드와 보호 범위를 전제로 이 규칙은 충돌 직렬가능성을 얻는 데 쓰인다. strict 2PL은 쓰기 잠금을 트랜잭션 종료까지 유지해 미확정 쓰기와 복구의 관계도 다룬다.[각주 1]
여기서 두 단계는 분산 커밋의 2PC와 다른 구분이다. 2PL은 공유 데이터 접근의 순서를, 2PC는 여러 참여자의 커밋 결정을 조정한다.
서로 다른 작업이 상대가 가진 잠금을 기다리면 교착상태(deadlock)가 생길 수 있다. DB는 이를 감지해 한 작업을 중단하거나 타임아웃을 적용할 수 있다. PostgreSQL의 행 잠금도 충돌하는 쓰기를 기다리게 하지만 일반적인 스냅샷 읽기까지 일괄 중단시키지는 않는다.[각주 2]
3. 1981년 낙관적 검증
Kung과 Robinson은 1981년 「On Optimistic Methods for Concurrency Control」에서 읽기·검증·쓰기 단계를 구분하는 방법을 제시했다. 먼저 작업을 진행하고, 읽고 쓴 데이터의 관계를 검사해 허용할 수 있는 실행인지 결정하는 방식이다.[각주 3]
이 접근에서 낙관적이라는 말은 작업이 언제나 성공한다는 뜻이 아니라, 충돌을 미리 모두 막는 대신 검증 시점에 찾아내고 필요하면 작업을 취소한다는 뜻이다. 충돌이 드물면 기다림을 줄일 여지가 있고, 같은 데이터에 충돌이 집중되면 버려지는 작업이 늘어난다.
웹 편집기의 버전 검사는 이 생각을 좁은 범위에서 볼 수 있는 사례다. 읽어둔 글의 버전과 현재 버전이 같을 때만 저장한다. 논문의 읽기·쓰기 집합 검증 전체와 한 행의 버전 조건은 구분해서 이해해야 한다.
4. 여러 버전을 읽는 방법
MVCC는 쓰는 작업이 새 버전을 만들고, 읽는 작업이 자신에게 보이는 버전을 고르게 하는 방식이다. 이전 값을 필요한 독자에게 남겨둘 수 있어 읽기와 쓰기 사이의 대기를 줄이는 데 쓰인다.
그러나 버전이 여러 개라는 사실만으로 어떤 격리를 얻는지가 정해지지는 않는다. 문장마다 읽을 스냅샷을 바꾸는지, 트랜잭션 동안 유지하는지, 읽기와 쓰기의 의존성 순환을 검사하는지가 함께 작동한다. PostgreSQL도 MVCC를 사용하면서 Read Committed·Repeatable Read·Serializable에 다른 규칙을 둔다.[각주 4]
잠금·버전·검증은 배타적인 제품 분류표가 아니다. MVCC DB도 쓰기 충돌에 잠금을 사용하고, 애플리케이션이 낙관적 버전 검사를 해도 저장 문장 안에서는 DB의 동시성 규칙이 적용된다.
5. 한 행의 변경을 조건으로 표현하기
다음 PostgreSQL 예제의 목적은 “버전이 같을 때만 저장한다”는 판단을 읽기와 쓰기 사이에 흩어놓지 않고, 저장 문장에 포함하는 것이다.
+편집자 A와 B가 같은 글을 읽은 뒤 A가 먼저 고쳐 저장하고, 이후 B가 처음 읽은 내용을 바탕으로 저장하면 A의 수정이 사라진다. 데이터베이스의 재고나 예약에서도 같은 일이 생긴다. 작업 하나하나는 올바른 절차를 따라도, 서로 끼어드는 순서에 따라 전체 규칙이 깨질 수 있다.
이를 다루려면 무엇을 한 번의 작업으로 보고, 어떤 데이터를 공유하며, 어느 결과를 올바른 것으로 받아들일지 정해야 한다. 한 변수의 원자적 갱신, 한 행의 버전 검사, 여러 행을 읽는 트랜잭션의 직렬가능성은 보호하는 범위가 다르다.
이 문서는 데이터베이스 동시성 제어를 중심으로, 잠금·낙관적 검증·다중 버전이 이어져 온 계보와 서로의 관계를 다룬다. 운영체제의 mutex나 언어의 원자 연산도 공유 상태를 조정하지만, DB 트랜잭션과 같은 경계와 실패 모델을 갖지는 않는다.
연혁
1976년: Eswaran·Gray·Lorie·Traiger가 「The Notions of Consistency and Predicate Locks in a Database System」에서 트랜잭션·일관성·스케줄을 정의하고, 이 개념들과 2단계 잠금의 관계를 분석했다. 물리적인 레코드와 조건으로 선택한 데이터 집합의 차이도 같은 논문에서 다뤘다.[각주 1]
1981년: Kung과 Robinson이 「On Optimistic Methods for Concurrency Control」에서 작업을 먼저 진행하고 충돌은 나중에 검증하는 낙관적 방법을 제시했다.[각주 2]
1983년: Philip A. Bernstein과 Nathan Goodman이 「Multiversion Concurrency Control—Theory and Algorithms」에서 다중 버전 스케줄의 정확성을 설명하고 여러 알고리즘을 다뤘다. 여러 버전을 쓰는 실행을 하나의 이론으로 정리한 기록이며, MVCC의 최초 발명일을 확정하는 자료는 아니다.[각주 3]
1995년: Berenson 등이 「A Critique of ANSI SQL Isolation Levels」에서 SQL 격리 수준의 정의와 스냅샷 격리를 분석하고, 스냅샷 격리에서 생기는 쓰기 왜곡을 사례로 들었다.[각주 4]
1997년 1월: HTTP/1.1의 RFC 2068이 If-Match를 정의하고, 읽은 뒤 바뀐 자원을 잘못 수정하지 않도록 요청에 엔터티 태그를 넣는 용도를 설명했다.[각주 5]
2022년: RFC 9110이 If-Match 조건의 의미를 규정했다.[각주 6]
잠금 규칙과 보호 대상 (1976)
Eswaran·Gray·Lorie·Traiger의 1976년 논문이 다룬 다중 사용자 DB에서는 읽기와 쓰기가 섞이는 순서부터 분석해야 한다. 순서가 달라져도 무결성을 지키려면 “여기에 잠금을 건다”는 결정만으로는 모자라고, 잠금을 얻고 놓는 규칙이 함께 있어야 한다. 2단계 잠금에서는 잠금을 늘리는 단계에서 줄이는 단계로 넘어간 뒤 새 잠금을 얻지 않는다.[각주 1]
Eswaran 등이 구분한 물리적인 레코드와 조건으로 선택한 데이터 집합은 예약을 예로 들면 구별하기 쉽다. “이미 존재하는 예약 행”과 “이 날짜에 생길 수 있는 예약”은 서로 다른 보호 대상이다. 동시성 제어가 지킬 범위를 논리적 규칙에서 읽어야 한다는 문제는 이 시기에 분명하게 드러났다.
낙관적 검증 (1981)
Kung과 Robinson이 1981년에 제시한 낙관적 방법에서 작업은 읽기·검증·쓰기 단계를 거치고, 어떤 실행을 허용할지는 작업들의 읽기·쓰기 집합 사이의 관계로 정한다. 충돌이 드물면 미리 기다리는 비용을 줄일 여지가 있지만, 검증에 실패한 작업은 버려진다. 낙관적이라는 이름은 성공을 가정하고 검증 시점까지 진행하는 운영 방식에서 나왔다.[각주 2]
웹 편집기의 버전 검사는 이 생각을 한 행에 적용한 예다. 저장하려는 행의 버전이 읽어둔 값과 같을 때만 변경하며, 논문의 전체 트랜잭션 검증 알고리즘보다 좁은 범위를 보호한다.
다중 버전의 이론화 (1983)
Bernstein과 Goodman의 1983년 논문은 다중 버전 스케줄의 정확성을 설명하고 새 알고리즘과 기존 알고리즘을 같은 이론으로 분석했다. 읽는 작업에 필요한 이전 버전을 남겨 두면 쓰기와 읽기 사이의 대기 관계가 달라진다. 어느 버전을 읽게 하고 어떤 실행을 거절할지는 별도의 규칙이므로, 버전을 보관하는 일과 직렬가능성의 조건은 나눠서 봐야 한다.[각주 3]
오늘날의 제품은 여러 기법을 함께 쓴다. MVCC DB가 쓰기 충돌에는 잠금을 쓰기도 하고, 애플리케이션의 낙관적 검사가 DB 내부의 쓰기 잠금과 함께 작동할 수도 있다.
스냅샷 격리와 쓰기 왜곡 (1995)
Berenson 등이 1995년에 분석한 쓰기 왜곡은 스냅샷 격리와 직렬가능성이 갈라지는 지점을 보여준다. 두 작업이 같은 스냅샷을 읽더라도 서로 다른 행을 바꾸면 전체 규칙이 깨질 수 있다.[각주 4]
한 행의 갱신 유실을 막는 일과 여러 행에 걸친 규칙을 지키는 일은 서로 다르다. 모든 행이 자기 버전 검사를 통과하더라도, 함께 읽은 조건이 더는 유지되지 않으면 업무의 결과가 달라질 수 있다. 그래서 잠금·검증·버전이라는 구현 이름보다, 어떤 작업 집합의 결과를 어떤 직렬 실행과 같게 만들지가 더 근본적인 질문이다.
HTTP 조건부 요청 (1997–2022)
1997년 RFC 2068은 HTTP/1.1의 조건부 요청을 정의했고, 2022년 RFC 9110은 그 의미를 다시 규정했다. DB 내부의 동시성 이론과 HTTP 조건부 요청은 서로 다른 계층에서 오래된 관찰에 근거한 쓰기를 막는다. 요청에 If-Match 조건이 붙어 오면 서버는 그 조건을 실제 저장까지 연결해야 한다.[각주 6]
사람이 편집하는 시간 내내 DB 잠금을 잡아 두는 대신 저장 시점의 버전을 확인하는 UI도 이 관점으로 읽을 수 있다. 충돌이 난 뒤 기다릴지, 다시 조회할지, 사용자에게 병합을 요청할지는 애플리케이션이 결정한다.
PostgreSQL 행 잠금과 남는 보호 범위
PostgreSQL의 SELECT ... FOR UPDATE는 선택한 행에 대한 충돌하는 갱신을 조정하며, 이 잠금은 트랜잭션이 끝날 때까지 유지된다. 이 행 잠금은 일반 SELECT를 막지 않고, 같은 행을 갱신·삭제·잠금하려는 작업을 기다리게 한다. 서로의 잠금을 기다리면 교착상태가 생길 수도 있다.[각주 7]
예약이 없는 자원을 조회해 0행을 얻으면 잠글 예약 행도 없다. 이때는 고유 제약, 대표 자원 행의 잠금, 적합한 격리 규칙처럼 업무 조건에 맞는 보호를 골라야 한다. 현재 PostgreSQL도 MVCC 위에서 서로 다른 격리 수준을 제공한다.[각주 8][각주 7][각주 9]
잠금 규칙, 낙관적 검증, 다중 버전 이론, HTTP의 사전조건은 모두 공유 상태의 관찰과 변경을 연결하는 문제를 다룬다. 각 기법의 계약을 읽을 때 기준이 되는 것은 그 기법이 무엇을 보호 범위로 삼는지다.
한 행의 버전 조건 예제
아래 PostgreSQL 코드는 “읽은 버전과 같을 때만 저장한다”는 조건을 저장 문장에 넣는다. 순서를 고정한 두 갱신으로 오래된 수정이 거절되는지 확인한다.