동시성 제어
개요
동시성 제어(concurrency control)는 겹쳐 진행되는 작업이 공유 상태를 읽고 바꿀 때, 허용할 순서와 결과를 정하는 방법이다. 작업이 실제로 여러 CPU에서 같은 순간에 실행되는 병렬성뿐 아니라, 한 작업이 입출력을 기다리는 동안 다른 작업이 진행되는 상황도 포함한다.
한 편집기가 읽은 글을 다른 편집기가 먼저 고쳤다고 하자. 앞선 편집기의 저장을 뒤의 편집기가 옛 내용으로 덮어쓰면 수정이 사라진다. 데이터베이스의 재고나 예약에서도 비슷한 일이 생긴다. 각 작업만 따로 읽으면 올바른 절차라도 서로 끼어드는 순서에 따라 전체 규칙이 깨질 수 있다.
이를 다루려면 무엇을 한 번의 작업으로 보고, 어떤 데이터를 공유하며, 어느 결과를 올바른 것으로 받아들일지 정해야 한다. 한 변수의 원자적 갱신, 한 행의 버전 검사, 여러 행을 읽는 트랜잭션의 직렬가능성은 보호하는 범위가 다르다.
이 문서는 데이터베이스 동시성 제어를 중심으로 잠금·낙관적 검증·다중 버전의 계보와 관계를 설명한다. 운영체제의 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 예제의 목적은 “버전이 같을 때만 저장한다”는 판단을 읽기와 쓰기 사이에 흩어놓지 않고, 저장 문장에 포함하는 것이다.
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;
첫 갱신은 버전 1의 A 본문을 반환하고, 두 번째 갱신은 조건에 맞는 행이 없어 0행을 반환한다. 최종 본문은 A의 수정이다. 호출자는 0행을 변경 성공과 구별해 알려야 한다. 이 코드는 실행 순서를 고정해 오래된 버전의 거절을 보여준다.
HTTP의 ETag와 If-Match도 읽은 표현과 현재 표현이 일치할 때만 변경한다는 조건을 전달하는 수단이다. 이 조건을 실제 저장 경계까지 연결해야 중간에 다른 쓰기가 끼어드는 문제를 막을 수 있다.5
6. 보호할 대상이 행보다 클 때
행 하나의 갱신 유실을 막아도 “최소 한 명의 당직자가 남아야 한다”는 여러 행의 규칙은 별도로 깨질 수 있다. 두 작업이 서로 다른 당직자 행만 변경하면 각 행의 버전 검사가 모두 성공하면서 전체 인원이 0명이 될 수 있기 때문이다.
존재하지 않는 데이터도 주의할 지점이다. 예약이 없는 자원을 SELECT ... FOR UPDATE로 조회하면 PostgreSQL에서는 잠글 예약 행을 찾지 못한다. 자원당 예약 하나라는 규칙은 고유 제약으로 표현하거나, 자원 자체의 대표 행 등 실제로 함께 보호할 대상을 정해야 한다.2
이처럼 충돌의 범위는 업무 규칙에서 나와야 한다. 개별 객체·행·조건으로 선택한 집합·여러 서비스의 작업을 같은 잠금 한 단어로 다루면 보장 범위가 흐려진다.
7. 멱등성·트랜잭션과의 관계
트랜잭션은 확정과 복구의 단위를 제공하고, 동시성 제어는 겹친 작업의 접근과 순서를 조정한다. 멱등성은 같은 요청을 반복했을 때 추가 효과가 생기는지 묻는다. 오래된 버전의 저장 요청을 거절하는 것과 같은 결제 요청을 다시 처리하지 않는 것은 다른 계약이다.
어떤 방법을 선택하든 충돌 이후에 기다릴지, 실패를 돌려줄지, 새 상태를 읽고 다시 판단할지를 정해야 한다. 이 결정은 보호 범위와 함께 동시성 제어의 실제 사용 방식을 이룬다.
각주
- 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. 원문. ↩
- PostgreSQL 18, §13.3 “Explicit Locking”. 공식 문서. ↩
- H. T. Kung·John T. Robinson, 「On Optimistic Methods for Concurrency Control」, ACM TODS 6(2), 1981, pp.213–226. 저자 공개본. ↩
- PostgreSQL 18, §13.2 “Transaction Isolation”. 공식 문서. ↩
- RFC 9110, §13.1.1 “If-Match”. 표준. ↩