Transaction
데이터베이스 트랜잭션은 여러 연산을 하나의 논리적 작업 단위로 묶어 데이터 일관성을 보장하는 핵심 메커니즘이다.
트랜잭션이 중요한 이유는 단순하다. 은행 계좌 이체를 생각해보면, A 계좌에서 출금은 성공했는데 B 계좌로 입금이 실패한다면 돈이 사라지는 심각한 문제가 발생한다.
당연히 실무에서도 밥 먹듯이 사용되고, 이런저런 사고의 원흉이 되기도 한다. 잘 알고 쓰는 것이 정말 중요하다.
ACID
ACID 는 데이터베이스 트랜잭션이 안전하게 수행되기 위해 보장해야하는 네 가지 속성의 약자이다.
Atomicity
원자성은 트랜잭션 내의 모든 연산이 전부 성공하거나 전부 실패해야 함을 의미한다. 부분적인 성공은 허용되지 않는다.
구현방식: Undo Log
-
데이터 변경 전 값을 Undo Log 에 기록
-
롤백 시 Undo Log 를 사용하여 이전 상태로 복원
-
트랜잭션이 커밋되면 Undo Log 는 일정 시간 후 정리
BEGIN;
UPDATE accounts SET balance = balance - 10000 WHERE id = 1; -- A 계좌 출금
UPDATE accounts SET balance = balance + 10000 WHERE id = 2; -- B 계좌 입금
COMMIT;
만약 두 번째 UPDATE에서 오류가 발생하면:
-
첫 번째 UPDATE도 롤백됨
-
Undo Log에 저장된 원래 balance 값으로 복원
-
돈이 사라지거나 중복 생성되지 않음
Consistency
일관성은 트랜잭션 실행 전후로 데이터베이스가 일관된 상태를 유지해야 함을 의미한다.
솔직히 처음 봤을 때 너무 당연하게 들려서 오히려 기억 안난적도 많다.
근데 별 다른건 아니고, 모든 데이터 무결성 제약 조건들이 트랜잭션 후에도 만족되어야 한다는 뜻이다.
예를 들면 이런 것들:
-
NOT NULL: 필수 필드는 반드시 값이 있어야 함
-
UNIQUE: 중복된 값이 존재하면 안 됨
-
FOREIGN KEY: 참조 무결성 유지
-
CHECK: 특정 조건 만족 (예: age >= 0, balance >= 0)
Isolation
격리성은 동시에 실행되는 여러 트랜잭션이 서로 영향을 주지 않고 독립적으로 실행되는 것처럼 보여야 함을 의미한다.
완벽한 격리는 트랜잭션들이 순차적으로 실행된 것과 동일한 결과를 보장한다.
하지만 완벽한 격리는 성능 저하를 초래하므로, 실무에서는 다양한 격리 수준(Isolation Level)을 사용해 성능과 일관성 사이의 트레이드오프를 조절한다.
Durability
지속성은 트랜잭션이 성공적으로 커밋되면 시스템 장애가 발생해도 결과가 영구적으로 데이터베이스에 보존되어야 함을 의미한다.
WAL (Write-Ahead Log)
-
트랜잭션 시작
-
변경 내용을 WAL에 먼저 기록 (디스크에 fsync)
-
메모리의 Buffer Pool에 변경 내용 반영
-
백그라운드에서 주기적으로 데이터 파일에 반영 (Checkpoint)
이상 현상
동시성 제어가 불완전할 때 발생할 수 있는 이상현상들이 있다. 주로 세 가지가 유명하다.
Dirty Read
커밋되지 않은 데이터를 다른 트랜잭션이 읽는 현상이다. Read-Committed 이상에서는 기본적으로 방지 된다.
Non-Repeatable Read
같은 트랜잭션 내에서 동일한 행을 두 번 읽었는데 값이 달라지는 현상이다. 첫 번째 Read 와 두 번째 Read 사이에서 다른 트랜잭션이 Commit 되면서 값이 바뀌는 경우에 해당한다.
Repeatable Read 부터는 방지된다.
Phantom Read
같은 조건으로 조회했을때 없던 행이 생기는 현상이다. Non-Repeatable Read 와의 차이점은, Non-Repeatable Read 는 값이 달라지는 것이고, Phantom Read 는 없던 행이 생기는 현상이라는 점이다.
참고로 Phantom Read 는 Repeatable Read 에서도 발생할 수 있다. (MySQL InnoDB 는 Gap Lock 으로 방지)
격리 수준 (Isolation Level)
ANSI SQL 표준은 위 세 가지 이상 현상을 기준으로 격리 수준을 4단계로 정의한다. 수준이 높아질수록 일관성은 강해지고 성능은 떨어진다.
| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| Read Uncommitted | 가능 | 가능 | 가능 |
| Read Committed | 차단 | 가능 | 가능 |
| Repeatable Read | 차단 | 차단 | 가능 |
| Serializable | 차단 | 차단 | 차단 |
각 수준의 의미는 단순하다.
- Read Uncommitted: 커밋되지 않은 데이터도 읽는다. 거의 쓰이지 않는다
- Read Committed: 커밋된 데이터만 읽는다. 대부분 RDBMS의 기본값(PostgreSQL, Oracle, SQL Server)
- Repeatable Read: 트랜잭션 내내 같은 행은 같은 값으로 보인다. MySQL InnoDB의 기본값
- Serializable: 동시 트랜잭션이 순차 실행한 것과 동일한 결과를 보장한다
표는 ANSI 표준일 뿐, 실제 DB 구현은 표준과 다른 경우가 많다.
- MySQL InnoDB의 Repeatable Read는 Gap Lock으로 Phantom까지 막는다 (표준보다 강함)
- PostgreSQL의 Repeatable Read는 Snapshot Isolation으로 동작한다. 표준 RR과 이름은 같지만 메커니즘이 다르며, Phantom도 자연스럽게 차단된다
- PostgreSQL의 Serializable은 SSI(Serializable Snapshot Isolation)로, 락이 아닌 충돌 감지로 직렬화를 보장한다
표준은 어디까지나 명세이고, 실제로는 DB가 어떻게 구현했는지를 봐야 한다. 같은 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ라도 MySQL과 PostgreSQL의 거동은 미묘하게 다르다.
격리 수준 선택 기준
- 대부분의 OLTP 서비스는 Read Committed로 충분하다. 동시성이 가장 높다
- 보고서·정합성이 중요한 통계 조회는 Repeatable Read (한 트랜잭션 안에서 일관된 스냅샷)
- 잔액 이체, 재고 차감처럼 직렬화가 필요한 핵심 로직은 Serializable 또는 명시적
SELECT FOR UPDATE
격리 수준을 무작정 올리면 동시성은 줄고 데드락 가능성이 커진다. 필요한 트랜잭션만 선택적으로 끌어올리는 것이 일반적이다.