7. Transactions

데이터 시스템들에서는 많은 것들이 잘못될 수 있다: 데이터베이스 소프트웨어/하드웨어의 고장, 애플리케이션의 크래시, 네트워크 간섭, 동시성 쓰기, 부분적으로 업데이트된 데이터, 레이스 컨디션. 시스템은 이에 대처할 수 있어야 한다. 수십 년동안 트랜잭션은 이런 문제들을 단순화하기 위한 메커니즘이었다. 이들은 데이터베이스에 접근하는 애플리케이션의 프로그래밍 모델을 단순화하기 위해 만들어졌다. 모든 애플리케이션이 트랜잭션이 필요한 것은 아니다. 트랜잭션은 언제 필요한가/ 이를 위해서는 트랜잭션이 어떤 안전성 보장을 하는지를 알아야 한다. 이것과 연관된 여러 동시성 제어에 대해 알아보자.

The Slippery Concept of a Transaction

거의 모든 관계형 데이터베이스와 일부 비관계형 데이터베이스는 트랜잭션을 지원한다. 2000년대 말부터 비관계형 데이터베이스들의 인기가 늘어났는데 트랜잭션은 이의 주 피해자였다. 대형 스케일 시스템은 좋은 성능과 높은 가용성을 유지하기 위해 트랜잭션을 버리기 시작했다. 하지만 다른 설계 선택처럼 트랜잭션도 장단점이 있다.

The Meaning of ACID

트랜잭션에 의해 보장되는 안전성들은 ACID로, 원자성, 일관성, 분리, 내구성을 말한다. 하지만 이것은 데이터베이스의 구현마다 다를 수 있다. ACID를 만족하지 않으면 대개 BASE라고 불린다. 이들의 정의 각각을 알아보자.

Atomicity

원자성은 더 작은 부분으로 쪼개질 수 없는 것을 말한다. ACID에서 원자성은 동시성에 대한 것이 아니다. ACID 원자성은 트랜잭션이 완전히 실행되지 않았을 때 연산들 중 일부가 되돌려지는 것이다. 이것이 없으면 크래시가 일어났을 때 변화 중 어떤 부분이 되고 어떤 부분이 안 되었는지 알 수가 없다. 중지가능성이 더 좋은 단어명이었을지도 모른다.

Consistency

일관성이라는 단어는 너무 남용되고 있다. 복제 일관성, 일관적 해싱, 선형성, ACID 일관성. ACID 일관성은 데이터에 대한 어떤 불변성이 항상 만족되어야 함을 의미한다. 이는 애플리케이션의 불변성 관념에 의존하고 애플리케이션이 트랜잭션을 정의해야 한다. 원자성, 분리성, 내구성은 데이터베이스의 특성이지만 일관성은 애플리케이션의 특성이다.

Isolation

대부분 데이터베이스는 동시에 여러 클라이언트에 의해 접근된다. 이 때 동시성 문제가 생길 수 있다. ACID에서 분리성은 동시에 작동하는 트랜잭션은 서로 분리되어야 한다는 것이다. 직렬화된 분리성은 성능 문제로 잘 쓰이지 않는다.

Durability

데이터베이스 시스템의 내구성은 트랜잭션이 성공적으로 수행되면 쓰여진 데이터가 유실되지 않음을 보장한다. 단일 노드 데이터베이스에서 내구성은 데이터가 하드 드라이브나 SSD같은 비휘발성 저장소에 써짐을 뜻한다. 완전 내구성은 존재하지 않는다.

Single-Object and Multi-Object Operations

