데이터베이스Medium#35
트랜잭션의 ACID 속성을 설명해주세요.
#DB#트랜잭션#ACID#SQL
힌트
원자성, 일관성, 격리성, 지속성의 영어 첫 글자입니다.
정답 및 해설
트랜잭션의 ACID 속성을 설명해주세요.
트랜잭션(Transaction)은 데이터베이스에서 하나의 논리적인 작업 단위를 이루는 연산들의 집합입니다. 계좌 이체처럼 여러 SQL 쿼리가 모두 성공하거나 모두 실패해야 하는 경우에 사용합니다. ACID는 트랜잭션이 안전하게 수행되기 위해 보장해야 하는 네 가지 핵심 속성(Atomicity, Consistency, Isolation, Durability)의 앞글자를 딴 약어입니다.
트랜잭션 기본 개념
트랜잭션의 필요성
-- 계좌 이체 예시: A계좌에서 B계좌로 10만원 이체
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100000 WHERE id = 'A';
-- 만약 여기서 서버가 다운된다면?
UPDATE accounts SET balance = balance + 100000 WHERE id = 'B';
COMMIT;
-- 트랜잭션 없이: A계좌에서만 돈이 빠지고 B계좌에는 입금 안 됨!
-- 트랜잭션 사용: 두 쿼리 모두 성공하거나 모두 없었던 일로 처리
트랜잭션 상태
Active(실행 중)
↓ (정상 완료)
Partially Committed(부분 완료)
↓ (모든 변경사항 디스크에 저장)
Committed(커밋됨) ← 영구적으로 반영
Active(실행 중)
↓ (오류 발생)
Failed(실패)
↓ (변경사항 취소)
Aborted(중단됨) ← 트랜잭션 이전 상태로 복원
A - 원자성 (Atomicity)
정의
트랜잭션의 모든 연산은 완전히 실행되거나 전혀 실행되지 않아야 합니다. 중간 상태가 존재하지 않습니다 (All or Nothing).
예시
-- 계좌 이체 트랜잭션
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100000 WHERE account_id = 'A';
-- ↑ 이 쿼리는 성공
-- 만약 여기서 오류 발생 (예: B 계좌가 존재하지 않음)
UPDATE accounts SET balance = balance + 100000 WHERE account_id = 'B';
-- ↑ 이 쿼리가 실패
-- 원자성 보장: 첫 번째 UPDATE도 롤백되어 A 계좌는 원래 금액 유지
ROLLBACK;
-- → A 계좌에서 돈이 빠지지 않음
구현 원리: 롤백 (Rollback)
데이터베이스는 트랜잭션 시작 전 상태를 Undo 로그에 기록합니다. 실패 시 Undo 로그를 사용하여 원래 상태로 복원합니다.
Undo 로그:
[T1 시작]
[T1: accounts(id=A).balance: 500000 → 400000] ← 변경 전 값 기록
[T1: accounts(id=B).balance: 200000 → 300000]
실패 시:
accounts(id=A).balance = 500000 (원복)
accounts(id=B).balance = 200000 (원복)
# 애플리케이션 수준에서의 원자성 구현 (Python + SQLAlchemy)
from sqlalchemy.orm import Session
def transfer_money(db: Session, from_id: int, to_id: int, amount: int):
try:
# 트랜잭션 시작 (자동)
sender = db.query(Account).filter(Account.id == from_id).first()
receiver = db.query(Account).filter(Account.id == to_id).first()
if sender.balance < amount:
raise ValueError("잔액 부족")
sender.balance -= amount # 출금
receiver.balance += amount # 입금
db.commit() # 모두 성공 시 커밋
print("이체 완료")
except Exception as e:
db.rollback() # 실패 시 모두 취소 (원자성!)
print(f"이체 실패, 롤백 완료: {e}")
raise
C - 일관성 (Consistency)
정의
트랜잭션 실행 전후로 데이터베이스가 미리 정의된 규칙과 제약 조건을 항상 만족해야 합니다. 트랜잭션이 DB를 유효한 상태에서 다른 유효한 상태로 변경해야 합니다.
예시
-- 일관성 제약 조건 예시
CREATE TABLE accounts (
id INT PRIMARY KEY,
balance INT NOT NULL CHECK (balance >= 0), -- 잔액은 0 이상
owner VARCHAR(100) NOT NULL
);
-- 일관성 위반 시도
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 1000000 -- 잔액보다 큰 금액 출금 시도
WHERE id = 1 AND balance = 500000;
-- CHECK 제약 조건 위반: balance가 음수가 됨 → 트랜잭션 실패, 롤백
ROLLBACK;
비즈니스 규칙 일관성
-- 주문-재고 일관성
BEGIN TRANSACTION;
-- 재고 감소
UPDATE products SET stock = stock - 1 WHERE id = 101 AND stock > 0;
-- 주문 생성
INSERT INTO orders (product_id, quantity, status)
VALUES (101, 1, 'CONFIRMED');
-- 재고가 0인데 주문이 생성되는 모순 상태가 없어야 함
COMMIT;
일관성의 책임
- DBMS: CHECK, UNIQUE, NOT NULL, FOREIGN KEY 등 제약 조건 강제
- 개발자: 비즈니스 로직상의 일관성 규칙 구현
I - 격리성 (Isolation)
정의
동시에 실행되는 여러 트랜잭션은 서로 영향을 주지 않아야 합니다. 각 트랜잭션은 마치 순차적으로 혼자 실행되는 것처럼 동작해야 합니다.
격리성 부재 시 발생하는 문제들
1. Dirty Read (더티 리드)
-- 트랜잭션 T1
BEGIN;
UPDATE accounts SET balance = 500000 WHERE id = 1;
-- 아직 커밋하지 않음
-- 트랜잭션 T2 (동시 실행)
SELECT balance FROM accounts WHERE id = 1;
-- → 500000 읽음 (T1이 커밋 전인데도!)
-- T1이 롤백하면 T2는 존재하지 않는 값을 읽은 것
ROLLBACK; -- T1 롤백
2. Non-Repeatable Read (반복 불가능한 읽기)
-- 트랜잭션 T1
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 결과: 300000
-- 트랜잭션 T2 (사이에 실행)
UPDATE accounts SET balance = 500000 WHERE id = 1;
COMMIT;
-- T1 다시 읽기
SELECT balance FROM accounts WHERE id = 1; -- 결과: 500000 (다름!)
-- 같은 트랜잭션에서 같은 쿼리가 다른 결과 반환
3. Phantom Read (팬텀 리드)
-- 트랜잭션 T1
BEGIN;
SELECT COUNT(*) FROM orders WHERE amount > 10000; -- 결과: 5개
-- 트랜잭션 T2
INSERT INTO orders (amount) VALUES (50000);
COMMIT;
-- T1 다시 집계
SELECT COUNT(*) FROM orders WHERE amount > 10000; -- 결과: 6개 (유령 레코드 등장!)
격리 수준 (Isolation Levels)
-- 격리 수준 설정
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 1. READ UNCOMMITTED (가장 낮은 격리)
-- Dirty Read 허용, Non-Repeatable Read 허용, Phantom Read 허용
-- 성능 최우선, 정확성 포기
-- 2. READ COMMITTED (기본값: PostgreSQL, Oracle, SQL Server)
-- Dirty Read 방지, Non-Repeatable Read 허용, Phantom Read 허용
-- 커밋된 데이터만 읽기 가능
-- 3. REPEATABLE READ (기본값: MySQL InnoDB)
-- Dirty Read 방지, Non-Repeatable Read 방지, Phantom Read 허용
-- 트랜잭션 중 같은 행은 항상 같은 값
-- 4. SERIALIZABLE (가장 높은 격리)
-- 모든 이상 현상 방지
-- 성능 저하 가장 큼 (트랜잭션이 순차 실행처럼 동작)
구현 방법: MVCC (Multi-Version Concurrency Control)
-- PostgreSQL의 MVCC 동작 방식 (개념)
-- 각 행에 버전 정보 저장
-- T1 시작 (transaction_id = 100)
BEGIN;
-- account id=1: balance=300000, xmin=50, xmax=null (현재 유효)
-- T2가 수정 (transaction_id = 101)
UPDATE accounts SET balance = 500000 WHERE id = 1;
-- 기존 행: balance=300000, xmin=50, xmax=101 (T101에 의해 만료)
-- 새 행: balance=500000, xmin=101, xmax=null
-- T1은 여전히 transaction_id=50이 만든 행을 봄 (스냅샷)
SELECT balance FROM accounts WHERE id = 1; -- 300000 (자신의 스냅샷)
COMMIT;
D - 지속성 (Durability)
정의
커밋된 트랜잭션의 결과는 영구적으로 저장되어야 합니다. 시스템 장애(전원 꺼짐, 서버 다운 등)가 발생해도 커밋된 데이터는 손실되지 않습니다.
구현 원리: WAL (Write-Ahead Logging)
WAL 원리: 데이터를 실제로 변경하기 전에 로그에 먼저 기록
1. 트랜잭션 시작
2. 변경 내용을 WAL(트랜잭션 로그)에 기록
3. 로그가 디스크에 flush되면
4. 실제 데이터 페이지 변경
5. COMMIT 완료
장애 복구:
- 재시작 시 WAL 로그를 읽어 커밋된 트랜잭션 재실행 (Redo)
- 미완료 트랜잭션 롤백 (Undo)
-- PostgreSQL WAL 설정 예시
-- postgresql.conf
-- wal_level = replica
-- fsync = on ← 커밋 시 반드시 디스크 동기화
-- synchronous_commit = on ← 동기식 커밋으로 지속성 보장
-- 지속성 확인
BEGIN;
INSERT INTO critical_data (value) VALUES ('중요한 데이터');
COMMIT;
-- 이 시점에서 전원이 꺼져도 데이터는 디스크에 영구 보존
fsync와 지속성 트레이드오프
fsync = on (기본):
- 커밋마다 OS 버퍼 → 디스크 동기화
- 지속성 완전 보장
- 성능 저하 (디스크 I/O 대기)
fsync = off (비권장):
- OS 버퍼에만 쓰고 리턴
- 서버 충돌 시 데이터 손실 가능
- 성능 향상 (벌크 로드 시 임시 사용)
ACID 속성 종합 예시
# Django ORM을 이용한 트랜잭션 예시
from django.db import transaction, models
class Account(models.Model):
balance = models.IntegerField(default=0)
@transaction.atomic # 트랜잭션 보장
def transfer(sender_id, receiver_id, amount):
"""
A(원자성): sender와 receiver 업데이트가 모두 성공하거나 모두 실패
C(일관성): balance >= 0 조건 항상 유지
I(격리성): 다른 동시 트랜잭션으로부터 격리
D(지속성): commit 후 서버 재시작해도 데이터 유지
"""
# select_for_update: 행 잠금으로 격리성 강화
sender = Account.objects.select_for_update().get(id=sender_id)
receiver = Account.objects.select_for_update().get(id=receiver_id)
if sender.balance < amount:
raise ValueError("잔액 부족") # 일관성 규칙 위반
sender.balance -= amount
receiver.balance += amount
sender.save()
receiver.save()
# @transaction.atomic이 자동으로 COMMIT 또는 예외 시 ROLLBACK
ACID 속성 비교 요약표
| 속성 | 한국어 | 핵심 의미 | 보장하는 것 | 구현 방법 |
|---|---|---|---|---|
| Atomicity | 원자성 | All or Nothing | 부분 실패 없음 | Undo 로그, Rollback |
| Consistency | 일관성 | 유효한 상태만 허용 | 제약 조건 위반 없음 | CHECK, FK, 비즈니스 로직 |
| Isolation | 격리성 | 트랜잭션 간 간섭 없음 | 동시성 이상 현상 방지 | MVCC, 잠금(Lock) |
| Durability | 지속성 | 커밋은 영구 보존 | 장애 후에도 데이터 유지 | WAL, fsync, 복제 |
| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| READ UNCOMMITTED | 허용 | 허용 | 허용 |
| READ COMMITTED | 방지 | 허용 | 허용 |
| REPEATABLE READ | 방지 | 방지 | 허용 |
| SERIALIZABLE | 방지 | 방지 | 방지 |