1. 트랜잭션의 예시
본격적으로 트랜잭션에 대해 알아보기 전에, 흔한 예시를 통해 이해를 쉽게 하고자 합니다.
우선, A가 B에게 100만 원을 송금하는 과정에 대해서 생각해 보겠습니다.A->B로 100만 원을 전송했다는 이야기는, 결국 A는 -1000000원, B는 +1000000원이 되었다는 이야기죠?이를 SQL로 작성해 보면 아래와 같이 작성할 수 있겠네요.
UPDATE account SET balance = balance - 1000000 WHERE id = 'A';
UPDATE account SET balance = balance + 1000000 WHERE id = 'B';
여기서 중요한 것은 '송금'이라는 작업이 성공하려면, 이 두 쿼리가 '모두' 실행이 되어야 합니다.
만약, 1번째 줄의 쿼리만 실행되고 2번째 줄의 쿼리는 실패한다면, A의 100만 원만 사라진 게 되는 거겠죠? 반대의 경우도 이상합니다. 따라서, 결국 이 송금은 둘 다 정상 처리돼야만 성공하는 '단일 작업'입니다.
SQL이 모두 성공을 해야만 정상적인 작업, 이러한 작업을 DB에서는 Transaction이라 부릅니다.
2. 트랜잭션의 정의
- 단일한 논리적인 작업 단위입니다.
- 논리적인 이유로 여러 SQL문들을 단일 작업으로 묶어서 나눠질 수 없게 만든 것이 Transaction입니다.
- Transaction의 SQL문들 중에 일부만 성공해서 DB에 반영되는 일은 일어나지 않습니다.
3. MySQL에서의 사용법
# 트랜잭션을 시작한다
START TRANSACTION;
# 송금 작업(A와 B의 기존 잔고는 200만원 이라고 가정하겠습니다)
UPDATE account SET balance = balance - 1000000 WHERE id = 'A';
UPDATE account SET balance = balance + 1000000 WHERE id = 'B';
# 커밋
# 지금까지 작업한 내용을 DB에 영구적으로 저장한다는 말입니다.
# 또한, Transaction을 종료한다는 뜻도 가지고 있습니다.
COMMIT;
# 추가로, A가 B에게 50만원을 더 송금하는 과정을 진행해 보겠습니다.
START TRANSACTION;
# A의 계좌에서 50만원 차감
UPDATE account SET balance = balance - 500000 WHERE id = 'A';
# A의 계좌를 확인해 보면 총 1500000이 지출되었으므로 500000만원만 남았다고 가정하겠습니다.
SELECT * FROM account // (A -> 500000)
# 롤백
# 지금까지 작업들을 모두 취소하고, Transaction 이전 상태로 되돌린다
# 또한, Transaction을 종료한다는 뜻도 가지고 있습니다.
ROLLBACK;
# 이제, 다시 계좌를 조회해보면 1000000으로 원상복구가 되어있습니다.
SELECT * FROM account // (A -> 1000000)
---
# AUTO COMMIT
# 각각의 SQL문을 자동으로 Transaction 처리 해주는 개념입니다.
# SQL문이 성공적으로 실행하면 자동으로 COMMIT합니다.
# 실행 중에 문제가 있었다면 알아서 ROLLBACK합니다.
# MySQL 에서는 기본값으로 AUTOCOMMIT이 Enabled 되어있습니다.
# 다른 DBMS도 물론 대부분 같은 기능을 지원합니다.
SELECT @@AUTOCOMMIT; // 1 -> True
# AUTOCOMMIT이 활성화된 상태이기 때문에 INSERT문이 실행되면 자동으로 COMMIT하면서 영구저장 됩니다.
INSERT INTO account VALUES ('A', 1000000);
# 이번엔, AUTOCOMMIT을 비활성화 해보겠습니다.
SET AUTOCOMMIT=0;
# 앞으로는 COMMIT이 자동으로 수행되지 않으므로, 특정 작업을 확정하려면 COMMIT, 취소하려면 ROLLBACK을 이용할 수 있습니다.
DELETE FROM account WHERE id = 'A';
ROLLBACK;
# 트랜잭션 시작과 함께, AUTOCOMMIT은 OFF됩니다. COMMIT/ROLLBACK 상태와 함께 Transction이 종료되면
# 원래 설정한 AUTOCOMMIT 상태로 돌아갑니다.
START TRANSACTION;
ROLLBACK;
위에서 코드로 설명해 보았는데, 짧게 다시 Transaction의 사용 패턴에 대해서 말씀드리자면,
- Transaction을 시작한다(AUTOCOMMIT은 비활성화된다)
- SQL작업을 포함한 로직을 수행한다
- 일련의 작업이 문제없이 동작했다면 Transaction을 COMMIT 하고 종료한다
- 문제가 발생했다면, Transaction을 ROLLBACK 하여 되돌린다.
- 3 또는 4의 작업이 끝났다면, AUTOCOMMIT의 설정이 돌아온다
4. ACID
ACID는 트랜잭션의 핵심 속성입니다. 각각 Atomicity, Consistency, Isolation, Durability의 약자인데요, 하나씩 알아봅시다.
Atomicity (원자성)
아까의 송금 작업에서, A의 계좌의 돈이 빠져나가고, B의 계좌에 돈이 들어와야 되는 작업이 모두 성공해야 의미가 있는 작업이라고 말씀드렸는데, 이런 특성을 원자성이라고 합니다.
- ALL or NOTHING (모두 성공하거나, 모두 실패하거나)
- Transaction은 논리적으로 쪼개질 수 없는 작업 단위이기 때문에 내부의 SQL문들이 모두 성공해야 합니다
- 중간에 SQL문이 실패하면 지금까지의 작업을 모두 취소하여 아무 일도 없었던 것처럼 ROLLBACK 합니다
이 부분에서 COMMIT, ROLLABCK이 일어났을 때의 변화를 담당하는 것은 DBMS입니다.
개발자가 담당해야 하는 부분은 언제 COMMIT 하거나, ROLLBACK 할지를 생각해야 합니다. 즉, 트랜잭션의 단위를 어떻게 설정할지 또는 어떤 문제가 발생했을 때 ROLLBACK 할지를 고려해야 한다는 말입니다.
Consistency (일관성)
A가 B에게 송금을 성공적으로 한 뒤에, 추가이체 하는 상황을 생각해 보겠습니다. 이때 SQL문에서 A의 계좌 이상의 금액을 B에게 송금한다면 어떻게 될까요?
CREATE TABLE account (
...
balance INT,
check (balance >= 0)
)
# A의 잔고보다 많은 금액을 이체함
UPDATE account SET balance = balance - 100000000 WHERE id = 'A';
UPDATE account SET balance = balance + 100000000 WHERE id = 'B';
첫 번째 쿼리부터 바로 실패해 버릴 것입니다. 개발자가 설정한 제약조건을 지키지 못하였기 때문입니다. 즉, 일관성을 깨뜨렸어요. 이런 상황에서, 두 번째 쿼리를 실행하는 게 의미가 있을까요? 다시 말해서, 이런 상황에서 트랜잭션을 이어가는 게 의미가 있을까요? 없습니다. 바로 ROLLBACK을 해줘야겠죠?
이렇게, DB의 상태를 일관성 있게 유지시켜 주는 것이 Consistency가 의미하는 바입니다.
- Transaction은 DB 상태를 Consistent 상태에서 또 다른 Consistent 상태로 바꿔줘야 합니다
- Constraints, trigger 등을 통해 DB에 정의된 Rules를 Transaction이 위반했다면 ROLLBACK 해야 합니다
- Transaction이 DB에 정의된 Rule을 위반했는지는 DBMS가 Commit 전에 확인하고 알려줍니다
- 그 외에, Application 관점에서 Transaction이 Consistent 하게 동작하는지는 개발자가 챙겨야 합니다
Isolation (격리성, 고립성)
A가 B에게 100만 원을 이체할 때, 동시에 B도 본인의 계좌에 100만원을 입금하는 상황을 생각해 보겠습니다. (아래는 SQL문이 아닌 이해를 위해 작성한 부분입니다.)
# 자세히 살펴보겠습니다
# A와 B가 모두 200만원을 가지고 있다고 해보겠습니다.
# A가 송금하는 과정입니다. (1번 Transaction 시작)
# 우선 A의 계좌를 조회하고
A.read(balance) => 2000000
# 100만원을 송금하고, 이 결과를 작성하겠습니다.
A.write(balance = 1000000) // 100만원 송금하여 100만원 남음
# 이제, B에 계좌에 100만원을 더해주기 위해 조회가 일어납니다.
B.read(balance) => 2000000
# 이 때, B도 본인의 계좌로 50만원을 입금합니다. (2번 Transaction 시작)
B.read(balance) => 2000000 // 아직 A -> B 송금한 내역이 반영이 되지 않았습니다.
B.write(balance = 2500000) // B가 입금한 내역만 반영이 된 상태입니다.
# 작업이 끝났으므로, 2번 Transaction은 종료되었습니다.
# 이제, 1번 Transaction의 작업인 100만원 입금을 해 주어야 하는데,
# 2번 Transaction이 수행되기 전에, 이미 B.read(balance) => 2000000 라는 값을 읽어왔기 때문에,
# 여기에 100만원을 더해줍니다.
B.write(balance = 3000000)
# B가 입금한 50만원은 사라졌습니다. 즉,
# 여러 Transaction이 함께 동작하면서 이상한 결과가 발생했습니다.
위에서 주목할 부분은 결국 '여러 Transaction이 함께 동작하면서 이상한 결과가 발생한 점'입니다. 이를 위해 필요한 성질이 Isolation입니다.
- 여러 Transaction들이 동시에 실행될 때도 혼자 실행되는 것처럼 동작하게 만듭니다
- Isolation을 엄격하게 구현하면 DB의 Performance가 감소하므로 DBMS는 여러 종류의 Isolation level을 제공합니다
- level을 높이면, 엄격하게 격리를 시켜 다른 트랜잭션의 영향을 받는 경우를 줄일 수 있지만 성능의 저하가 존재합니다
- level을 낮추면, 여러 트랜잭션을 실행시킬 수 있는 동시성이 좋아져 Performance가 좋아지지만 다른 트랜잭션으로부터 영향을 받을 가능성이 커집니다
- 개발자는 Isolation level 중에 어떤 level로 Transaction을 동작시킬지 설정할 수 있습니다
- Concurrency Control의 주된 목표가 Isolation입니다.
Durability(영존성)
UPDATE account SET balance = balance - 1000000 WHERE id = 'A';
UPDATE account SET balance = balance + 1000000 WHERE id = 'B';
처음부터 봤던 이 송금작업이 정상적으로 수행되어 COMMIT이 일어난 뒤에는, DB에 영구적으로 저장됩니다. 이를 Durability라고 합니다.
- COMMIT 된 Transaction은 DB에 영구적으로 저장합니다
- DB System에 문제가 생겨도 COMMIT된 Transaction은 DB에 남아 있습니다
- '영구적으로 저장한다'라는 의미는 일반적으로 비 휘발성 메모리(SSD, HDD,..)에 저장함을 의미합니다
- 기본적으로 Transaction의 Durability는 DBMS가 보장합니다
참고자료
'DB' 카테고리의 다른 글
파티셔닝(Partitioning) 샤딩(Sharding) 레플리케이션(Replication) (1) | 2023.10.19 |
---|