Transaction

kudos·2021년 5월 16일
0

데이터베이스

목록 보기
6/8

1. 개념

트랜잭션은 다양한 데이터 항목들을 액세스하고 갱신하는 프로그램 수행의 단위다. 트랜잭션은 begin transaction과 end transaction 형태의 명령문 또는 함수 호출로 구분된다. 하나의 트랜잭션은 이 begin transaction과 end transaction 사이에서 실행되는 모든 연산들을 가리킨다.

ACID

데이터베이스 시스템은 다음의 트랜잭션 성질들을 지원해야 한다.

  • 원자성(Atomacity) : All or nothing, 즉 트랜잭션의 모든 연산들이 정상적으로 수행 완료되거나 전혀 어떠한 연산도 수행되지 않은 연래 상태가 되도록 해야 한다.
  • 일관성(Consistency) : 고립 상태(동시에 수행되는 트랜잭션이 없는 상태)에서의 트랜잭션 수행이 데이터베이스의 일관성을 보존해야 한다.
  • 고립성(Isolation) : 트랜잭션 동시 실행의 결과가 어떤 순서로 트랜잭션들을 한 번에 하나씩 실행해서 얻은 결과와 같다는 것을 보장하는 것이다.
  • 지속성(Durability) : 트랜잭션이 성공적으로 수행 완료되고 나면, 트랜잭션에 의해 데이터베이스에 변경된 내용은 시스템에 오류가 발생한다 하더라도 지속되어야 한다.

2. 저장 장치의 구조

트랜잭션의 원자성과 지속성을 보장하는 방법을 이해하기 위해서는 데이터 항목이 데이터베이스에 어떻게 저장되고 액세스되는지 좀더 깊게 이해해야 한다.

  • 휘발성 저장 장치 : 휘발성 저장 장치 내의 정보는 시스템 자애 시에 많은 경우 손실된다. 이러한 저장 장치의 예로는 메인 메모리와 캐시 메모리가 있다. 메모리 자체의 속도가 빠르고 휘발 성 저장 장치 내의 모든 데이터는 직접 접근이 가능하기 때문에 접근 속도가 매우 빠르다.
  • 비휘발성 저장 장치 : 비휘발성 저장 장치에 저장된 정보는 시스템 장애 시에도 보존된다. 예로는 온라인 저장소로 사용하는 2차 저장 장치인 자기 디스크와 플래시 저장 장치가 있다. 비휘발성 저장 장치는 랜덤 액세스를 하는 데 있어 휘발성 저장 장치보다 느리다. 그러나 이 장치에도 데이터 손실을 야기하는 장애가 발생할 수 있다.
  • 안정 저장 장치 : 안정 저장 장치에 있는 정보는 절대 손실되지 않는다. 안정 저장 장치는 이론적으로는 얻을 수 없지만 데이터 손실이 거의 없도록 하는 기술로 근접하게 구현이 가능하다. 안정 저장 장치를 구현하기 위해서는 서로 장애의 원인이 독립적인 여러 비휘발성 저장 장치(보통 디스크)에 정보를 복사한다. 안정 저장 장치의 데이터를 갱신할 때에는 갱신 도중 발생한 실패가 데이터 손실로 연결되지 않도록 처리해야 한다.

트랜잭션이 지속적이기 위해서는 트랜잭션의 변경 사항이 안정 저장 장치에 기록되어야 한다. 이와 비슷하게, 트랜잭션의 원자성을 보장하기 위해서는 변경 사항이 디스크에 기록되기 전에 로그 레코드가 안정 저장 장치에 기록되어야 한다. 따라서 한 시스템이 지속성과 원자성을 어느 정도나 보장해줄 수 있는가는 안정 저장 장치가 얼마나 안정적인가에 영향을 받는다.

3. 트랜잭션의 원자성과 지속성

1) 트랜잭션 모델

  • active : 초기 상태로, 트랜잭션이 실행 중이면 active 상태에 있다고 말할 수 있다.
  • partially committed : 마지막 명령문이 실행된 후의 상태
  • failed : 정상적인 실행이 더이상 진행될 수 없을 때의 상태
  • aborted : 트랜잭션이 롤백되고 데이터베이스가 트랜잭션 시작 전 상태로 환원되고 난 후의 상태
  • committed : 트랜잭션이 성공적으로 완료된 후의 상태

