한 줄 요약: ACID 는 트랜잭션이 지켜야 할 네 가지 계약임. InnoDB 는 Atomicity 를 undo log(롤백), Isolation 을 MVCC + lock(스냅샷 읽기), Durability 를 redo log(WAL) 로 구현함. Consistency 는 DB 제약 조건과 애플리케이션 불변식이 함께 지켜야 함. 네 속성이 함께 동작해야 비로소 트랜잭션이 안전하며, 어느 하나라도 설정이나 설계로 약화하면 데이터 정합성이 깨질 수 있음.


개요

트랜잭션은 하나의 논리적 작업 단위임. “이체” 처럼 여러 DML 이 묶여 있을 때, 중간에 실패하면 아무 일도 없었던 것처럼 되돌려야 하고, 성공하면 어떤 장애가 와도 데이터가 살아있어야 함. ACID 는 이 두 가지 보장을 네 속성으로 구체화한 계약임.

속성보장 내용주요 구현 / 책임
Atomicity (원자성)트랜잭션 전체가 적용되거나 전혀 적용되지 않음undo log — ROLLBACK 시 이전 이미지로 복원
Consistency (일관성)트랜잭션 전후 모두 데이터 무결성 규칙을 만족DB 제약 + 애플리케이션 비즈니스 규칙
Isolation (격리성)동시 실행 트랜잭션이 서로의 중간 상태를 보지 않음MVCC(스냅샷 읽기) + row-level lock
Durability (내구성)커밋된 트랜잭션은 장애 후에도 유지됨redo log — WAL 원칙으로 크래시 복구

결론

왜 이런 구현인가

질문답변
왜 롤백에 undo log 가 필요한가DML 은 buffer pool 의 페이지를 직접 수정함. 수정 전 이미지를 undo log 에 남겨두지 않으면 ROLLBACK 시 원래 값을 복원할 방법이 없음.
왜 Consistency 는 “DB 만” 책임지지 않는가NOT NULL · FK · CHECK 는 DB 가 강제하지만, “잔액이 음수가 되면 안 된다” 같은 비즈니스 규칙은 애플리케이션이 직접 검증해야 함. DB 제약만으로는 부족함.
왜 SELECT 가 UPDATE 를 막지 않는가 (MVCC)undo log 가 수정 전 버전을 들고 있어, SELECT 는 잠금 없이 자신의 스냅샷 시점에 맞는 버전을 읽을 수 있음. SELECT 와 UPDATE 가 독립적으로 동작해 처리량이 유지됨.
왜 커밋 후에도 데이터가 디스크에 없을 수 있나성능을 위해 데이터 페이지는 buffer pool 에서 나중에 flush 됨. 단, redo log 는 커밋 시 먼저 디스크에 기록돼 크래시 후 재적용으로 복구 가능함.

운영 시 기억할 것

  • innodb_flush_log_at_trx_commit=1 유지 — 기본값이자 Durability 완전 보장 설정임. 2·0 으로 낮추면 OS/mysqld 크래시 시 커밋된 트랜잭션도 잃을 수 있음.
  • 롱 트랜잭션은 undo 회수를 지연시킴 — 오래 열린 consistent read 가 있으면 과거 버전을 지워야 하는 Purge 가 늦어져 undo tablespace 사용량과 MVCC 비용이 늘 수 있음.
  • CHECK 제약은 8.0.16 부터 강제됨 — 이전 버전에서는 파서가 통과시키지만 강제하지 않았음. 업그레이드 전 기존 데이터의 위반 여부를 확인해야 함.
  • REPEATABLE READ 기본 격리 수준 — InnoDB 기본값. 첫 번째 SELECT 시점에 스냅샷이 생성돼 같은 트랜잭션 안에서 항상 일관된 뷰를 제공함. 격리 수준별 동작 차이는 별도 글에서 다룸.

상세

A — Atomicity: undo log 와 ROLLBACK

트랜잭션 안의 DML 은 buffer pool 페이지를 직접 수정함. 각 수정 시 수정 전 이미지(before image) 가 undo log 에 기록됨. ROLLBACK 이 발생하면 InnoDB 는 undo log 를 역순으로 따라가며 모든 변경을 원래 값으로 되돌림.

크래시 복구에서도 같은 원리가 적용됨. redo log 로 커밋된 변경을 재적용(REDO phase)한 뒤, undo log 로 미완료 트랜잭션을 되돌림(UNDO phase).

SAVEPOINT 를 사용하면 트랜잭션 전체가 아닌 특정 지점까지만 롤백할 수 있음.

