[이해하기] 데이터베이스 트랜잭션의 기본 속성 – ACID 그리고 ACID 를 보장하는 방법

1. 데이터베이스 (Database) 의 트랜잭션 (Transaction) 과 ACID

데이터베이스에서의 트랜젝션이란, 데이터의 추가, 수정, 삭제 등을 처리하는 여러 단계들을 하나로 묶은 논리적인 작업 단위를 의미합니다. 특히 데이터베이스의 작업 단위 (Query) 하나는 실행되거나, 실행되지 않아야 합니다. (All or nothing)

(** 참고 : 데이터베이스 관리 시스템 (DBMS) 의 성능은 초당 트랜잭션의 실행 수 ( TPS : Transaction per second) 로 측정합니다.)

은행 계좌 이체를 예로 설명해보겠습니다. 송신자가 일정 금액을 보내면 송신자 계좌의 금액은 감소되어야 하고, 수신자의 계좌에 금액이 금액이 증가 되어야 하는데 이 과정이 모두 하나의 트랜잭션 단위로 이루어져야 할 것입니다. (정상적으로 완료된 경우 Commit 되었다 라고 표현합니다.) 만약 해당 작업을 진행하는 도중, 어느 한 단계에서라도 문제가 발생한다면 위 과정 자체가 모두 취소되고 원 상태로 복구 (Rollback) 되어야 합니다.

이러한 실 세계의 정보를 저장하고 처리하는 데이터베이스의 특성을 고려하여, 데이터베이스는 기본적으로 ACID (Atomicity, Consistency, Isolation, Durability) 라는 원칙을 지켜야 합니다.

(1) 원자성 (Atomicity)
원자성이란, 트랜잭션과 관련된 작업들이 부분적으로 실행되다가 중단되지 않는 것을 보장하는 것을 의미합니다. 예를 들어, 은행 계좌 이체는 성공할 수도 실패할 수도 있습니다. 그러나 보내는 쪽에서의 잔고가 감소하는 작업만 성공하고, 수신 측에 잔고가 추가 되는 작업을 실패해서는 안됩니다. 원자성은 이와 같이 중간 단계까지만 실행되고 실패하는 일이 없도록 보장해야 한다는 것을 의미합니다.

(2) 일관성 (Consistency)
트랜잭션의 실행이 성공적으로 완료되면 데이터베이스를 항상 일관성 있는 상태로 유지해야된다 라는 것을 의미합니다. 예를 들어, 고객 정보가 담겨있는 데이터베이스에 새로운 고객이 등록될 경우, 그 데이터베이스를 참조 하는 다른 하위 계층의 데이터베이스 또는 테이블도 같은 고객의 세부 정보 (예> 이름, 전화 번호 등) 를 가져와야 합니다. 또 다른 예시로, 모든 계좌는 잔고가 있어야 한다 라는 무결성 제약이 있다면 이를 위반하는 트랜잭션은 모두 중단되어야 합니다.

(** 무결성 : 데이터베이스에 저장된 데이터 값과 그것이 표현하는 현실 세계의 실제값이 일치해야 한다 라는 정확성을 가지는 조건을 의미합니다. 모든 계좌는 잔고가 있어야 한다는 무결성 제약 조건의 경우, ‘0’ 을 포함한 어떠한 형태의 값이라도 기록되어야 하는 ‘Null 무결성’ 을 설명하고 있습니다. 실제 계좌의 잔고는 0 혹은 마이너스가 기록될 수 있을지라도 아무 값이 없는 ‘Null’ 상태가 될 경우 다른 계좌와의 정상적인 트랜잭션이 일어날 수 없기 때문입니다.)

(3) 독립성 또는 고립성 (Isolation)
트랜잭션이 수행 되고 있을 때, 다른 트랜잭션의 연산 작업이 중간에 끼어들어 기존 트랜잭션의 작업에 영향을 주지 못하도록 독립성을 보장하는 것을 의미합니다. (즉, 동시성 제어가 필요)

데이터베이스의 트랜잭션도 CPU 의 프로세스들을 처리하는 것과 마찬가지로, 성능과 효율성을 위해 병행처리를 할 수 있습니다. (병행처리 : 특정 시간 동안 작업을 하다가 부여된 시간이 끝나면 다른 트랜잭션을 실행해나가는 방식) 이 때, 많은 트랜잭션들이 병행되어 조금씩 처리되어 가는 도중 공통된 데이터를 조작하는 경우, 데이터가 흔히 이야기하는 ‘꼬이는’ 상황이 발생할 수 있습니다. (예> A 트랜잭션에서 은행 계좌의 100원을 빼내는 연산을 하던 도중, B 트랜잭션으로 처리 순서가 넘어가서 50원이 빼내는 연산이 먼저 처리되는 경우 등)

따라서 독립성 또는 고립성을 보장하면, 예를 들어 은행 직원이 계좌의 이체 작업을 하는 도중 다른 추가 데이터베이스 쿼리 (추가, 수정, 삭제 등) 를 실행하더라도 그 순간에 해당 계좌들을 들여다보거나 다른 작업을 동시에 수행할 수 없게 됩니다. (기존 트랜잭션이 완료되고 나서야 계좌를 확인하거나 추가 쿼리를 실행할 수 있도록 트랜잭션 처리는 순차적이어야 합니다.)

(4) 지속성 (Durability)
성공적으로 수행된 트랜잭션은 요청된 작업의 내용이 데이터베이스에 영원히 반영되어야 함을 의미합니다. 시스템 문제가 발생하거나, 데이터베이스의 일치 작업을 수행 하더라도 데이터베이스의 내용은 기존과 같이 유지되어야 함을 의미합니다. 이를 위해서 모든 트랜잭션은 로그로 남겨져 시스템 장애 발생 전 상태로 되돌릴 수 있어야 합니다. 또한 모든 트랜잭션은 로그에 모든 것이 저장된 후에만 정상적으로 처리된 Commit 상태로 고려될 수 있습니다.