2) 트랜잭션 수행 과정

트랜잭션은 active 상태에서 시작한다. 트랜잭션이 마지막 명령문을 실행하고 나면 partially committed 상태로 들어가게 된다. 이 시점에서 트랜잭션은 자신의 모든 실행을 완료했지만 여전히 중단될 가능성을 가지고 있다. 왜냐하면 실행 결과가 아직 메인 메모리에 존재하기 때문에 하드웨어 오류 등이 발생하면 메인 메모리 내의 데이터를 잃게 되어 성공적인 완료를 못할 수도 있기 때문이다.

데이터베이스 시스템은 실패가 발생했더라도 시스템이 재시작되었을 때 트랜잭션에 의해 이전에 수행되었던 갱신 내용이 다시 수행될 수 있도록 충분한 정보를 디스크에 저장한다. 필요한 정보가 다 저장되었을 때 트랜잭션은 committed 상태로 들어가게 된다.

트랜잭션이 더이상 정상적인 실행을 진행할 수 없을 때(하드웨어 오류/논리적 오류), 트랜잭션은 failed 상태로 들어가게 된다. 이러한 트랜잭션은 반드시 롤백 되어야 한다. 그 다음 트랜잭션은 aborted 상태로 들어간다. 이 시점에서 시스템은 다음 두 가지 선택지를 가진다.

  • 트랜잭션이 하드웨어 오류 또는 트랜잭션 자체의 논리적 오류가 아닌, 소프트웨어에 의해 중단된 경우, 그 트랜잭션을 재시작할 수 있다. 재시작된 트랜잭션은 새로운 트랜잭션으로 간주된다.
  • 트랜잭션을 강제 종료 시킬 수 있다. 이러한 강제 종료는 주로 응용 프로그램을 수정해야 하는 논리적 오류가 있거나 입력이 잘못된 경우, 또는 원하는 데이터가 데이터베이스에 없는 경우이다.

4. 트랜잭션 고립성

1) 트랜잭션 동시성 허용

트랜잭션 처리 시스템은 보통 여러 트랜잭션들이 동시에 수행되는 것을 허용한다. 여러 트랜잭션들이 동시에 데이터를 갱신함에 따라 앞에서 살펴본 것처럼 데이터의 일관성과 관련된 여러 가지 복잡한 문제들이 생긴다. 트랜잭션들의 동시성과 일관성을 모두 보장하기 위해서는 추가적인 노력이 필요하다. 트랜잭션들이 한 번에 하나씩 순차적으로 실행되도록 하면 문제는 훨씬 간단해지지만 동시성을 허용하면 다음과 같은 이점을 얻을 수 있다.

  • 처리율과 자원 이용률 향상 : 하나의 트랜잭션은 많은 단계들로 구성되어 있다. 이들 중 어떤 것들은 I/O 처리를 요청하고 다른 것들은 CPU를 요청할 수 있다. 컴퓨터 시스템에 있는 CPU와 디스크는 서로 병렬적으로 동작할 수 있다. 따라서 CPU와 I/O 시스템의 병렬성은 여러 트랜잭션들을 동시에 실행될 수 있게 해준다. 트랜잭션 A가 하나의 디스크에서 읽기와 기록을 하는 동안 트랜잭션 B는 CPU에서 뭔가를 처리할 수 있고 트랜잭션 C는 다른 디스크에서 읽기와 쓰기를 할 수도 있다. 이 모든 것들이 시스템의 처리율, 즉 주어진 시간에 처리되는 트랜잭션의 수를 높인다. 마찬가지로 프로세서와 디스크 이용률 또한 증가한다.
  • 대기 시간 감소 : 시스템에 여러 트랜잭션이 서로 섞여서 실행되고 있을 때 그 중에는 실행시간이 긴 것도 있고 짧은 것도 있다. 만약 트랜잭션들이 순차적으로 실행된다면 짧은 트랜잭션들이 긴 트랜잭션들이 끝날 때까지 오랜 시간을 기다리고 있어야 할 것이다. 동시 수행은 실행 중인 트랜잭션의 예기치 않은 지연을 줄일 수 있으며 평균 응답 시간, 즉 트랜잭션이 요청된 후에 완료될 때까지 걸리는 평균 시간을 줄일 수 있다.