ACID에서 원자성과 분리성은 데이터베이스가 같은 트랜잭션 내에서 클라이언트가 여러 쓰기를 할 때 만족해야 하는 것이다. 쓰기 도중 에러가 발생하면 복구 시 지금까지 쓰여진 것들이 원복되어야 한다. 동시에 발생하는 트랜잭션은 서로를 간섭하면 안 된다. 이런 정의들이 가정하는 다체 트랜잭션은 데이터의 여러 복제가 동시에 유지되어야 할 때 필요하다. 실제 예에서는 분리성이 필요하고, 원자성도 필요하다. 다체 트랜잭션은 어떤 읽기/쓰기 연산이 같은 트랜잭션에 속하는지를 판정하는 방법을 필요로 한다. 하지만 많은 비관계형 데이터베이스는 연산들을 그룹핑하는 그런 방법을 지원하지 않는다.

Single-object writes

원자성과 분리성은 하나의 오브젝트가 변경될 때에도 적용 가능하다. 저장소 엔진들은 단일 오브젝트 레벨에서는 원자성과 분리성을 만족시켜야 한다. 어떤 데이터베이스는 증가 등 더 복잡한 원자적 연산을 지원하기도 한다. 이런 단일 오브젝트 연산은 유용하지만 트랜잭션으로 볼 수는 없다.

The need for multi-object transactions

많은 분산형 데이터 스토어는 멀티 오브젝트 트랜잭션을 버렸다. 파티션을 통해 구현하기 힘들고 가용성과 성능이 크게 필요한 부분에서 방해가 될 수 있기 때문이다. 다체 트랜잭션이 필요한가? 필요할 때가 있다. 관계형 데이터베이스에서 테이블 내 행이 다른 테이블의 행을 참조할 때. 문서형 데이터 모델에서 여러 문서 내 오브젝트를 동시에 업데이트해야 할 때. 이차 인덱스가 있는 데이터베이스일 때. 이런 애플리케이션들은 트랜잭션 없이 구현될 수도 있지만 에러 처리가 더 복잡해진다.

Handling errors and aborts

트랜잭션의 핵심 특성은 에러가 발생할 시 중단되고 안전하게 원복될 수 있다는 점이다. 모든 시스템이 이 철학을 따르지는 않는다. 에러는 피할 수 없다. 이를 처리할 때 중단된 트랜잭션을 재시도하는 것은 단순하지만 완벽하지는 않다. 트랜잭션이 성공했지만 네트워크가 실패한 경우라면 트랜잭션은 두 번 수행된다. 에러가 과부하 때문이면 이 방식은 문제를 더 심화시킨다. 영구적 에러가 발생했으면 재시도는 의미가 없다. 트랜잭션이 데이터베이스 외부에 부가효과를 가진다면 이는 트랜잭션이 중단되어도 남을 수 있다. 클라이언트 프로세스가 재시도 중 실패하면 데이터베이스에 쓰려던 데이터는 유실된다.

Weak Isolation Levels

두 트랜잭션이 같은 데이터를 접근하지 않는다면 상호의존적이지 않으므로 안전하게 병렬로 수행될 수 있다. 동시성 버그는 테스트하기 힘든데, 이는 타이밍이 안 좋을 때만 발생하기 때문이다. 그래서 데이터베이스는 트랜잭션 분리를 제공해 애플리케이션 개발자들에게 동시성 이슈를 숨기려고 노력해 왔다. 실제로 분리는 그렇게 간단하지 않다. 부족한 트랜잭션 분리로 인해 발생하는 동시성 버그는 단지 이론적인 문제가 아니다. 도구에 그저 의존하기보다는 발생할 수 있는 동시성 문제를 잘 이해하고 이를 어떻게 막을지를 생각해야 한다. 실제 사용되는 약한 분리 레벨, 경합 조건과 그에 따른 적절한 분리 레벨을 알아보자.

Read Committed

가장 기본적인 트랜잭션 분리 레벨은 읽기 수행으로, 두 가지 가정을 한다. 수행된 읽기만 읽고, 수행된 쓰기만 덮어쓴다.

No dirty reads

수행되지 않은 읽기를 트랜잭션이 볼 수 있으면 불결한 읽기라 한다. 읽기 수행 분리 수준을 지키는 트랜잭션은 불결한 읽기를 막아야 한다. 이를 통해 데이터 일관성을 보장할 수 있고 혼란을 막을 수 있다.

