트랜잭션은 다양한 데이터 항목들을 액세스하고 갱신하는 프로그램 수행의 단위다. 트랜잭션은 begin transaction과 end transaction 형태의 명령문 또는 함수 호출로 구분된다. 하나의 트랜잭션은 이 begin transaction과 end transaction 사이에서 실행되는 모든 연산들을 가리킨다.
데이터베이스 시스템은 다음의 트랜잭션 성질들을 지원해야 한다.
- 원자성(Atomacity) : All or nothing, 즉 트랜잭션의 모든 연산들이 정상적으로 수행 완료되거나 전혀 어떠한 연산도 수행되지 않은 연래 상태가 되도록 해야 한다.
- 일관성(Consistency) : 고립 상태(동시에 수행되는 트랜잭션이 없는 상태)에서의 트랜잭션 수행이 데이터베이스의 일관성을 보존해야 한다.
- 고립성(Isolation) : 트랜잭션 동시 실행의 결과가 어떤 순서로 트랜잭션들을 한 번에 하나씩 실행해서 얻은 결과와 같다는 것을 보장하는 것이다.
- 지속성(Durability) : 트랜잭션이 성공적으로 수행 완료되고 나면, 트랜잭션에 의해 데이터베이스에 변경된 내용은 시스템에 오류가 발생한다 하더라도 지속되어야 한다.
트랜잭션의 원자성과 지속성을 보장하는 방법을 이해하기 위해서는 데이터 항목이 데이터베이스에 어떻게 저장되고 액세스되는지 좀더 깊게 이해해야 한다.
트랜잭션이 지속적이기 위해서는 트랜잭션의 변경 사항이 안정 저장 장치에 기록되어야 한다. 이와 비슷하게, 트랜잭션의 원자성을 보장하기 위해서는 변경 사항이 디스크에 기록되기 전에 로그 레코드가 안정 저장 장치에 기록되어야 한다. 따라서 한 시스템이 지속성과 원자성을 어느 정도나 보장해줄 수 있는가는 안정 저장 장치가 얼마나 안정적인가에 영향을 받는다.
트랜잭션은 active 상태에서 시작한다. 트랜잭션이 마지막 명령문을 실행하고 나면 partially committed 상태로 들어가게 된다. 이 시점에서 트랜잭션은 자신의 모든 실행을 완료했지만 여전히 중단될 가능성을 가지고 있다. 왜냐하면 실행 결과가 아직 메인 메모리에 존재하기 때문에 하드웨어 오류 등이 발생하면 메인 메모리 내의 데이터를 잃게 되어 성공적인 완료를 못할 수도 있기 때문이다.
데이터베이스 시스템은 실패가 발생했더라도 시스템이 재시작되었을 때 트랜잭션에 의해 이전에 수행되었던 갱신 내용이 다시 수행될 수 있도록 충분한 정보를 디스크에 저장한다. 필요한 정보가 다 저장되었을 때 트랜잭션은 committed 상태로 들어가게 된다.
트랜잭션이 더이상 정상적인 실행을 진행할 수 없을 때(하드웨어 오류/논리적 오류), 트랜잭션은 failed 상태로 들어가게 된다. 이러한 트랜잭션은 반드시 롤백 되어야 한다. 그 다음 트랜잭션은 aborted 상태로 들어간다. 이 시점에서 시스템은 다음 두 가지 선택지를 가진다.
트랜잭션 처리 시스템은 보통 여러 트랜잭션들이 동시에 수행되는 것을 허용한다. 여러 트랜잭션들이 동시에 데이터를 갱신함에 따라 앞에서 살펴본 것처럼 데이터의 일관성과 관련된 여러 가지 복잡한 문제들이 생긴다. 트랜잭션들의 동시성과 일관성을 모두 보장하기 위해서는 추가적인 노력이 필요하다. 트랜잭션들이 한 번에 하나씩 순차적으로 실행되도록 하면 문제는 훨씬 간단해지지만 동시성을 허용하면 다음과 같은 이점을 얻을 수 있다.
스케줄은 시스템에 실행 중인 트랜잭션들이 어떤 순서에 따라 실행되는지를 보여준다. 일련의 트랜잭션의 스케줄은 반드시 그들 트랜잭션들의 모든 명령어들을 포함하고 있어야 하고 명령어는 개별 트랜잭션의 명령어 순서를 따라야 한다.
순차적 스케줄
동시 수행 스케줄
데이터베이스 시스템이 여러 트랜잭션을 동시에 수행할 때 이에 대응하는 스케줄은 더이상 순차적이지 못하다. 만약 두 트랜잭션들이 동시에 수행되고 있을 경우 운영체제 시스템은 일정 시간 한 트랜잭션을 수행하다가 문맥 전환을 일어켜 다른 트랜잭션을 수행하는 과정을 반복한다. 여러 트랜잭션들을 동시에 수행하는 경우 CPU 시간은 이들 트랜잭션들 간에 공유된다.
아래 그림 17.4는 올바른 동시 수행 스케줄의 예시이다. 이 경우에는 순차적으로 실행한 스케줄을 완료했을 때와 동일한 상태를 가지게 된다.
그러나 모든 동시 수행 스케줄이 올바른 상태에 도달하도록 하는 것은 아니다. 그림 17.5 스케줄이 실행된 후에는 최종 잔고가 스케줄 시작 전과 달라지게 된다. 이 상태는 비일관성 상태이다.
만약 동시 수행을 제어하는 일이 전적으로 운영체제에게 주어져 있다면 조금 전에 살펴본 것과 같이 데이터베이스를 비일관성 상태로 만드는 많은 스케줄이 있을 수 있다. 데이터베이스 시스템이 일관된 상태에 있도록 스케줄을 실행하는 것은 데이터베이스 시스템의 몫이다. 데이터베이스 시스템의 동시성 제어 컴포넌트가 이 역할을 수행한다.
동시 수행한 스케줄의 결과가 트랜잭션을 하나씩 순차적으로 수행하는 스케줄의 실행 결과와 동일하도록 함으로써 데이터베이스의 일관성을 보장할 수 있다. 이런 스케줄을 직렬(serializable) 스케줄이라고 한다.
동시성 제어 컴포넌트가 어떻게 직렬성을 보장하는지 보기 전에, 한 스케줄이 직렬성을 갖는지 판단하는 방법을 생각해보자. 순차적 스케줄은 반드시 직렬성을 갖는다. 그러나 여러 트랜잭션들의 각 단계가 교차 수행되면 직렬성을 판단하기 어렵다. 트랜잭션은 프로그램이기 때문에 트랜잭션이 수행하는 연산이 무어싱며 여러 트랜잭션들의 연산이 어떻게 상호 작용하는지 정확히 파악하기는 어렵다. 그렇게 때문에 트랜잭션이 데이터 항목에 행하는 다양한 연산을 고려하지 않고 두 가지 연산 read와 write만을 고려할 것이다.
트랜잭션 , 에 들어있는 두 개의 연속적인 명령어 I와 J로 구성된 스케줄 S를 고려해보자. read와 write만을 고려하면 다음 네 가지 경우가 잇을 수 있다.
위에 따르면, 만일 같은 데이터 항목에 서로 다른 트랜잭션의 연산이 일어나는데 그 중 최소 하나는 write 연산일 경우 I와 J는 서로 충돌(conflict)한다고 말할 수 있다.
그림 17.6을 보자. 의 write(A)는 의 read(A)와 충돌한다. 그러나 의 write(A)는 의 read(B)와는 충돌이 일어나지 않는다.
I와 J가 스케줄 S에 있는 연속적인 명령어들이라고 하자. 만일 I와 J가 서로 다른 트랜잭션들의 명령어이고 서로 충돌하지 않는다면 두 명령어의 순서를 서로 바꿔서 새로운 스케줄 S'을 만들 수 있다. 이때 스케줄 S와 S'은 서로 일치한다고 볼 수 있다. 왜냐하면 I와 J의 순서를 제외하고는 나머지 명령어들의 순서가 양쪽 스케줄에서 동일하고 I, J 두 명령어도 순서에 무관하기 때문이다. 따라서 그림 17.7은 그림 17.6과 같은 스케줄이다. 이렇게 스케줄 S가 충돌이 일어나지 않는 명령어들의 순서를 바꿔서 스케줄 S'로 변경된다면 S와 S'이 충돌 동등(conflict equivalent)하다고 말할 수 있다.
충돌 동등에 대한 개념은 충돌 직렬성이라는 개념으로 확장된다. 아래의 스케줄 6은 위의 스케줄 3에서 순서를 서로 바꿀 수 있는 명령어들의 순서를 모두 바꾼 스케줄이다. 이렇게 스케줄 S가 한 순차 스케줄에 충돌 동등하면 그 스케줄 S는 충돌 직렬적(conflict serializable)이라고 한다. 그러므로 스케줄 3은 충돌 직렬적이다.
스케줄 S로부터 우선순위 그래프(precedence graph)라고 하는 방향성 그래프를 생성할 수 있다.
만일 S의 순위 그래프에 사이클이 있을 경우 스케줄 S는 충돌 직렬적인 스케줄이 아니다. 반대로 만일 그래프에 사이클이 없다면 스케줄 S는 충돌 직렬적인 스케줄이다.
트랜잭션의 직렬 순서(serializability order)는 순위 그래프의 부분 순서에 대응되는 선형 순서를 결정하는 위상 정렬(topological sorting)을 통해 얻을 수 있다. 그러므로 충돌 직렬성을 검사하기 위해서는 먼저 순위 그래프를 만든 다음 사이클 탐색 알고리즘을 실행하면 된다.
지금까지는 트랜잭션의 실패가 없다고 가정하고 스케줄을 다뤘다. 이제는 동시 실행 중에 트랜잭션의 실패가 어떤 영향을 끼치는지에 대해 살펴보려고 한다.
만약 어떤 이유에서건 트랜잭션 가 실패하면, 트랜잭션의 원자성을 보장하기 위해 이 트랜잭션에 의해 생긴 영향을 취소해야 한다. 동시 실행을 지원하는 시스템에서는 에 종속적인 트랜잭션 (가 에 의해 기록된 데이터를 읽을 경우) 또한 중단될 수 있도록 보장해야 한다. 이러한 것을 보증하기 위해 시스템에서 수행하는 스케줄의 형태에 어떤 제한을 둘 필요가 있다.
그림 17.14의 부분 스케줄 9를 살펴보자. 여기서 은 read(A)만 수행하는 트랜잭션이다. 이 스케줄을 부분 스케줄(partial schedule)이라고 한 이유는 에 대해 commit 또는 abort 명령이 없기 때문이다. 은 read(A)를 수행한 후 바로 커밋한다. 따라서 은 이 아직 active 상태인 상황에서 커밋한다.
이제 이 커밋을 하기 전에 실패했다고 가정하자. 은 이 기록한 데이터 항목 A의 값을 이미 읽었다. 따라서 은 에 종속적(dependent)이다. 이때문에 원자성을 보장하기 위해서는 도 취소해야 한다. 그러나 은 이미 커밋되었고 취소할 수 없다. 따라서 의 실패로부터 올바로 복구할 수 없는 상황이 되었다.
스케줄 9은 복구 불가능한 스케줄의 예이다. 복구 가능한 스케줄(recoverable schedule)이란 모든 트랜잭션 쌍 와 에 대해, 가 이전에 기록한 데이터 항목을 가 읽었다면 의 커밋 연산의 의 커밋 연산보다 먼저 발생하는 스케줄을 말한다. 즉, 스케줄 9이 복구 가능하기 위해서는 은 이 커밋할 때까지 커밋을 지연해야 한다.
스케줄이 복구 가능하다고 해도 트랜잭션 의 실패로부터 올바르게 복구하기 위해서는 여러 트랜잭션들을 롤백시켜야 한다.
다음 그림 17.15의 부분 스케줄을 고려해보자. 이 실패했다고 가정하자. 은 반드시 롤백되어야 한다. 는 에 종속적이기 때문에 는 롤백되어야 한다. 은 에 종속적이기 때문에 또한 롤백되어야 한다. 이렇게 하나의 트랜잭션이 취소됨으로써 다른 일련의 트랜잭션들이 따라서 취소되는 현상을 연쇄적 롤백(cascading rollback)이라고 부른다.
연쇄적 롤백은 바람직하지 않은 현상이다. 왜냐하면 연쇄적 롤백으로 인해 상당히 많은 양의 작업이 취소되기 때문이다. 그러므로 연쇄적 롤백이 발생하지 않도록 스케줄에 제한을 줘야 한다. 이러한 스케줄을 비연쇄적인 스케줄(cascadeless schedule)이라고 한다. 비연쇄적인 스케줄은 에 의해 기록된 데이터를 가 읽는 트랜잭션 쌍에서, 의 읽기 연산을 실행하기 전에 의 커밋 연산이 먼저 실행되는 스케줄을 말한다.
직렬성은 프로그래머가 트랜잭션의 코드를 작성할 때에 동시성을 고려하지 않을 수 있도록 해주는 유용한 개념이다. 모든 트랜잭션이 각자 혼자 수행되었을 때에 데이터베이스의 일관성을 깨지 않는다면, 직렬성은 이들을 동시에 수행해도 일관성을 유지할 수 있다는 것을 보장한다. 그러나 직렬성을 보장하기 위한 규약이 어떤 애플리케이션에서는 동시성을 거의 허용하지 않는 경우가 생길 수도 있다.
이러한 경우를 위해 SQL 표준에서는 한 트랜잭션이 다른 트랜잭션들과 비직렬적으로 수행되어도 됨을 지정할 수 있다. SQL 표준에 지정된 고립성 수준은 다음과 같다.
위의 모든 고립성 수준은 추가적으로 dirty wirte을 허용하지 않는다. 즉, commit 또는 aborted 되지 않은 다른 트랜잭션이 기록한 데이터 항목에 대한 기록은 허용하지 않는다.
많은 데이터베이스 시스템은 기본적으로 read committed 고립성 수준으로 동작한다. 시스템의 기본 설정을 사용하지 않고 SQL을 이용해 명시적으로 고립성 수준을 설정할 수 있다. 애플리케이션 설계자는 시스템의 성능을 높이기 위해 약한 고립성 수준을 사용할 수 있다. 직렬성을 보장하기 위해서는 다른 트랜잭션이 대기하도록 하거나 어떤 경우에는 트랜잭션이 직렬성 수행을 할 수 없게 되는 경우에는 취소하기도 한다. 성능을 위해 데이터베이스의 일관성이 깨질 위험을 감수하는 것이 근시안적인 것으로 보일 수도 있지만, 비일관성이 애플리케이션에 영향을 미치지 않는다면 이 방식도 의미를 가질 수 있다.
Database System Concepts 14장