동시성 제어
개요
동시성 제어(concurrency control)는 겹쳐 진행되는 작업이 공유 상태를 읽고 바꿀 때, 허용할 순서와 결과를 정하는 방법이다. 작업이 실제로 여러 CPU에서 같은 순간에 실행되는 병렬성뿐 아니라, 한 작업이 입출력을 기다리는 동안 다른 작업이 진행되는 상황도 포함한다.
한 편집기가 읽은 글을 다른 편집기가 먼저 고쳤다고 하자. 앞선 편집기의 저장을 뒤의 편집기가 옛 내용으로 덮어쓰면 수정이 사라진다. 데이터베이스의 재고나 예약에서도 비슷한 일이 생긴다. 각 작업만 따로 읽으면 올바른 절차라도 서로 끼어드는 순서에 따라 전체 규칙이 깨질 수 있다.
이를 다루려면 무엇을 한 번의 작업으로 보고, 어떤 데이터를 공유하며, 어느 결과를 올바른 것으로 받아들일지 정해야 한다. 한 변수의 원자적 갱신, 한 행의 버전 검사, 여러 행을 읽는 트랜잭션의 직렬가능성은 보호하는 범위가 다르다.
이 문서는 데이터베이스 동시성 제어를 중심으로 잠금·낙관적 검증·다중 버전의 계보와 관계를 설명한다. 운영체제의 mutex나 언어의 원자 연산도 공유 상태를 조정하지만, DB의 트랜잭션과 동일한 경계와 실패 모델을 갖는 것은 아니다.
일관성을 스케줄과 잠금으로 설명하다 (1976)
공유 데이터를 여러 사용자가 다루는 DB에서는 작업의 읽기와 쓰기가 섞이는 순서를 분석해야 한다. 순서가 달라져도 무결성을 유지하려면 단순히 “여기에 잠금을 건다”를 넘어 잠금을 얻고 놓는 규칙이 필요하다.
Eswaran·Gray·Lorie·Traiger의 1976년 논문은 트랜잭션·일관성·스케줄을 정의하고 2단계 잠금의 관계를 분석했다. 잠금을 늘리는 단계에서 줄이는 단계로 넘어간 뒤에는 새 잠금을 얻지 않는 규칙이 핵심이다.1
논문은 물리적인 레코드와 조건으로 선택한 데이터 집합의 차이도 다룬다. “이미 존재하는 예약 행”과 “이 날짜에 생길 수 있는 예약”은 같은 보호 대상이 아니다. 동시성 제어의 범위를 논리적 규칙에서 읽어야 한다는 문제가 이 시기에 분명하게 드러났다.
충돌을 뒤에서 검증하는 낙관적 접근 (1981)
Kung과 Robinson의 1981년 「On Optimistic Methods for Concurrency Control」는 먼저 작업을 진행하고 나중에 충돌을 검증하는 방법을 제시했다. 읽기·검증·쓰기 단계와 읽기·쓰기 집합의 관계를 통해 허용할 실행을 결정한다.2
충돌이 드문 경우에는 미리 기다리는 비용을 줄일 여지가 있지만, 검증 실패로 버려지는 작업이 생긴다. 낙관적이라는 이름은 성공을 낙관적으로 가정해 검증 시점까지 진행한다는 운영 방식에 붙은 것이다.
웹 편집기의 버전 검사는 이 생각을 좁은 범위에서 보여준다. 저장하려는 행의 버전이 읽어둔 값과 같을 때만 변경한다. 한 행의 조건부 갱신과 논문의 전체 트랜잭션 검증 알고리즘을 같은 구현으로 부를 수는 없다.
여러 버전을 사용하는 기법의 이론화 (1983)
Philip A. Bernstein과 Nathan Goodman의 1983년 「Multiversion Concurrency Control—Theory and Algorithms」는 다중 버전 스케줄의 정확성을 설명하고 여러 알고리즘을 다룬다. 이는 MVCC의 최초 발명일을 확정하는 자료가 아니라, 여러 버전을 활용하는 실행을 공통된 이론으로 설명한 기록이다.3
읽는 작업에 필요한 이전 버전을 남기면 쓰는 작업과 독자의 대기 관계가 달라진다. 하지만 어느 버전을 읽고 어떤 실행을 거절하는지는 별도의 규칙이다. 버전 보관과 직렬가능성의 조건을 구분하는 관점이 중요해진다.
오늘날의 제품도 여러 기법을 함께 쓴다. MVCC DB가 쓰기 충돌에 잠금을 사용하거나, 애플리케이션의 낙관적 검사가 DB 내부의 쓰기 잠금과 함께 작동할 수 있다.
스냅샷과 직렬가능성의 차이가 드러나다 (1995)
Berenson 등의 1995년 연구는 SQL 격리 수준의 정의와 스냅샷 격리를 분석했다. 두 작업이 같은 스냅샷을 읽어도 서로 다른 행을 바꾸며 전체 규칙을 깨뜨릴 수 있다는 쓰기 왜곡이 중요한 사례다.4
이 구분은 한 행의 갱신 유실을 막는 일과 여러 행의 규칙을 지키는 일이 다름을 보여준다. 모든 행이 자기 버전 검사를 통과하더라도, 함께 읽은 조건이 더 이상 유지되지 않으면 업무의 결과가 달라질 수 있다.
따라서 잠금·검증·버전이라는 구현 이름보다 어떤 작업 집합의 결과를 어떤 직렬 실행과 같게 만들 것인지가 더 근본적인 질문으로 남는다.
읽은 버전을 HTTP 변경 조건으로 보내다 (1997~2022)
HTTP/1.1의 1997년 RFC 2068은 If-Match를 정의하고, 읽은 뒤 바뀐 자원을 잘못 수정하지 않도록 요청에 엔터티 태그를 넣는 용도를 설명했다. 2022년 RFC 9110도 이 조건의 의미를 규정한다.56
DB 내부의 동시성 이론과 HTTP 조건부 요청은 같은 프로토콜은 아니지만, 오래된 관찰에 근거한 쓰기를 막는 문제를 서로 다른 경계에서 다룬다. 서버는 요청의 조건을 실제 저장까지 연결해야 한다.
사람이 편집하는 시간 전체에 DB 잠금을 잡는 대신 저장 시점의 버전을 확인하는 UI도 이 관점으로 읽을 수 있다. 충돌 이후 기다릴지, 재조회할지, 사용자에게 병합을 요청할지는 애플리케이션의 결정이다.
현대 DB에서 남는 보호 범위의 문제
PostgreSQL의 SELECT ... FOR UPDATE는 선택한 행의 충돌하는 갱신을 조정하며 잠금을 트랜잭션 종료까지 유지한다. 일반 스냅샷 읽기까지 전부 막는 동작은 아니다. 서로 잠금을 기다리면 교착상태가 발생할 수도 있다.7
예약이 없는 자원을 조회해 0행을 얻으면 잠글 예약 행도 없다. 고유 제약, 대표 자원 행의 잠금, 적합한 격리 규칙처럼 업무 조건에 맞는 보호가 필요하다. 현재 PostgreSQL도 MVCC 위에서 서로 다른 격리 수준을 제공한다.78
1976년의 잠금 규칙, 1981년의 검증, 1983년의 다중 버전 이론, HTTP의 사전조건은 모두 공유 상태의 관찰과 변경을 연결하는 문제를 다룬다. 보호 범위가 무엇인지가 각 기법의 계약을 읽는 기준이다.
한 행의 버전 조건을 보여주는 작은 예제
다음 PostgreSQL 코드는 “읽은 버전과 같을 때만 저장한다”는 조건을 저장 문장에 넣는다. 순서를 고정한 두 갱신으로 오래된 수정의 거절을 확인한다.
CREATE TEMP TABLE posts (
id integer PRIMARY KEY,
body text NOT NULL,
version bigint NOT NULL
);
INSERT INTO posts VALUES (1, 'original', 0);
UPDATE posts
SET body = 'edited by A', version = version + 1
WHERE id = 1 AND version = 0
RETURNING body, version;
UPDATE posts
SET body = 'edited by B', version = version + 1
WHERE id = 1 AND version = 0
RETURNING body, version;
SELECT body, version FROM posts WHERE id = 1;
첫 갱신은 edited by A, 1을 반환하고 두 번째는 0행을 반환한다. 최종 본문은 A의 수정이다. 호출자는 0행을 변경 성공과 구별해야 한다. 이 예제의 검사는 한 행의 버전에 한정된다.
각주
- K. P. Eswaran·J. N. Gray·R. A. Lorie·I. L. Traiger, 「The Notions of Consistency and Predicate Locks in a Database System」, CACM 19(11), 1976, pp.624–633. 원문. ↩
- H. T. Kung·John T. Robinson, 「On Optimistic Methods for Concurrency Control」, ACM TODS 6(2), 1981, pp.213–226. 저자 공개본. ↩
- Philip A. Bernstein·Nathan Goodman, 「Multiversion Concurrency Control—Theory and Algorithms」, ACM TODS 8(4), 1983, pp.465–483. 원문 사본. ↩
- Hal Berenson 외, 「A Critique of ANSI SQL Isolation Levels」, SIGMOD, 1995, 스냅샷 격리 및 A5B. 원문. ↩
- RFC 2068, 1997-01, §14.25 “If-Match”. 역사적 HTTP/1.1 정의. 원문. ↩
- RFC 9110, §13.1.1 “If-Match”. 표준. ↩
- PostgreSQL 18, §13.3 “Explicit Locking”. 공식 문서. ↩
- PostgreSQL 18, §13.2 “Transaction Isolation”. 공식 문서. ↩