No dirty writes

대개 우리는 쓰기가 동시에 이루어질 때 나중에 일어난 쓰기가 먼저 일어난 쓰기를 덮어씌울 것을 기대한다. 이를 위해서는 불결한 쓰기를 막아야 한다. 이를 통해 특정한 동시성 문제를 피할 수 있다. 복수 오브젝트를 업데이트할 때의 문제. 하지만 두 카운터 증가에서 오는 경합 조건은 다른 방법으로 막아야 한다.

Implementing read committed

읽기 수행은 매우 널리 쓰이는 분리 수준이다. 데이터베이스는 저수준 락을 사용해 불결한 쓰기를 막는다. 불결한 읽기도 락으로 막을 수도 있다. 하지만 이는 응답 시간을 지연시킨다. 그러므로 대다수 데이터베이스는 쓰여진 오브젝트에 대해 이전 수행된 값과 현재 락을 들고 있는 트랜잭션에서 쓰여지는 값을 모두 기억하는 방식으로 불결한 읽기를 막는다.

Snapshot Isolation and Repeatable Read

읽기 수행 분리로 트랜잭션이 필요한 모든 것을 이룰 수 있을까? 그렇지 않다. 읽기 왜곡이 일어날 수 있다. 이는 반복 불가능한 읽기의 예이다. 대개 몇 초 뒤에 다시 로드하면 일관적인 데이터를 볼 수 있게 되므로 큰 문제는 아니지만 어떤 상황들에선 문제가 될 수 있다. 백업 시에 비일관성이 발생했을 때, 데이터베이스의 큰 부분을 분석할 때. 스냅샷 분리는 이 문제에 대한 가장 흔한 해법이다. 이는 백업이나 분석 같은 오래 걸리는 읽기 전용 쿼리에 대해 유용하다. 이는 널리 쓰이낟.

Implementing snapshot isolation

스냅샷 분리의 구현은 불결한 쓰기를 막기 위해 쓰기 락을 쓴다. 불결한 읽기를 막기 위해서는 읽기 수행 시 썼던 메커니즘을 일반화한다. 데이터베이스가 읽기 수행 수준만 필요하고 스냅샷 분리는 필요 없다면 오브젝트의 버전 2개를 유지하는 것으로 충분하다. 트랜잭션에 대해 고유 단조증가 트랜잭션 ID를 부여함으로써 이를 해결할 수 있다. 삭제와 업데이트도 이를 통해 이루어질 수 있다.

Visibility rules for observing a consistent snapshot

트랜잭션이 데이터베이스로부터 읽으면 트랜잭션 ID가 어떤 오브젝트가 가측 가능하고 볼 수 있는지를 결정한다. 이는 오브젝트의 생성과 삭제에도 적용된다. 즉, 오브젝트는 읽기 시 그 오브젝트가 수행된 상태이거나 그 오브젝트의 삭제가 수행되지 않은 상태일 때만 관측 가능하다. 데이터를 제자리에서 업데이트하는 대신 값이 바뀔 때마다 새 버전을 만듦으로써 데이터베이스는 일관적인 스냅샷을 제공할 수 있다.

Indexes and snapshot isolation

다버전 데이터베이스에서는 인덱스는 오브젝트의 모든 버전을 가리키고 인덱스 쿼리가 현 트랜잭션에서 볼 수 없는 오브젝트 버전을 걸러내도록 할 수 있다. 실제로는 많은 세부 구현이 다버전 동시성 제어의 성능을 결정한다. B-트리를 쓰더라도 트리에 대해 덧붙이기 전용/쓰기 시 복제를 사용할 수 있다. 이 때 모든 쓰기 트랜잭션은 새 B-트리를 만들어 일관적인 스냅샷을 만든다.

Repeatable read and naming confusion