START TRANSACTION;
SAVEPOINT sp1;
-- sp1 이후 작업만 선택적으로 취소 가능
ROLLBACK TO sp1;   -- sp1 이전 변경은 유지
COMMIT;

undo log 는 ROLLBACK 외에도 MVCC 의 재료 로 쓰임. 다른 트랜잭션이 이전 버전의 행을 읽을 때 undo log record 에서 수정 전 데이터를 가져와 consistent read 를 구성함.


C — Consistency: DB 제약과 애플리케이션 책임

InnoDB 가 강제하는 제약은 다음과 같음.

제약 종류강제 시점위반 시 오류 번호
NOT NULLINSERT / UPDATE 시1048
UNIQUEINSERT / UPDATE 시1062
FOREIGN KEYINSERT / UPDATE / DELETE 시1452 / 1451
CHECK (8.0.16+)INSERT / UPDATE 시3819

제약이 트랜잭션 안에서 위반되면 해당 DML 만 실패하고 트랜잭션은 열린 채로 유지됨. ROLLBACK 은 자동으로 일어나지 않으므로 애플리케이션이 명시적으로 처리해야 함.

MySQL 8.0.16 이전의 CHECK 는 선언해도 실제로 강제되지 않았으므로 무결성 보장 효과가 없었음. 8.0.16 이상부터는 CHECK 가 실제 제약으로 동작해 위반 INSERT / UPDATE 가 실패함. 따라서 예전 버전에서 CHECK 를 선언해 둔 테이블이라도 기존 데이터가 조건을 만족한다고 가정하면 안 됨.

Consistency 는 DB 제약만으로 완성되지 않음. “이체 후 두 계좌 잔액의 합이 변하지 않아야 한다”는 비즈니스 규칙은 DB 가 모름 — 애플리케이션이 트랜잭션 안에서 직접 검증해야 함.


I — Isolation: MVCC + lock

MVCC(Multi-Version Concurrency Control) 는 같은 행의 여러 버전을 동시에 유지하는 방식임. SELECT 는 자신의 스냅샷 시점에 맞는 버전을 잠금 없이 읽고, UPDATE 는 최신 버전을 잠가서 수정함. 덕분에 SELECT 와 UPDATE 가 서로를 차단하지 않음.

InnoDB 의 기본 격리 수준은 REPEATABLE READ 임. 트랜잭션 안에서 첫 번째 consistent read(잠금 없는 SELECT) 가 실행되는 시점에 스냅샷이 생성됨. 이후 다른 트랜잭션이 변경하고 커밋해도 같은 트랜잭션 안에서는 스냅샷 시점 데이터를 계속 봄.

sequenceDiagram
    participant T1 as 트랜잭션 1
    participant DB as InnoDB
    participant T2 as 트랜잭션 2

    T1->>DB: START TRANSACTION
    T1->>DB: SELECT balance (→ 70, 스냅샷 생성)
    T2->>DB: UPDATE balance = 999
    T2->>DB: COMMIT
    T1->>DB: SELECT balance (→ 70, 스냅샷 유지)
    T1->>DB: COMMIT

스냅샷 읽기(consistent read)와 달리 SELECT ... FOR UPDATE / FOR SHARE, UPDATE, DELETEcurrent read 로, 항상 최신 커밋 버전을 읽고 행에 잠금을 걸음.

격리 수준(REPEATABLE READ / READ COMMITTED / SERIALIZABLE 등)에 따라 스냅샷 생성 시점과 잠금 범위가 달라짐.


D — Durability: redo log 와 WAL

WAL(Write-Ahead Logging) 원칙: 데이터 페이지를 디스크에 쓰기 전에 redo log 를 먼저 디스크에 기록함. 커밋 시 buffer pool 의 dirty page 가 아직 .ibd 파일에 없어도, redo log 가 있으면 크래시 후 재적용으로 복구 가능함.

redo log 가 디스크에 내려가는 시점은 innodb_flush_log_at_trx_commit 이 결정함.

동작크래시 시 손실 가능
1 (기본값)커밋마다 write + flush정상 fsync 기준 없음
2커밋마다 OS 버퍼에 write, 1초마다 flushOS 크래시 시 최대 1초
01초마다 write + flushmysqld/OS 크래시 시 최대 1초

스토리지나 OS 가 fsync() 완료를 거짓으로 보고하면 1 에서도 하드웨어 수준 내구성은 깨질 수 있음. MySQL 설정 관점에서는 1 이 full ACID compliance 에 필요한 기본값임.

redo log 내부에서는 변경 record 가 LSN 증가 순서로 append 되고, checkpoint 가 진행되면서 오래된 redo 가 잘려 나감.


테스트