2. ACID 의 구현

(1) 원자성 (Atomicity) 보장
원자성을 보장하기 위해서는, 처리 중인 트랜잭션에서 오류가 발생했을 때 현재의 처리 중인 내용을 취소하고 트랜잭션 실행 시 임시 영역에 저장해두었던 이전의 상태 (Commit) 를 다시 불러와서 Rollback 하는 방법이 있습니다. (이 전의 데이터들이 임시로 저장되는 별도의 영역을 Rollback Segment 라고 합니다.) 만약 트랜잭션이 처리해야 할 양이 많아지는 경우에는 중간 부분의 Save point 를 지정하여 해당 부분 부터만 Rollback 작업을 수행함으로써 리소스의 낭비를 방지하고 처리 시간을 단축할 수 있습니다.

(2) 일관성 (Consistency) 보장
트랜잭션이 일어났을 때 미리 정의된 트리거 (Trigger) 를 통해 일관성이 보장될 수 있습니다. 예를 들어, 한 쪽 데이터베이스의 테이블에 정보의 수정이 일어났을 경우 다른 쪽 테이블에도 함께 수정될 수 있도록 명시적으로 자동 업데이트를 하는 명령 등을 구성하는 방법이 있습니다.

(3) 독립성 또는 고립성 (Isolation) 보장

– Lock & Unlock
데이터를 읽거나 쓰기 작업 중일때는 해당 영역에 Lock 을 걸어서 다른 트랜잭션이 접근하지 못하도록하고, 먼저 들어온 트랜잭션 요청이 끝나면 Unlock 하여 다른 트랜잭션이 처리될 수 있도록 허용 하는 방식을 통해 독립성/고립성을 보장할 수 있습니다. 단, Lock 과 Unlock 을 잘 못 사용하게 되는 경우 어떤 트랜잭션도 수행될 수 없는 데드락 (Deadlock) 상태에 빠질 수 있습니다. (예> 트랜잭션1 -> 트랜잭션3 -> 트랜잭션2 -> 트랜잭션1 -> …무한 대기 루프)

참고로 다음과 같이 두 가지의 Lock 방식이 있습니다.
> 보수적인 Locking (Conservative Locking)
트랜잭션이 시작되면 모두 Lock 을 하는 방식으로 데드락이 발생하는 경우는 없으나 병행성이 떨어지게 됩니다.
> 강한 Locking (Strict Locking)
트랜잭션이 완전히 Commit 될 때까지 Lock 을 걸어두고 있다가 Commit 이 후 Unlock을 하는 방식으로 데드락이 발생할 수 있지만 병행성이 좋습니다. (일반적으로는 병행성이 좋은 Strict Locking 방식을 사용합니다.)

– 2단계 Locking 프로토콜 (2PL – 2 Phase Locking Protocol)
데드락을 방지하기 위한 2단계 Locking 프로토콜은, 여러 트랜잭션이 동시에 한 데이터로 접근할 수 없도록 2가지의 Lock 방법으로 트랜잭션 처리에 제한을 두는 규약을 의미합니다. 요약해서, Growing Phase (상승 단계) 에서는 Lock 만 수행되어야 하고 Shrinking Phase (하강 단계) 에서는 Unlock 만 수행되어 Lock 과 Unlock 이 교차로 수행되지 않도록 하는 방식을 의미합니다. 특히 네트워크 환경에서는 그 환경의 특성상 ACID 특성을 보장하는 것이 매우 어려우므로 이와 같은 2단계 Locking 프로토콜 방식을 통해 ACID 특성을 보장할 수 있습니다.

– MVCC (Multi-Version Concurrency Control, 다중 버전 동시성 제어)
MVCC 모델에서는 다음과 같은 Snapshot 을 이용하는 방법으로 동시성을 제어하여 데드락을 방지하고 개선된 트랜잭션 성능을 보장합니다.
(1) 트랜잭션이 일어나는 특정 시점의 데이터베이스 Snapshot 을 읽습니다.
(2) 이 Snapshot 데이터에 대한 트랜잭션 수행 중 발생하는 변경 사항은 다른 사용자가 볼 수 없습니다. (트랜잭션이 Commit 될때까지)
(3) 데이터가 업데이트 되면, 이전의 데이터를 덮어 쓰는게 아니라 새로운 버전의 데이터를 만듭니다.
(4) 대신 이전 버전의 데이터와 비교해서 변경된 내용을 기록합니다.
(5) 위와 같은 과정을 거쳐 하나의 데이터에 대해 여러 버전의 데이터가 존재하게 됩니다.
(6) 사용자는 가장 마지막 버전의 데이터를 읽게 됩니다.
MVCC 의 접근 방식은 Snapshot 을 이용하여 Locking 을 필요로 하지 않기 때문에 일반적인 RDBMS 보다 더 빠르게 동작할 수 있습니다. 또한 데이터를 읽기 시작 할 때, 다른 사람이 그 데이터를 삭제하거나 수정하더라도 영향을 받지 않고 데이터를 사용할 수 있습니다. 단, MVCC 방식을 이용하게 될 경우 사용하지 않는 데이터가 계속 쌓이게 되므로 데이터를 적절하게 정리해주는 별도의 시스템이 필요합니다. 또한 데이터의 버전이 충돌하면 애플리케이션 영역에서 이러한 문제를 해결해야 합니다.




(참고)
– Principles of transaction-oriented database recovery : https://dl.acm.org/doi/10.1145/289.291





# Steven

답글 남기기