스냅샷 분리는 유용한 분리 수준이지만 여러 다른 이름으로 분리기도 한다. 이는 SQL 표준이 이를 규정하지 않았기 때문이다. SQL 표준의 분리 레벨은 흠이 많은데 이는 혼란을 불러왔다.

Preventing Lost Updates

읽기 수행과 스냅샷 분리 수준은 두 트랜잭션 쓰기가 동시에 일어날 때는 고려하지 않는다. 이 때는 업데이트 손실 문제가 생길 수 있다. 이는 애플리케이션이 데이터베이스로부터 읽고 이를 수정한 뒤 수정한 값을 읽을 때 생긴다. 이는 여러 상황에서 생길 수 있으므로 해법도 여러 가지이다.

Atomic write operations

많은 데이터베이스는 원자적 업데이트 연산을 지원한다. 하지만 모든 쓰기가 원자적 연산으로 표현될 수 있는 것은 아니다. 원자적 연산은 대개 오브젝트가 읽을 때 락을 걸어 업데이트가 끝날 때까지 다른 읽기가 읽을 수 없게 함으로써 구현된다. 오브젝트-관계 매핑 프레임워크를 쓰면 데이터베이스에서 제공하는 원자적 연산이 아니라 불안전한 읽기-수정-쓰기 사이클을 낳는 코드를 쓰기 쉽다.

Explicit locking

업데이트 유실을 막는 다른 방법은 애플리케이션이 업데이트될 오브젝트에 대해 명시적으로 락을 거는 것이다. 이는 잘 작동하지만 애플리케이션 코드를 잘 써야 한다.

Automatically detecting lost updates

다른 방법은 병렬로 읽기-수정-업데이트 사이클을 실행한 뒤 트랜잭션 관리자가 업데이트 유실을 발견하면 트랜잭션을 중단시키고 그 사이클을 재시도시키는 것이다. 이의 장점은 데이터베이스가 스냅샷 분리와 함께 쓰기 좋다는 것이다. 업데이트 유실 감지는 좋은 특성인데, 애플리케이션 코드가 특별한 데이터베이스 특성을 쓰도록 요구하지 않기 때문이다.

Compare-and-set

트랜잭션을 제공하지 않는 데이터베이스에서는 종종 원자적 비교-세팅 연산을 발견하기도 한다. 다만 이에 의존하기 전에 데이터베이스에서 제공하는 비교-세팅 연산이 안전한지를 확인해야 한다.

Conflict resolution and replication

복제된 데이터베이스에서는 업데이트 손실을 막는 부분에서 추가 조치가 필요하다. 데이터에 대한 단일 업데이트된 복제가 있다는 보장이 안 되기 때문이다. 그러므로 동시적 쓰기를 허용해 값에 대한 여러 충돌하는 버전을 만들고 애플리케이션 코드나 특수한 자료구조로 이 버전들을 해결하고 병합해야 한다. 원자적 연산은 연산에서 교환법칙이 성립할 때 복제 컨텍스트에서 잘 동작한다. 반면에 마지막 쓰기가 승리하는 충돌 해결법은 업데이트 손실에 취약하다.

Write Skew and Phantoms

데이터 오염을 막기 위해서는 불결한 쓰기와 업데이트 손실을 막아야 한다. 하지만 경합 조건이 이것만 있는 것은 아니다. 쓰기 왜곡이 있을 수 있다.

Characterizing write skew

쓰기 왜곡은 불결한 쓰기나 업데이트 손실은 아니지만 명백히 경합 조건이다. 이는 업데이트 손실의 일반화로 볼 수 있다. 이를 막기 위한 선택지는 더 제한적이다: 원자적 단일 오브젝트 연산은 쓸 수 없다. 업데이트 손실 자동 감지도 쓸 수 없다. 대신 트랜잭션에 대한 조건이 이를 고려해야 한다. 아니면 연관 트랜잭션에 락을 걸 수도 있다.

More examples of write skew

