사전 리비전
개념

리비전

gabury1고친 사람 github-actions[bot]

리비전은 계속 바뀌는 데이터의 한 시점 모습에 붙인 이름입니다. 문서를 고치면 「3번」이던 것이 「4번」이 되듯, 내용이 바뀔 때마다 새 리비전이 하나 생깁니다. 이름이 있으니 예전 모습을 다시 꺼내거나 두 시점을 견줄 수 있습니다. 누군가 그사이 데이터를 바꿨는지 알아채는 데도 씁니다.

쉽고 빠른 이해

리비전은 데이터가 바뀔 때마다 그 순간의 모습에 붙이는 이름표입니다. 문서를 고치면 「3번」이던 것이 「4번」이 되는 식입니다.

이름표가 없으면 「어제 그 내용」을 가리킬 방법이 없습니다. 둘이 동시에 같은 데이터를 고칠 때 한쪽이 다른 쪽을 몰래 덮어써도 알아채지 못합니다.

어떻게 도나:

  1. 데이터를 바꾸면 옛 모습을 지우지 않고 새 모습을 하나 더 남깁니다
  2. 새 모습에 새 이름표를 붙입니다. 순서대로 번호를 매기거나 내용으로 이름을 만듭니다
  3. 새 모습은 자기가 어느 모습에서 나왔는지도 함께 기억합니다

옛 모습을 남기는 만큼 저장 공간을 먹습니다. 오래된 것은 언젠가 정리해야 합니다. 옛 모습을 다시 볼 일도, 여럿이 동시에 고칠 일도 없는 데이터라면 리비전을 두지 않아도 됩니다.

상세

이 절은 비유 하나로 리비전을 먼저 떠올리고, 리비전이 무엇을 가리키는지 봅니다. 그다음 리비전에 이름을 붙이는 두 방식과, 그 이름이 어디까지를 덮는지를 봅니다. 뒤에서는 리비전이 백엔드에서 실제로 쓰이는 두 가지 일을 짚습니다. 갈라진 이력을 알아채는 일과 덮어쓰기를 막는 일입니다.

개정판 번호로 떠올리기

회사 규정집 표지에 「3차 개정판」이라고 적혀 있다고 해 봅시다. 조항 하나를 고치면 표지가 「4차 개정판」으로 바뀝니다. 「3차 개정판 12쪽」이라고 말하면 누구나 같은 쪽을 펼칩니다.

리비전이 가리키는 것

리비전(revision)은 바뀐 부분이 아니라 바뀐 뒤의 전체 모습을 가리킵니다. 「5번 리비전」이라고 하면 그 시점의 내용 전부가 하나로 정해집니다. 바뀐 줄만 모은 것은 따로 diff(차이)라고 부릅니다.

두 리비전이 있으면 둘 사이의 차이를 계산할 수 있습니다. 반대로 첫 모습과 차이들을 차례로 쌓으면 어느 리비전이든 다시 만들 수 있습니다. 그래서 시스템에 따라 전체를 매번 남기기도 합니다. 차이만 남기는 시스템도 있습니다. 어느 쪽이든 밖에서 보이는 리비전은 전체 모습입니다.

일상에서는 버전이라는 말과 거의 같은 뜻으로 씁니다. 이 문서에서는 한 시점의 모습에 붙은 이름을 끝까지 리비전이라고 부릅니다.

버전관리 도구는 파일의 리비전을 쌓아 가는 도구입니다. 이 도구에서 새 리비전을 만드는 동작을 커밋이라고 부릅니다.

이름을 붙이는 두 방식

리비전 이름을 만드는 방법은 크게 둘입니다. 차례대로 번호를 매기거나, 내용에서 이름을 계산합니다.

내용에서 이름을 계산할 때는 해시 함수를 씁니다. 해시 함수는 아무 길이의 입력을 받아 짧은 고정 길이 값을 내는 함수입니다. 같은 입력에는 늘 같은 값을 냅니다. 이 값을 보통 16진수 문자열로 적습니다.

순번 내용 해시
이름 모양 1, 2, 3 처럼 늘어나는 정수 긴 16진수 문자열
순서 이름만 보고 앞뒤를 안다 이름만으로는 모른다
누가 매기나 번호를 세는 곳이 한 곳이어야 한다 어디서 만들어도 같은 내용이면 같은 이름

순번은 읽기 쉽습니다. 「7번이 5번보다 새것」이 바로 보입니다. 다만 다음 번호를 정하는 곳이 하나여야 합니다. 서로 떨어진 두 컴퓨터가 동시에 「다음은 8번」이라고 정하면 번호가 겹칩니다.