2) 트랜잭션 스케줄

스케줄은 시스템에 실행 중인 트랜잭션들이 어떤 순서에 따라 실행되는지를 보여준다. 일련의 트랜잭션의 스케줄은 반드시 그들 트랜잭션들의 모든 명령어들을 포함하고 있어야 하고 명령어는 개별 트랜잭션의 명령어 순서를 따라야 한다.

순차적 스케줄 & 동시 수행 스케줄

  • 순차적 스케줄

  • 동시 수행 스케줄
    데이터베이스 시스템이 여러 트랜잭션을 동시에 수행할 때 이에 대응하는 스케줄은 더이상 순차적이지 못하다. 만약 두 트랜잭션들이 동시에 수행되고 있을 경우 운영체제 시스템은 일정 시간 한 트랜잭션을 수행하다가 문맥 전환을 일어켜 다른 트랜잭션을 수행하는 과정을 반복한다. 여러 트랜잭션들을 동시에 수행하는 경우 CPU 시간은 이들 트랜잭션들 간에 공유된다.
    아래 그림 17.4는 올바른 동시 수행 스케줄의 예시이다. 이 경우에는 순차적으로 실행한 스케줄을 완료했을 때와 동일한 상태를 가지게 된다.

    그러나 모든 동시 수행 스케줄이 올바른 상태에 도달하도록 하는 것은 아니다. 그림 17.5 스케줄이 실행된 후에는 최종 잔고가 스케줄 시작 전과 달라지게 된다. 이 상태는 비일관성 상태이다.

    만약 동시 수행을 제어하는 일이 전적으로 운영체제에게 주어져 있다면 조금 전에 살펴본 것과 같이 데이터베이스를 비일관성 상태로 만드는 많은 스케줄이 있을 수 있다. 데이터베이스 시스템이 일관된 상태에 있도록 스케줄을 실행하는 것은 데이터베이스 시스템의 몫이다. 데이터베이스 시스템의 동시성 제어 컴포넌트가 이 역할을 수행한다.
    동시 수행한 스케줄의 결과가 트랜잭션을 하나씩 순차적으로 수행하는 스케줄의 실행 결과와 동일하도록 함으로써 데이터베이스의 일관성을 보장할 수 있다. 이런 스케줄을 직렬(serializable) 스케줄이라고 한다.

5. 직렬성

1) 충돌 직렬성 개념

동시성 제어 컴포넌트가 어떻게 직렬성을 보장하는지 보기 전에, 한 스케줄이 직렬성을 갖는지 판단하는 방법을 생각해보자. 순차적 스케줄은 반드시 직렬성을 갖는다. 그러나 여러 트랜잭션들의 각 단계가 교차 수행되면 직렬성을 판단하기 어렵다. 트랜잭션은 프로그램이기 때문에 트랜잭션이 수행하는 연산이 무어싱며 여러 트랜잭션들의 연산이 어떻게 상호 작용하는지 정확히 파악하기는 어렵다. 그렇게 때문에 트랜잭션이 데이터 항목에 행하는 다양한 연산을 고려하지 않고 두 가지 연산 read와 write만을 고려할 것이다.

트랜잭션 TiT_{i}, TjT_{j}에 들어있는 두 개의 연속적인 명령어 I와 J로 구성된 스케줄 S를 고려해보자. read와 write만을 고려하면 다음 네 가지 경우가 잇을 수 있다.

  1. I=read(Q), J=read(Q) : I와 J의 순서는 아무 문제가 되지 않는다.
  2. I=read(Q), J=write(Q) : 만일 I가 J보다 먼저 나온다면 TiT_{i}가 가지는 Q의 값은 명령어 J가 있는 TjT_{j}가 기록한 Q의 값과는 다르다. 만일 J가 먼저 나온다면 TiT_{i}TjT_{j}가 기록한 값을 가지게 된다. 따라서 I와 J의 순서는 문제가 된다.
  3. I=write(Q), J=read(Q) : 2번과 같음
  4. I=write(Q), J=write(Q) : 두 명령어 모두 write 연산이므로 이 명령어들의 순서는 TiT_{i} 또는 TjT_{j}의 write 연산을 무효로 만든다. 그러므로 스케줄 S의 다음 명령어 read(Q)로 얻어지는 값에 영향을 주기 때문에 그 순서는 문제가 된다.