쓰기 왜곡에는 다른 예들이 있다. 방 예약 시스템, 멀티플레이어 게임, 사용자 이름 정하기, 이중 지출 방지.

Phantoms causing write skew

위의 모든 예들은 비슷한 패턴을 따른다: 한 트랜잭션의 쓰기가 다른 트랜잭션의 검색 결과를 변경할 때. 이를 유령이라 한다.

Materializing conflicts

데이터베이스에 인공적으로 락을 생성할 수 있을까? 이를 충돌 물질화라 한다. 이를 수행하기는 어렵고 에러에 취약하므로 최후의 수단으로만 써야 한다.

Serializability

읽기 수행이나 스냅샷 분리 수준만으로 막을 수 없는 경합 조건이 있다. 분리 수준을 이해하기 어려운 경우, 분리 수준을 결정하기 어려운 경우, 경합 조건을 감지하기 위한 도구가 없는 경우. 이 경우 직렬화 분리를 사용한다. 이는 가장 강한 분리 수준으로 여겨진다. 이는 어떻게 구현할까? 트랜잭션을 연속된 순서로 구현하거나, 이단 락을 걸거나, 낙관적 동시성 제어를 하거나.

Actual Serial Execution

동시성 문제를 피하는 가장 간단한 방법은 동시성을 완전히 없애는 것이다. 왜 이 접근법이 진작 나오지 못했을까? 두 가지 개발이 이 접근법을 가능케 했다: RAM의 가격 인하, OLTP 트랜잭션이 짧고 작은 수의 읽기/쓰기만 함이 발견됨. 트랜잭션을 직렬로 실행하는 접근법은 대역폭이 단일 CPU 코어에 제한되어 있다.

Encapsulating transactions in stored procedures

초기에 데이터베이스 트랜잭션의 목적은 모든 사용자 활동 흐름을 아우르는 것이었다. 하지만 사람들의 응답 시간은 기계에 비해 늦다. 그래서 사람이 응답하기를 기다리는 동안에도 트랜잭션은 계속 실행되어야 한다. 트랜잭션이 상호작용이라면, 동시성을 허용하지 않으면 처리량이 매우 형편없어진다. 그러므로 단일 스레드 직렬 트랜잭션은 상호작용적 복수 명령어 트랜잭션을 허용하지 않고, 전체 트랜잭션 코드를 데이터베이스에 축적 절차로 제출해야 한다.

Pros and cons of stored procedures

축적 절차는 1999년부터 SQL 표준에 들어왔다. 하지만 각 데이터베이스의 구현이 구식이었고, 데이터베이스에서 작동되는 코드가 관리하기 어려웠고, 나쁘게 쓰여질 경우 애플리케이션 서버에서 쓰여진 나쁜 코드보다 성능 저하에 큰 영향을 미쳤다. 하지만 이 문제들은 극복될 수 있다. 축적 절차와 메모리상 데이터를 통해, 단일 스레드에서 모든 트랜잭션을 실행하는 것이 가능해졌다. 이 경우 축적 절차가 결정론적이어야 한다.

Partitioning

트랜잭션을 직렬화하는 것은 동시성 문제를 없애지만 처리량을 단일 CPU 코어로 제한시킨다. 복수 CPU 코어로 확장시키려면 데이터를 분할시켜야 한다. 하지만 복수의 파티션에 접근해야 하는 트랜잭션에 대해서는 데이터베이스가 그 모든 파티션을 조정해야 한다. 파티션들에 대한 트랜잭션은 이런 조정 오버헤드가 있기 때문에 단일 파티션 트랜잭션보다 느리다. 트랜잭션이 단일 파티션이 될 수 있는지는 애플리케이션에서 사용되는 데이터의 구조에 의존한다.

Summary of serial execution

트랜잭션 직렬화는 다음 조건에서 쓸만하다: 모든 트랜잭션이 작고 빨라야 하고, 전체 데이터셋이 메모리에 들어갈 수 있어야 하고, 쓰기 처리량을 단일 CPU 코어에서 다룰 수 있어야 하고, 파티션들에 대한 트랜잭션이 매우 제한적이어야 한다.