내용 해시는 이 문제를 피합니다. 리비전의 내용을 해시 함수에 넣어 이름을 만들면 누구와 상의하지 않아도 이름이 정해집니다. 그래서 같은 데이터를 여러 곳에 나눠 두는 시스템이 이 방식을 자주 고릅니다. 이렇게 나눠 두는 일을 복제라고 합니다.

내용 해시에도 약점이 있습니다. 이름만 보고는 어느 쪽이 먼저인지 모릅니다.

두 방식을 섞기도 합니다. CouchDB 의 문서 리비전은 3- 처럼 앞에 순번을 두고 뒤에 해시를 붙입니다. 두 곳이 동시에 3번을 만들어 번호가 겹쳐도 뒤의 해시가 다르므로 둘을 가를 수 있습니다. 순번은 대략의 순서를 보여 줍니다. 해시는 같은 번호를 단 서로 다른 리비전을 가릅니다.

이름이 덮는 범위

리비전 이름이 무엇 하나에 붙는지도 시스템마다 다릅니다. 순번이든 해시든 마찬가지인데, 여기서는 읽기 쉬운 순번으로 봅니다. Subversion 은 파일 여러 개를 담은 묶음, 곧 저장소 전체에 번호 하나를 붙입니다. CouchDB 는 문서마다 번호를 따로 매깁니다.

저장소 전체에 붙이면 번호 하나가 모든 파일의 모습을 한꺼번에 고릅니다. 「리비전 40」이라고만 해도 그때의 파일 전부가 정해집니다. 다만 파일 하나만 보면 리비전 39와 40의 내용이 같을 수 있습니다. 다른 파일이 바뀌어도 번호는 올라가기 때문입니다.

문서마다 매기면 번호가 그 문서가 바뀐 횟수를 따라갑니다. 문서 하나의 이력을 보기는 쉽습니다. 그 대신 「그 시점 전체의 모습」을 가리키려면 문서마다 번호를 하나씩 모아야 합니다.

부모를 기억하는 리비전

새 리비전은 대개 자기가 어느 리비전에서 나왔는지를 적어 둡니다. 이 앞 리비전을 부모라고 부릅니다. 부모를 따라 거슬러 올라가면 데이터가 걸어온 이력이 한 줄로 나옵니다.

두 사람이 같은 부모에서 각자 새 리비전을 만들면 이력이 둘로 갈라집니다. 앞 절에서 본 번호 겹침이 바로 이 상황입니다. 두 갈래가 서로를 모른 채 같은 번호 3을 받습니다. 아래 그림에서는 둘을 3a·3b로 갈라 적었습니다.

flowchart TD
    R1["리비전 1"] --> R2["리비전 2"]
    R2 --> A["리비전 3a · 가 쪽 수정"]
    R2 --> B["리비전 3b · 나 쪽 수정"]
    A --> M["리비전 4 · 둘을 합친 모습"]
    B --> M

그림에서 리비전 2 아래로 두 갈래가 생겼습니다. 둘 다 같은 부모를 가리키므로 시스템은 이력이 갈라졌다는 것을 압니다. 이렇게 갈라진 리비전들을 어떻게 정리할지 정하는 일이 충돌 해소입니다.

정리하는 방법 가운데 하나는 둘을 합친 새 리비전을 만드는 것입니다. 이 일을 병합이라고 부릅니다. 합친 리비전은 부모를 둘 가집니다.

부모 정보가 없으면 갈라짐을 알아챌 수 없습니다. 두 수정 가운데 나중에 저장된 쪽이 앞선 쪽을 그냥 덮어쓰게 됩니다.

덮어쓰기를 막는 조건부 쓰기

리비전은 백엔드에서 동시에 들어온 수정이 서로를 덮어쓰지 않게 막는 데 자주 씁니다. 읽을 때 받은 리비전을 쓸 때 같이 보냅니다. 서버는 지금 리비전이 그것과 같을 때만 쓰기를 받아 줍니다.

쓰면서 보낸 리비전은 곧 새로 만들려는 리비전의 부모입니다. 서버는 「네가 고친 부모가 아직 최신인가」를 확인하는 셈입니다. 앞 절의 갈라짐이 생기기 전에 막는 방법입니다.