위에 따르면, 만일 같은 데이터 항목에 서로 다른 트랜잭션의 연산이 일어나는데 그 중 최소 하나는 write 연산일 경우 I와 J는 서로 충돌(conflict)한다고 말할 수 있다.

그림 17.6을 보자. T1T_{1}의 write(A)는 T2T_{2}의 read(A)와 충돌한다. 그러나 T2T_{2}의 write(A)는 T1T_{1}의 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은 충돌 직렬적이다.

2) 충돌 직렬성을 검사하는 방법

스케줄 S로부터 우선순위 그래프(precedence graph)라고 하는 방향성 그래프를 생성할 수 있다.

만일 S의 순위 그래프에 사이클이 있을 경우 스케줄 S는 충돌 직렬적인 스케줄이 아니다. 반대로 만일 그래프에 사이클이 없다면 스케줄 S는 충돌 직렬적인 스케줄이다.

트랜잭션의 직렬 순서(serializability order)는 순위 그래프의 부분 순서에 대응되는 선형 순서를 결정하는 위상 정렬(topological sorting)을 통해 얻을 수 있다. 그러므로 충돌 직렬성을 검사하기 위해서는 먼저 순위 그래프를 만든 다음 사이클 탐색 알고리즘을 실행하면 된다.

6. 고립성과 원자성

지금까지는 트랜잭션의 실패가 없다고 가정하고 스케줄을 다뤘다. 이제는 동시 실행 중에 트랜잭션의 실패가 어떤 영향을 끼치는지에 대해 살펴보려고 한다.

만약 어떤 이유에서건 트랜잭션 TiT_{i}가 실패하면, 트랜잭션의 원자성을 보장하기 위해 이 트랜잭션에 의해 생긴 영향을 취소해야 한다. 동시 실행을 지원하는 시스템에서는 TiT_{i}에 종속적인 트랜잭션 TjT_{j}(TjT_{j}TiT_{i}에 의해 기록된 데이터를 읽을 경우) 또한 중단될 수 있도록 보장해야 한다. 이러한 것을 보증하기 위해 시스템에서 수행하는 스케줄의 형태에 어떤 제한을 둘 필요가 있다.

1) 복구 가능한 스케줄

그림 17.14의 부분 스케줄 9를 살펴보자. 여기서 T7T_{7}은 read(A)만 수행하는 트랜잭션이다. 이 스케줄을 부분 스케줄(partial schedule)이라고 한 이유는 T6T_{6}에 대해 commit 또는 abort 명령이 없기 때문이다. T7T_{7}은 read(A)를 수행한 후 바로 커밋한다. 따라서 T7T_{7}T6T_{6}이 아직 active 상태인 상황에서 커밋한다.

이제 T6T_{6}이 커밋을 하기 전에 실패했다고 가정하자. T7T_{7}T6T_{6}이 기록한 데이터 항목 A의 값을 이미 읽었다. 따라서 T7T_{7}T6T_{6}종속적(dependent)이다. 이때문에 원자성을 보장하기 위해서는 T7T_{7}도 취소해야 한다. 그러나 T7T_{7}은 이미 커밋되었고 취소할 수 없다. 따라서 T6T_{6}의 실패로부터 올바로 복구할 수 없는 상황이 되었다.

스케줄 9은 복구 불가능한 스케줄의 예이다. 복구 가능한 스케줄(recoverable schedule)이란 모든 트랜잭션 쌍 TiT_{i}TjT_{j}에 대해, TiT_{i}가 이전에 기록한 데이터 항목을 TjT_{j}가 읽었다면 TiT_{i}의 커밋 연산의 TjT_{j}의 커밋 연산보다 먼저 발생하는 스케줄을 말한다. 즉, 스케줄 9이 복구 가능하기 위해서는 T7T_{7}T6T_{6}이 커밋할 때까지 커밋을 지연해야 한다.

2) 비연쇄적인 스케줄

