posting: 2022-05-01, update: 2023-10-06
1. 트랜잭션 개념
트랜잭션은 다양한 데이터 항목에 접근하고 갱신하는 프로그램 수행의 단위이다. 트랜잭션은 여러 연산으로 구성되어 있지만 사용자에게는 하나의 나눌 수 없는 단위로 보여야 한다. 여기서 ”하나의 나눌 수 없는 단위“라는 말이 매우 중요하다. 이것으로부터 트랜잭션의 성질인 ACID가 나온다. 트랜잭션의 성질인 ACID의 설명은 다음과 같다.
- 원자성(Atomicity): 트랜잭션의 모든 연산이 정상적으로 수행 완료되거나 아니면 어떠한 연산도 수행되지 않은 원래 상태가 되도록 해야 한다.
- 트랜잭션은 “하나의 나눌 수 없는 단위”이므로 전부 실행되거나 전부 실행되지 않아야 한다.
- 이를 위해 데이터베이스는 트랜잭션이 쓰기 연산을 할 때 예전 값을 계속 추적한다. 추적한 값은 로그(log)라 불리는 파일에 기록된다. 트랜잭션이 비정상 종료된다면 데이터베이스 시스템은 로그를 사용해 데이터를 이전 값으로 되돌린다.
- 고립성(Isolation): 여러 트랜잭션이 동시에 수행될 때, 다른 트랜잭션이 동시에 수행되고 있는지를 몰라야 한다. 즉, 다른 트랜잭션에 영향을 받지 않아야 한다.
- 트랜잭션은 하나의 단위이기에 여러 데이터베이스 연산으로 구성된 것처럼 보여선 안된다. 하지만 하나의 데이터베이스 연산이 데이터베이스에 여러 번 접근하거나 트랜잭션 하나가 여러 개의 SQL문으로 이루어질 수 있다. 이때, 서로 다른 트랜잭션에서 이런 연산을 수행한다면 CPU는 context switching을 하면서 서로 다른 트랜잭션을 번갈아 수행할 수 있다. 여기서 트랜잭션 하나가 다른 트랜잭션에 영향을 미친다면 트랜잭션은 하나의 단위로 보이지 않는다.
- 만약 고립성이 제대로 지켜지지 않아 실세계의 실제 상태를 반영하지 못한다면 이 상태를 비일관성 상태(inconsistent state)라고 한다.
- 지속성(durability): 트랜잭션이 성공적으로 수행 완료되고 나면, 트랜잭션에 의해 변경된 데이터베이스 내용은 시스템에 오류가 발생한다고 하더라도 영구적으로 반영되어야 한다.
- 시스템이 올바르게 트랜잭션을 수행했어도 시스템에 장애가 발생하면 트랜잭션의 변경사항은 소실된다. 때문에 트랜잭션 실행 결과는 시스템 장애가 발생해도 영구적으로 반영돼야 한다.
- 일관성(Consistency): 고립 상태에서 트랜잭션 수행이 데이터베이스의 일관성을 보존해야 한다.
- 하나의 트랜잭션이 일관된 상태의 데이터베이스에서 원자적이고 고립된 상태로 실행되었다면 데이터베이스는 트랜잭션이 종료되었어도 일관된 상태여야 한다.
2. 트랜잭션의 여러 상태
위에서 언급했듯이 트랜잭션은 항상 성공하지 않기 때문에 다음과 같은 상태들을 가질 수 있다.
- action: 초기 상태로, 실행 중인 트랜잭션을 의미한다
- partially committed: 마지막 명령문이 실행된 후의 상태
- 트랜잭션 실행은 완료했지만 실행 결과가 메모리에 존재하고 디스크에 반영되지 않은 상태이다. 이때 하드웨어 오류가 발생한다면 메인 메모리 내의 데이터는 휘발되어 성공적인 완료를 하지 못할 수도 있다.
- failed: 정상적인 실행이 더 진행될 수 없을 때 가지는 상태
- failed 상태의 트랜잭션은 반드시 롤백 되고 중단 상태로 들어가야 한다.
- aborted: 트랜잭션이 롤백 되고 데이터베이스가 트랜잭션 시작 전 상태로 환원되고 난 후의 상태
- 롤백은 트랜잭션에 의해 수행된 갱신이 무효화된 상태를 의미한다.
- 트랜잭션의 중단과 롤백은 주로 로그를 유지하는 방법을 통해 이루어진다. 트랜잭션이 데이터베이스에 가한 모든 수정은 우선적으로 로그에 기록된다. 기록 후 데이터베이스에 수정 내용이 반영된다. 로그에는 다음과 같은 값을 기록한다.
- 트랜잭션 식별자
- 수정하는 데이터 항목의 식별자
- 데이터 항목의 이전 값
- 새로운 값
- 트랜잭션이 중단 상태로 들어갔을 때, 시스템은 두 가지 선택을 할 수 있다.
- 하드웨어 오류 또는 트랜잭션 자체의 논리적 오류가 아닌 소프트웨어 오류로 인해 중단되었다면, 그 트랜잭션을 재시작할 수 있다. 재시작된 트랜잭션은 새로운 트랜잭션으로 간주한다
- 트랜잭션을 강제 종료(kill)할 수 있다. 강제 종료는 보통 애플리케이션을 직접 수정해야 하는 논리적 오류나 입력이 잘못된 경우, 또는 필요한 데이터 데이터베이스에 없는 경우이다.
- committed: 트랜잭션이 성공적으로 완료된 후의 상태
- 하나의 트랜잭션이 커밋 되었다는 것은 데이터베이스가 그 트랜잭션에 의해 새로운 일관된 상태로 변경되었다는 것을 의미한다. 이 일관된 상태는 시스템 오류가 발생해도 지속되어야 한다.
- 커밋 된 트랜잭션으로 인한 변화는 되돌릴 수 없다. 커밋 된 트랜잭션의 영향을 취소하는 유일한 방법은 보상 트랜잭션(compensating transaction)을 실행하는 것이다.
3. 트랜잭션 고립성
트랜잭션을 한 번에 하나씩 순차적으로(serially) 실행되게 한다면 데이터 일관성과 관련된 여러 문제들을 고려하지 않아도 된다. 하지만 여러 이점들 때문에 데이터베이스에선 여러 트랜잭션들이 동시에 수행된다. 때문에 트랜잭션의 동시성과 일관성을 모두 보장하기 위한 조치가 필요하다. 동시에 여러 트랜잭션을 실행하면 다음과 같은 이점을 얻는다.
- 처리율과 자원 이용률 향상: 하나의 트랜잭션을 구성하는 단계의 연산은 크게 I/O 처리와 CPU 연산으로 나눌 수 있다. 컴퓨터 시스템의 CPU과 디스크는 병렬적으로 동작할 수 있다. 때문에 I/O 연산과 CPU 처리는 병렬적으로 처리될 수 있다. 이런 병렬성을 이용하면 하나의 트랜잭션이 디스크 I/O를 진행할 때 다른 트랜잭션이 CPU 연산을 수행할 수 있다. 이는 결국 시스템의 처리율(throughtput - 주어진 시간에 처리되는 트랜잭션의 수)를 높인다. 그뿐만 아니라 프로세서와 디스크 이용률(utilization)도 증가한다.
- 대기 시간 감소: 트랜잭션의 실행 시간은 일정하지 않다. 만약 트랜잭션이 순차적으로 실행된다면 짧은 트랜잭션이 긴 트랜잭션이 끝날 때 가지 대기해야 할 수 있다. 이는 트랜잭션 수행의 지연을 초래한다. 만약 서로 다른 트랜잭션이 데이터베이스 내의 서로 다른 데이터를 조작하고 있다면 CPU 연산과 디스크 I/O는 병렬 수행이 가능하므로 CPU 연산력과 디스크 I/O라는 자원을 트랜잭션들이 공유해 사용하면 보다 지연을 줄일 수 있다. 또 한 평균 응답 시간(average response time), 즉 트랜잭션이 요청된 후에 완료될 때까지 걸리는 평균 시간을 줄일 수 있다.
이런 이유로 트랜잭션들의 실행 순서는 더 이상 순차적(serial)이지 않다. 운영체제는 context switching을 통해 서로 다른 트랜잭션들을 번갈아가면서 수행한다. 여러 트랜잭션을 동시에 실행하면 CPU 처리 시간은 이들 트랜잭션 간에 공유된다. 여기서 운영체제는 그저 context swtiching을 이용해 서로 다른 트랜잭션을 번갈아가면서 수행하기만 하므로 데이터베이스를 항상 일관된 상태에 있게 트랜잭션들을 동시 수행하는 것은 데이터베이스 시스템의 몫이다. 데이터베이스 시스템의 동시성 제어(concurrency-control) 구성요소가 이 역할을 한다.
동시 수행한 트랜잭션들의 결과가 트랜잭션을 하나씩 순차적으로 수행한 결과와 같게 해서 데이터베이스의 일관성을 보장할 수 있다. 이를 직렬 가능(serializable)이라 한다.
3.1. 트랜잭션 고립성 수준(isolation level)
직렬 가능성은 서로 다른 트랜잭션들을 동시에 수행해도 일관성을 유지할 수 있게 해준다. 하지만 직렬 가능성을 보장하기 위한 규약이 애플리케이션에서 동시성을 거의 허용하지 않는 경우가 생길 수 있다. 이때 약한 수준의 일관성을 사용할 수 있다. 대신 프로그래머가 데이터베이스의 정합성을 보장해야 한다.
SQL 표준에 명시된 고립성 수준은 다음과 같다.
- Serializable: 직렬 가능한 실행을 보장한다.
- Repeatable read: 오직 커밋 된 레코드만 읽을 수 있다. 트랜잭션 하나가 레코드를 두 번 읽는 사이에 다른 트랜잭션이 그 레코드를 갱신하지 못한다. 하지만 다른 트랜잭션에서 수행한 변경 작업에 의한 레코드가 보였다 안 보였다 하는 phantom read 문제가 발생한다. 때문에 직렬 가능하지 않을 수 있다.
- Read committed: 커밋된 레코드만 읽는다. 하나의 트랜잭션에서 두 번에 읽기를 시도할 때 두 번의 읽기 사이에 다른 트랜잭션이 해당 데이터를 변경한다면 두 번의 읽기는 서로 다른 결과를 낸다. repeatable read를 지원하지 않는다.
- Read uncommited: 커밋 되지 않은 데이터도 접근할 수 있다. SQL에서 가장 낮은 수준의 고립성이다.