Two-Phase Locking (2PL)

데이터베이스 트랜잭션 직렬화에 대해 널리 쓰였던 알고리즘으로 이단 락이 있다. 이는 락과 비슷하지만 락 요구 조건이 훨씬 더 강하다. 오브젝트에 대해 쓰거나 읽으려면 다른 트랜잭션이 수행되거나 중지될 때까지 기다려야 한다. 그러므로 쓰기가 읽기도 막고 읽기가 쓰기도 막는다.

Implementation of two-phase locking

2PL은 직렬화된 분리 수준으로 널리 쓰인다. 이 락은 공유 모드일 수도 있고 배타 모드일 수도 있다. 트랜잭션이 오브젝트를 읽을 때는 공유 모드로 락을 얻어야 한다. 오브젝트를 쓸 때는 배타 모드로 락을 얻어야 한다. 읽은 뒤 쓸 때는 공유 락을 배타 락으로 업그레이드해야 한다. 트랜잭션이 락을 얻었으면 그것이 끝날 때까지 락을 유지해야 한다. 이 때 데드락이 발생할 수 있다.

Performance of two-phase locking

2단 락의 가장 큰 단점은 성능이다. 이는 대개 동시성이 감소함에 따라 온다. 전통적인 관계형 데이터베이스는 트랜잭션의 시간을 제한하지 않는다. 그래서 2단 락을 제공하는 데이터베이스의 응답 시간은 매우 불안정하다. 또한 데드락도 더 빈번하다.

Predicate locks

앞에선 유령 문제를 알아보았다. 직렬화 분리를 쓰는 데이터베이스는 이를 막아야 한다. 개념적으로 이는 서술 락을 통해 막을 수 있다. 이는 트랜잭션이 어떤 조건을 만족하는 오브젝트를 읽을 때 해당하는 공유 서술 락을 얻어야 한다. 트랜잭션이 오브젝트를 삽입/수정/삭제할 때는 새 값이 존재하는 서술 락을 만족하는지를 체크해야 한다. 핵심은 서술 락은 아직 존재하지 않지만 미래에 존재하게 될 수 있는 오브젝트에도 적용된다는 것이다.

Index-range locks

서술 락은 성능이 좋지 않다. 그래서 이단 락을 쓰는 데이터베이스는 인덱스 기반 락을 쓴다. 이는 서술 락의 매칭 대상을 크게 확장한 개념이다. 이를 인덱스 기반으로 매칭하는 것이다. 이는 유령과 쓰기 왜곡을 효과적으로 막는다. 범위 락이 매칭될 수 있는 적절한 인덱스가 없으면 데이터베이스는 전체 테이블에 대한 공유 락으로 후퇴한다.

Serializable Snapshot Isolation (SSI)

직렬화 분리와 좋은 성능은 양립 불가능한가? 그렇진 않다: 직렬화 스냅샷 분리라는 알고리즘이 있다. 이는 요즈음 단일 노드 데이터베이스나 분산 데이터베이스에서 널리 쓰인다.

Pessimistic versus optimistic concurency control

이단 락은 비관적 동시성 제어로 불린다. 직렬화 부리는 비관적 동시성 제어의 극단적 사례이다. 반례로, 직렬화 스냅샷 분리는 낙관적 동시성 제어이다. 이는 오래된 아이디어이며 그 장단점은 오래 논해졌다. 용량이 충분하고 트랜잭션간 경합이 크지 않으면 낙관적 동시성 제어가 더 좋은 성능을 낸다. 직렬화 스냅샷 분리는 스냅샷 분리에서 쓰기간 직렬화 충돌을 감지하고 어떤 트랜잭션을 중지해야 할 지를 판단하는 부분을 추가한 것이다.

Decisions based on an outdated premise