sequenceDiagram
    participant 가
    participant 서버
    participant 나
    가->>서버: 읽기
    서버-->>가: 내용 · 리비전 3
    나->>서버: 읽기
    서버-->>나: 내용 · 리비전 3
    가->>서버: 리비전 3 기준으로 쓰기
    서버-->>가: 성공 · 이제 리비전 4
    나->>서버: 리비전 3 기준으로 쓰기
    서버-->>나: 거절 · 지금은 리비전 4

그림에서 가와 나는 같은 리비전 3을 읽었습니다. 가가 먼저 써서 리비전이 4가 됐습니다. 나는 여전히 3을 기준으로 썼으므로 거절됩니다. 나는 다시 읽고 가의 수정을 본 뒤에 다시 시도합니다.

관계형 데이터베이스에서는 테이블에 리비전 칸을 하나 두고 이렇게 씁니다. 조건에 맞는 행이 없으면 바뀐 행 수가 0으로 돌아옵니다.

SQL
UPDATE doc SET body = '새 내용', rev = 4
WHERE id = 1 AND rev = 3;  -- 가: 1행 바뀜
UPDATE doc SET body = '딴 내용', rev = 4
WHERE id = 1 AND rev = 3;  -- 나: 0행 바뀜

두 번째 문장이 0행을 돌려받으면 애플리케이션은 누군가 먼저 고쳤다는 것을 압니다.

덮어쓰기를 막는 흔한 방법은 잠그기입니다. 고치는 동안 다른 사람이 그 데이터를 못 건드리게 막아 두는 것입니다. 조건부 쓰기는 미리 잠그지 않습니다. 충돌이 드물다고 보고 일단 쓰고, 쓰는 순간에만 확인합니다. 그래서 이 방식을 낙관적 동시성 제어라고 부릅니다.

웹에서는 ETag(Entity Tag, 엔티티 태그)가 같은 역할을 합니다. 서버가 응답에 실어 준 리비전 값을 클라이언트가 수정 요청에 다시 실어 보냅니다.

옛 리비전을 남기는 값

리비전을 남기면 옛 모습을 다시 읽을 수 있습니다. 잘못 바꾼 것을 되돌리는 롤백이 여기에 기댑니다.

데이터베이스는 보통 누가 값을 쓰는 동안 그 값을 읽으려는 쪽을 기다리게 합니다. 읽는 도중 값이 바뀌면 앞뒤가 안 맞는 결과를 볼 수 있어서입니다. 옛 리비전을 남기면 읽는 도중 누가 값을 바꿔도 읽던 리비전은 바뀌지 않습니다. 그래서 읽기와 쓰기가 서로 기다리지 않아도 됩니다. 이 방식을 MVCC(Multi-Version Concurrency Control, 다중 버전 동시성 제어)라고 부릅니다. 이 이름의 「버전」은 리비전과 같은 말입니다.

남기는 데 드는 것은 저장 공간입니다. 바뀔 때마다 모습이 하나씩 쌓이므로 그냥 두면 끝없이 늘어납니다. 그래서 대부분의 시스템은 오래된 리비전을 지우는 정리 작업을 따로 둡니다. 보관할 리비전 수에 상한을 두거나, 일정 시점보다 오래된 것을 한꺼번에 걷어 냅니다.

정리한 뒤에는 그 리비전을 다시 꺼낼 수 없습니다. 지워진 리비전을 기준으로 한 조건부 쓰기나 되돌리기는 실패합니다. 얼마나 남길지는 되돌릴 수 있어야 하는 기간과 저장 공간 사이에서 정합니다.

반대로 한 번 쓰고 바뀌지 않는 데이터나, 옛 모습을 다시 볼 일도 동시에 고칠 일도 없는 데이터에는 리비전을 두지 않습니다. 남길 까닭 없이 공간만 먹기 때문입니다.

관련 항목

리비전을 만들고 쌓아 가는 동작

커밋 · 병합 · 롤백 · 브랜치 · 태그 · 스냅숏 · diff

리비전을 기록의 단위로 쓰는 시스템

버전관리 · Git · Subversion · etcd · CouchDB · Kubernetes · MediaWiki

리비전 이름을 만드는 수단

해시 함수 · 해시 · 논리 시계 · 벡터 시계 · 타임스탬프

리비전으로 동시 수정을 다루는 기법

낙관적 동시성 제어 · MVCC · ETag · 조건부 요청 · 충돌 해소 · Last Write Wins · 락

리비전이 쌓인 뒤 치르는 관리 작업

리비전 이력 · 컴팩션 · 툼스톤 · 가비지 컬렉션 · 보존 기간

다른 이름: revision · 리비전 번호 · revision number