테스트 환경: MySQL 8.4.8 (mysql:8.4 이미지, 2026-05-29 직접 확인)

SELECT VERSION() AS mysql_version,
       @@transaction_isolation AS isolation,
       @@innodb_flush_log_at_trx_commit AS flush_log_at_trx_commit;
mysql_version  isolation        flush_log_at_trx_commit
8.4.8          REPEATABLE-READ  1
-- 사전 준비
CREATE TABLE accounts (
  id INT PRIMARY KEY,
  name VARCHAR(20),
  balance INT NOT NULL,
  CONSTRAINT chk_balance CHECK (balance >= 0)
);
CREATE TABLE transfers (
  id INT AUTO_INCREMENT PRIMARY KEY,
  from_id INT, to_id INT, amount INT,
  FOREIGN KEY (from_id) REFERENCES accounts(id),
  FOREIGN KEY (to_id)   REFERENCES accounts(id)
);
INSERT INTO accounts VALUES (1, 'Alice', 100), (2, 'Bob', 50);

1. Atomicity — SAVEPOINT 부분 롤백

START TRANSACTION;
UPDATE accounts SET balance = balance - 30 WHERE id = 1;  -- Alice: 100 → 70
SAVEPOINT sp1;
UPDATE accounts SET balance = balance + 30 WHERE id = 2;  -- Bob: 50 → 80
SELECT id, name, balance FROM accounts;
id  name   balance
1   Alice  70
2   Bob    80
ROLLBACK TO sp1;   -- Bob 변경만 취소, Alice 변경은 유지
SELECT id, name, balance FROM accounts;
id  name   balance
1   Alice  70
2   Bob    50
COMMIT;
SELECT id, name, balance FROM accounts;
id  name   balance
1   Alice  70
2   Bob    50

ROLLBACK TO sp1 은 sp1 이후의 Bob 변경만 되돌리고 Alice 변경은 유지함. COMMIT 으로 Alice 변경이 확정됨.


2. Consistency — CHECK · FK 제약 위반

START TRANSACTION;
UPDATE accounts SET balance = balance + 5 WHERE id = 2;
 
-- CHECK 위반: balance 가 음수가 되는 UPDATE
UPDATE accounts SET balance = balance - 999 WHERE id = 1;
 
SELECT id, name, balance FROM accounts WHERE id = 2;
ROLLBACK;
SELECT id, name, balance FROM accounts WHERE id = 2;
ERROR 3819 (HY000): Check constraint 'chk_balance' is violated.
 
id  name  balance
2   Bob   55
 
id  name  balance
2   Bob   50
START TRANSACTION;
 
-- FK 위반: 존재하지 않는 account(id=99) 참조
INSERT INTO transfers (from_id, to_id, amount) VALUES (1, 99, 10);
 
SELECT COUNT(*) AS transfers_count FROM transfers;
ROLLBACK;
ERROR 1452 (23000): Cannot add or update a child row:
a foreign key constraint fails (`testdb`.`transfers`,
CONSTRAINT `transfers_ibfk_2` FOREIGN KEY (`to_id`) REFERENCES `accounts` (`id`))
 
transfers_count
0

명시 트랜잭션 안에서는 제약 위반 DML 만 실패하고 트랜잭션은 열린 채 유지됨. 실패 전에 실행한 Bob 의 +5 변경은 같은 세션 안에서 계속 보이며, 애플리케이션이 이후 ROLLBACK 또는 COMMIT 을 명시해야 함.


3. Isolation — MVCC REPEATABLE READ 스냅샷

두 세션을 동시에 실행한 결과임.

세션 T1 (먼저 SELECT, SLEEP 중 T2 커밋 후 다시 SELECT):

START TRANSACTION;
SELECT id, name, balance FROM accounts WHERE id = 1;  -- 스냅샷 생성
-- (T2 가 balance = 999 로 커밋한 뒤)
SELECT id, name, balance FROM accounts WHERE id = 1;  -- 스냅샷 유지
COMMIT;
-- 첫 번째 SELECT
id  name   balance
1   Alice  70
 
-- 두 번째 SELECT (T2 커밋 이후)
id  name   balance
1   Alice  70

세션 T2 (T1 이 열려있는 동안 UPDATE + COMMIT):

UPDATE accounts SET balance = 999 WHERE id = 1;
COMMIT;
SELECT id, name, balance FROM accounts WHERE id = 1;
id  name   balance
1   Alice  999

T2 가 999 로 커밋했지만 T1 은 스냅샷 시점인 70 을 계속 봄. T2 의 직접 조회는 최신값 999 를 반환함 — T1 과 T2 가 독립된 뷰를 가짐.


참고