앞서 본 예제에서는 다음 패턴이 반복되었다: 트랜잭션이 데이터베이스로부터 데이터를 읽고, 쿼리의 결과를 조사하고, 그 결과에 기반해 어떤 액션을 취한다. 즉 그 쿼리의 결과는 트랜잭션의 가정이라 할 수 있다. 이 때 데이터베이스는 쿼리 결과가 바뀌면 그 쿼리 결과에 기반하는 모든 쓰기는 옳지 않다고 가정해야 한다. 쿼리 결과가 바뀌었는지는 어떻게 알까?

Detecting stale MVCC reads

트랜잭션이 다른 트랜잭션의 쓰기 결과에 따라 읽기 결과가 영향받을 수 있다. 이 때문에 데이터베이스는 트랜잭션이 다른 트랜잭션의 쓰기를 언제 무시해야 할지를 결정해야 한다. 불필요한 멈춤을 막음으로써 직렬화 스냅샷 분리는 일관적인 스냅샷으로부터의 장기간 읽기에 대해 스냅샷 분리를 지원할 수 있다.

Detecting writes that affect prior reads

두 번째 경우는 다른 트랜잭션이 이전 읽기를 덧씌우는 경우이다. 인덱스 기반 락에서 인덱스를 바꾸는 경우. 이런 경우에는 이전 읽기 결과가 무효화된다. 트랜잭션이 데이터베이스에 쓸 때는 영향을 준 데이터를 읽은 최근의 트랜잭션의 인덱스를 모두 조사해야 한다. 이 경우 충돌이 발생하면 해결해야 한다.

Performance of serializable snapshot isolation

직렬화 스냅샷 분리의 성능은 어떻게 될까? 이 때 불필요한 중지의 수를 실제 구현에서 줄이는 것이 중요하다. 이단 락과 비교하자면 직렬화 스냅샷의 가장 큰 이득은 한 트랜잭션이 다른 트랜잭션의 락 대기를 막을 필요가 없다는 것이다 또한, 단일 CPU 코어에 처리량이 제한되지 않는다는 장점도 있다. 이 때 중단이 일어나는 빈도가 성능에 큰 영향을 미친다.

Summary

트랜잭션은 애플리케이션이 특정한 동시성 문제와 특정한 하드웨어/소프트웨어 고장이 일어나지 않도록 보장하는 추상화 층이다. 많은 오류 종류들이 단순한 트랜잭션 중지로 환원될 수 있으며 애플리케이션은 단지 재시도만 하면 된다. 이 장에서는 트랜잭션이 막는 데 도움을 줄 수 있는 여러 문제들의 예를 알아보았다. 모든 애플리케이션이 이 문제들 모두에 취약한 것은 아니다. 단일 레코드만 읽고 쓰는 등의 단순 접근 패턴을 가진 애플리케이션의 경우 트랜잭션 없이 관리할 수 있다. 하지만 더 복잡한 접근 패턴을 가졌을 경우 트랜잭션은 고려해야 하는 잠재적 오류 케이스의 숫자를 크게 줄인다.

트랜잭션 없이는 여러 에러 시나리오 (프로세스 크래시, 네트워크 중단, 전력 중단, 디스크 꽉 참, 예상하지 못한 도잇성 등)가 데이터가 여러 방식으로 비일관적이 될 수 있음을 의미한다. 예를 들어 비표준화된 데이터는 원본 데이터와 비교해 일관성을 잃을 수 있다. 트랜잭션 없이는 데이터베이스에서 발생할 수 있는 복잡한 상호작용하는 접근에 대해 매우 원인을 파악하기 힘들어진다.

