트랜잭션
트랜잭션(transaction)은 원자성이 있는(atomic, 쪼개지지 않는) 데이터베이스 내 일련의 동작들을 말한다.
데이터베이스 내 동작들이라고 한다면, 당연히 데이터의 읽고 쓰기 과정을 말할 것이다.
예를 들어, 어떤 공동 계좌가 하나 있다고 생각을 해보자.
이 계좌에는 1만원의 돈이 들어있다.
그리고 '공동' 계좌이기 때문에, 계좌에서 돈을 넣고 빼는 행위는 여러 사용자에 의해서 병행으로 발생할 수 있다.
만약 내가 이 계좌에서 1000원을 출금을 할 때 필요한 과정을 생각해보면 다음과 같이 생각할 수 있다.
1. 데이터베이스에 접근인가를 받아 접근한다. (은행 계좌라면 본인 인증은 필수 일 것이다.)
2. 계좌의 잔액을 조회한다. (10000원 read)
3. 만약 계좌의 잔액이 1000원보다 많다면, 돈을 출금한다.
4. 계좌의 잔액을 1000원 감소시킨다. (9000원 write)
즉, 이 과정에서 '데이터베이스에 가해지는 일련의 동작' 을 본다면 '돈을 출금' 하는 하나의 행위를 위해서는
계좌의 잔액을 조회하고(read) -> 출금한 뒤 남은 잔액으로 갱신하는(wirte) 행위가 함께 일어나야 한다.
만약 지금 이 계좌가 나의 개인 계좌였다면 아무런 문제가 없었을 것이다.
그런데, 이 계좌는 '공동' 계좌라서 여러 사용자가 병행적으로 접근할 수 있다.
(이때 동시에 접근할 필요는 없다. 지난 글에서 정리했듯, 동시성과 병행성은 다르다.)
따라서 또 다른 사용자가 이 계좌에 병행적으로 접근해서 돈을 인출하려고 해본다고 하자.
그리고 이 사용자는 6000원의 돈을 출금하려고 한다고 해보자.
이때 만약 '돈을 출금' 하는 과정에서 데이터베이스에 가해지는 일련의 동작에 원자성이 보장되지 않으면 다음과 같은 현상이 발생할 수 있다.
초기 계좌 잔액 : 10,000원
1. A가 계좌의 잔액을 조회한다. (read) => 10,000원 확인
2. B가 계좌의 잔액을 조회한다. (read) => 10,000원 확인
3. A가 1,000원을 인출하고, 잔액을 갱신한다. (write) => 9,000원으로 갱신
4. B가 6,000원을 인출하고, 잔액을 갱신한다. (write) => 4,000원으로 갱신
최종 계좌 잔액 : 4,000원
두 사용자가 데이터베이스에 접근한 뒤 잔액을 조회했는데, 마침 A가 잔액을 조회하고 출금하는 사이에 B가 잔액을 조회한 상황이다.
B는 A가 인출하기 전에 조회했으므로 10,000원의 잔액을 조회했고, 자신이 인출한 뒤 남은 잔액을 이 10,000원의 잔액을 기준으로 계산할 것이다.
따라서 A가 1,000원을 인출해서 계좌에는 9,000원의 잔액이 남아있음에도 불구하고, B는 10,000원을 기준으로 잔액을 계산한 4,000원을 데이터베이스에 쓰게 되므로, 실제로 저장되어야 하는 3,000원보다 많은 금액이 잔액으로 계산되게 된다.
따라서 이렇게 병렬적으로 요청이 들어왔을 때, 요청을 처리하는 과정에서 read, write 명령의 실행 순서를 프로그래머가 직접 정해서 이런 문제가 발생되지 않도록 해야한다.
(마치 운영 체제에서 프로그램을 실행할 때 프로세스가 공유 자원에 순차적으로 접근하도록 락을 걸어두는 것과 같다.)
즉, 한 사용자가 실행하는 조회 - 갱신하는 과정을 하나로 묶어서 반드시 같이 일어나게끔 해줘야 한다.
그리고 데이터베이스에서 이렇게 일련의 과정을 하나로 묶는 것을 트랜잭션이라고 한다.
(락을 걸어서 다른 사용자는 트랜잭션이 진행되는 동안 접근할 수 없도록 DB에서 막는 것)
이렇게 트랜잭션을 통해서 병렬적으로 들어오는 요청을 제대로 처리하면, 데이터베이스의 상태를 일관되도록 유지할 수 있다.
(consistent state)
위 예제에서는 트랜잭션을 걸어서 B가 접근했을 때는 잔액이 9,000원으로 보이도록 데이터의 일관성을 유지하는 것이다.
ACID
DB에서 중요한 용어로 ACID 라는 용어가 있다.
ACID는 Atomic, Consistent, Isolated, Durable 의 줄임말로
Atomic : 하나의 동작을 수행할 때, 그 세부 동작들이 쪼개지지 않아야 하는 것
Consistent : 항상 데이터베이스에 대한 상태를 동일한 상태로 인식해야 하는 것
(위 예시에서는 A가 1000원을 인출한 시점에서 9,000원으로 인식하지만 B는 여전히 10,000원으로 인식하므로 동일한 상태로 인식하지 않고 있다.)
Isolated : 하나의 프로세스에서 일어나는 일이 다른 프로세스에 영향을 주지 않아야 하는 것
Durable : 만약 트랜잭션을 수행하는 중 장애가 났다면, 다시 복구되었을 때 트랜잭션이 처음부터 다시 실행되어 원래 기능을 마칠 수 있어야 하는 것
그리고 이러한 ACID 성질을 지키는 것은 DB를 사용하는 관리자의 책임이기 때문에,
관리자는 데이터베이스를 사용할 때 반드시 integrity constratints (무결성 제약조건) 을 명시해주어야 한다.
병렬적으로 들어오는 트랜잭션 스케줄링
a, b 라는 2개의 동일한 작업에 대한 트랜잭션 요청이 병렬적으로 오는 경우, 적잘하게 스케줄링을 해주어야 한다.
만약 {T1, T2, ..., Tn} 의 트랜잭션 요청이 오는 경우, DBMS는 이 순서대로 T1, ..., Tn 의 트랜잭션이 실행되도록 보장해준다.
이를 위해, 트랜잭션은 어떤 오브젝트를 읽거나 쓰기 전에 lock을 걸어두도록 요청한 뒤 기다리면, DBMS가 lock을 걸어준 뒤에 트랜잭션이 실행되고, 트랜잭션이 끝난 뒤 락이 해제된다.
(이를 Strict 2PL locking protocol 이라고 부른다.)
예를 들어, 어떤 Ti 트랜잭션이 X라는 대상에 대해 쓰기 작업을 하고 있고, 이 작업이 X를 읽는 Tj 트랜잭션에 영향을 준다면,
Tj 트랜잭션은 Ti 트랜잭션이 끝날 때까지 X에 걸린 락 때문에 접근할 수 없게되어 반드시 Ti 가 처리된 이후에 Tj가 처리된다.
만약 Tj가 이미 Y에 락을 걸어두었는데, Ti가 나중에 Y에 락을 걸어두도록 요청을 하면 데드락이 발생하고, Ti, Tj는 모두 아무 작업도 할 수 없게 될 수도 있다.
더 자세한 스케줄링 프로토콜은 뒤에서 자세히 정리할 예정이다.
원자성 보장
DBMS는 트랜잭션을 통해 원자성(atomicity)을 보장하여 어떤 일련의 작업들이 모두 성공하거나 모두 실패하도록 할 수 있다.
(즉, 일부만 성공하고 중간에 시스템에 충돌이 발생하면, 모두 실패하도록 처리한다.)
이를 위해서는 트랜잭션 내에서 액션을 수행하기 전에 log(history)를 남겨야 한다.
트랜잭션 안에서 '나 지금 쓴다.', '나 지금 읽는다' 와 같이 모든 행위를 하기 전에 기록을 남기는 것이다.
이를 통해 만약 트랜잭션의 동작 중간에 문제가 생기더라도 문제가 발생한 지점부터 이어서 진행함으로써 문제를 복구할 수 있다.
만약 데이터베이스에 액션을 수행한 후에 로그를 남긴다고 한다면, 액션이 수행된 후, 로그를 남기기 전에 장애가 발생한 경우, 해당 액션은 되돌릴 수 없어 원자성을 보장하는데 문제가 생길 것이다.
예를 들어 내가 '나 지금 읽는다' 라고 로그를 남겼는데, 읽기를 하지 못하고 장애가 발생했다면, 로그를 기반으로 지금까지 수행한 모든 작업을 되돌리고, 나중에 시스템이 복구 되었을 때, 로그를 보고 기존에 하던 수행을 쭉 이어나간다. (따라서 이를 통해 durable을 보장할 수 있다.)
그리고 이 프로토콜을 가리켜 WAL(Write-Ahead Log, 로그 우선 기록) protocol 이라고 부른다.
'CS > 기초데이터베이스' 카테고리의 다른 글
[데이터베이스] 6. 개념적 데이터베이스 설계 (0) | 2024.10.14 |
---|---|
[데이터베이스] 5. 다양한 관계 (동일 엔티티 셋 내 관계, 약개체, ISA 계층, 집단화) (0) | 2024.10.14 |
[데이터베이스] 4. Key Constraints, Participation Constraints (0) | 2024.10.11 |
[데이터베이스] 3. ER-Model (Entity, Relationship, Key) (0) | 2024.10.08 |
[데이터베이스] 1. 데이터베이스와 DBMS 개념 (0) | 2024.09.28 |