스케줄이 복구 가능하다고 해도 트랜잭션 TiT_{i}의 실패로부터 올바르게 복구하기 위해서는 여러 트랜잭션들을 롤백시켜야 한다.
다음 그림 17.15의 부분 스케줄을 고려해보자. T8T_{8}이 실패했다고 가정하자. T8T_{8}은 반드시 롤백되어야 한다. T9T_{9}T8T_{8}에 종속적이기 때문에 T9T_{9}는 롤백되어야 한다. T10T_{10}T9T_{9}에 종속적이기 때문에 T10T_{10} 또한 롤백되어야 한다. 이렇게 하나의 트랜잭션이 취소됨으로써 다른 일련의 트랜잭션들이 따라서 취소되는 현상을 연쇄적 롤백(cascading rollback)이라고 부른다.
연쇄적 롤백은 바람직하지 않은 현상이다. 왜냐하면 연쇄적 롤백으로 인해 상당히 많은 양의 작업이 취소되기 때문이다. 그러므로 연쇄적 롤백이 발생하지 않도록 스케줄에 제한을 줘야 한다. 이러한 스케줄을 비연쇄적인 스케줄(cascadeless schedule)이라고 한다. 비연쇄적인 스케줄은 TiT_{i}에 의해 기록된 데이터를 TjT_{j}가 읽는 트랜잭션 쌍에서, TjT_{j}의 읽기 연산을 실행하기 전에 TiT_{i}의 커밋 연산이 먼저 실행되는 스케줄을 말한다.

7. 트랜잭션 고립성 수준

직렬성은 프로그래머가 트랜잭션의 코드를 작성할 때에 동시성을 고려하지 않을 수 있도록 해주는 유용한 개념이다. 모든 트랜잭션이 각자 혼자 수행되었을 때에 데이터베이스의 일관성을 깨지 않는다면, 직렬성은 이들을 동시에 수행해도 일관성을 유지할 수 있다는 것을 보장한다. 그러나 직렬성을 보장하기 위한 규약이 어떤 애플리케이션에서는 동시성을 거의 허용하지 않는 경우가 생길 수도 있다.

이러한 경우를 위해 SQL 표준에서는 한 트랜잭션이 다른 트랜잭션들과 비직렬적으로 수행되어도 됨을 지정할 수 있다. SQL 표준에 지정된 고립성 수준은 다음과 같다.

  • serializable : 직렬적 실행을 보장
  • repeatable read : 커밋된 레코드들만 읽을 수 있고 한 트랜잭션이 한 레코드를 두 번 읽는 사이에 다른 트랜잭션이 그 레코드를 갱신하지 못하도록 한다. 그러나 트랜잭션들은 직렬적이지 않을 수 있다.
  • read committed : 커밋된 레코드들만 읽을 수 있지만 반복 읽기는 요구되지 않는 것이다. 예를 들어 한 트랜잭션이 한 레코드를 두 번 읽는 사이에 그 레코드는 다른 완료된 트랜잭션들에 의해 갱신될 수 있다.
  • read uncommitted : 커밋되지 않은 데이터도 읽는다. SQL에서 허용하는 가장 낮은 수준의 고립성 수준이다.

위의 모든 고립성 수준은 추가적으로 dirty wirte을 허용하지 않는다. 즉, commit 또는 aborted 되지 않은 다른 트랜잭션이 기록한 데이터 항목에 대한 기록은 허용하지 않는다.

많은 데이터베이스 시스템은 기본적으로 read committed 고립성 수준으로 동작한다. 시스템의 기본 설정을 사용하지 않고 SQL을 이용해 명시적으로 고립성 수준을 설정할 수 있다. 애플리케이션 설계자는 시스템의 성능을 높이기 위해 약한 고립성 수준을 사용할 수 있다. 직렬성을 보장하기 위해서는 다른 트랜잭션이 대기하도록 하거나 어떤 경우에는 트랜잭션이 직렬성 수행을 할 수 없게 되는 경우에는 취소하기도 한다. 성능을 위해 데이터베이스의 일관성이 깨질 위험을 감수하는 것이 근시안적인 것으로 보일 수도 있지만, 비일관성이 애플리케이션에 영향을 미치지 않는다면 이 방식도 의미를 가질 수 있다.

참고

Database System Concepts 14장

profile
kudos

0개의 댓글