이 장에서는 동시성 제어에 대해 깊숙히 알아보았다. 널리 쓰이는 여러 분리 수준, 구체적으로 읽기 수행, 스냅샷 분리 (반복 가능한 읽기), 직렬화. 경합 조건의 여러 예를 논함으로써 이 분리 수준을 식별해 보았다.

  • 불결한 읽기 : 한 클라이언트가 다른 클라이언트의 쓰기가 수행되기 전 읽는 것. 읽기 수행 이상의 분리 수준은 불결한 읽기를 막는다.
  • 불결한 쓰기 : 한 클라이언트가 다른 클라이언트가 썼으나 수행되지 않은 쓰기를 덮어쓰는 것. 거의 모든 트랜잭션 구현은 불결한 쓰기를 막는다.
  • 읽기 왜곡 : 클라이언트가 데이터베이스의 다른 부분을 다른 시점에 읽는 것. 이 중 어떤 경우는 반복불가능한 읽기로 불리기도 한다. 이 문제는 가장 흔히는 트랜잭션이 한 특정 시점에 대응하는 일관적 스냅샷으로부터 읽게 하는 스냅샷 분리로 방지된다. 이는 다버전 동시성 제어와 함께 구현된다.
  • 업데이트 손실 : 두 클라이언트가 읽기-수정-쓰기 사이클을 동시에 수행할 때. 하나는 다른 하나의 쓰기가 불러온 변화를 반영하지 않고 덧씌워서 데이터가 유실된다. 스냅샷 분리의 어떤 구현들은 이런 이상을 자동적으로 막지만, 다른 구현들은 직접 락을 걸어줘야 한다 (SELECT FOR UPDATE).
  • 쓰기 왜곡 : 트랜잭션이 어떤 것을 읽고 관측된 값에 기반한 결정을 한 뒤 그 결정을 데이터베이스에 쓰지만, 쓰기가 이뤄지는 시점에 그 결정의 가정이 바뀌었을 때. 직렬화된 분리만이 이 이상을 막을 수 있다.
  • 유령 읽기 : 트랜잭션이 어떠한 검색 조건과 매칭되는 오브젝트를 읽고, 다른 클라이언트가 그 검색 결과에 영향을 주는 쓰기를 할 때. 스냅샷 분리는 간단한 유령 읽기를 막지만, 쓰기 왜곡에 관련된 유령은 인덱스 기반 락 같은 특수한 처리가 필요하다.

약한 분리 수준은 이런 이상들 중 일부를 막아 주지만 애플리케이션 개발자들에게 다른 이상에 대해서는 직접 다루도록 한다. (예를 들면 명시적으로 락을 걸게 함으로써). 오직 직렬화된 분리만이 이 모든 문제를 막을 수 있다. 직렬화 트랜잭션을 구현하기 위해서는 세 가지 서로 다른 접근법이 있다.

  • 트랜잭션을 직렬화된 순서로 실행하는 것. 각 트랜잭션의 실행 속도가 매우 빠르고 트랜잭션의 처리량이 단일 CPU 코어에서 감당할 수 있다면 이것은 단순하고 효과적인 선택지이다.
  • 이단 락. 수십 년 동안 이것은 직렬화를 구현하는 표준적인 방식이었지만 성능 문제 때문에 많은 애플리케이션은 이것의 사용을 피하였다.
  • 직렬화된 스냅샷 분리 : 앞의 접근법들의 단점 대부분을 피하는 상대적으로 최신의 알고리즘. 이는 낙관적인 접근법을 해서 트랜잭션이 블로킹 없이 진행되도록 허용한다. 트랜잭션이 수행을 원할 때에는 체크된 뒤 실행이 직렬화될 수 없으면 중단된다.

이 장의 예제에서는 관계형 데이터 모델을 사용했다. 하지만 트랜잭션은 어떤 데이터 모델이 쓰였는지에 상관없이 귀중한 데이터베이스 특성이다. 이 장에서는 단일 기기에서 동작하는 데이터베이스의 관점에서 아이디어와 알고리즘을 다뤄 보았다. 분산된 데이터베이스에서의 트랜잭션은 더 어려운 난점이 존재하는데, 이것들에 대해서는 다음 장들에서 다룬다.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중