계좌 이체 중에 서버가 죽으면 돈은 어디로 갈까요? 출금은 됐는데 입금이 안 된 상태로 남는 건 아닐까요?


트랜잭션이란

하나의 논리적 작업 단위 입니다. 여러 SQL 문이 모여서 하나의 작업을 구성하고, 이 작업은 전부 성공하거나 전부 실패 해야 합니다.

계좌 이체를 예로 들면, "A 계좌에서 출금"과 "B 계좌에 입금"은 반드시 함께 성공하거나 함께 실패해야 합니다. 출금만 되고 입금이 안 되면 돈이 증발하는 거니까요.

SQL
-- 이 두 문장은 하나의 트랜잭션으로 묶여야 한다
UPDATE account SET balance = balance - 10000 WHERE id = 'A';
UPDATE account SET balance = balance + 10000 WHERE id = 'B';

ACID 원칙

트랜잭션이 보장해야 할 네 가지 속성입니다. 데이터베이스를 공부하면 가장 먼저 만나는 개념인데, 각각이 어떤 문제를 해결하는지 에 초점을 맞추면 이해가 빨라집니다.

Atomicity — 원자성

전부 성공하거나, 전부 실패하거나.

트랜잭션 안의 모든 연산은 하나의 단위로 취급됩니다. 중간에 하나라도 실패하면 이전에 성공한 연산도 모두 취소(ROLLBACK)됩니다.

InnoDB에서는 Undo Log 가 이걸 가능하게 합니다. 각 변경 전의 데이터를 Undo Log에 저장해 두고, 롤백이 필요하면 이 로그를 역순으로 적용합니다.

SQL
BEGIN;
UPDATE account SET balance = balance - 10000 WHERE id = 'A';  -- 성공
UPDATE account SET balance = balance + 10000 WHERE id = 'B';  -- 실패!
ROLLBACK;  -- A의 출금도 취소됨

Consistency — 일관성

트랜잭션 전후로 데이터베이스는 항상 유효한 상태여야 한다.

"유효한 상태"란 제약 조건(PK, FK, UNIQUE, CHECK 등)이 모두 만족되는 상태 를 의미합니다. 잔액이 마이너스가 되면 안 되는 규칙이 있다면, 트랜잭션이 그 규칙을 깨뜨리는 순간 롤백되어야 합니다.

공부하다 보면 Consistency가 가장 추상적으로 느껴지는데, 사실 나머지 세 속성(A, I, D)이 제대로 보장되면 자연스럽게 따라오는 성질에 가깝습니다.

Isolation — 격리성

동시에 실행되는 트랜잭션은 서로의 중간 상태를 보지 못한다.

A가 이체 중인데, B가 동시에 같은 계좌 잔액을 조회하면 어떤 값이 보여야 할까요? 이체 전 금액? 이체 후 금액? 중간 금액? 이걸 결정하는 게 격리 수준(Isolation Level) 입니다.

InnoDB에서는 두 가지 메커니즘으로 격리성을 구현합니다.

  • MVCC(Multi-Version Concurrency Control): 데이터의 여러 버전을 유지해서, 읽기 트랜잭션이 쓰기 트랜잭션을 기다리지 않게 함
  • Lock(잠금): 동시에 같은 데이터를 수정하려는 경우 순서를 강제

격리 수준별 차이가 궁금하다면 ** 격리 수준 심화** 글을 참고해 주세요.

Durability — 지속성

한번 커밋되면, 서버가 죽어도 데이터는 살아 있어야 한다.

커밋된 트랜잭션의 결과는 어떤 장애 상황에서도 유실되지 않아야 합니다. InnoDB는 Redo Log(WAL, Write-Ahead Logging) 로 이걸 보장합니다.

동작 방식을 간단히 정리하면 이렇습니다.

  1. 데이터를 변경하면 Buffer Pool(메모리)의 페이지를 수정
  2. 커밋 시 변경 내용을 Redo Log에 먼저 기록 (디스크에 flush)
  3. 실제 데이터 파일에는 나중에 비동기로 반영 (체크포인트)
  4. 크래시가 발생하면 Redo Log를 ** 재생(replay)**해서 커밋된 내용을 복구

"데이터 파일에 직접 쓰기 전에 로그에 먼저 쓴다"는 게 WAL의 핵심입니다.


트랜잭션 라이프사이클

트랜잭션은 아래와 같은 상태를 거칩니다.

PLAINTEXT
Active → Partially Committed → Committed

Failed → Aborted

각 상태의 의미는 다음과 같습니다.

상태설명
Active트랜잭션이 실행 중. SQL 문들이 수행되고 있는 상태
Partially Committed마지막 SQL 문까지 실행 완료. 아직 디스크에 반영(commit)되지 않은 상태
Committed변경 내용이 디스크에 영구 반영된 상태. 이후 롤백 불가
Failed오류 발생으로 더 이상 진행 불가한 상태
AbortedROLLBACK 완료. 모든 변경이 취소되고 트랜잭션 이전 상태로 복원

실제 SQL로 보면 이런 흐름입니다.

SQL
-- 계좌 이체 트랜잭션
BEGIN;  -- Active 상태 시작

-- A 계좌에서 출금
UPDATE account SET balance = balance - 10000 WHERE id = 'A';

-- 잔액 확인
SELECT balance INTO @remaining FROM account WHERE id = 'A';

-- 잔액이 부족하면 롤백
IF @remaining < 0 THEN
    ROLLBACK;  -- Failed → Aborted
END IF;

-- B 계좌에 입금
UPDATE account SET balance = balance + 10000 WHERE id = 'B';

COMMIT;  -- Partially Committed → Committed

실무에서는 보통 애플리케이션 코드에서 try-catch로 감싸서 처리합니다.

JAVA
// Spring @Transactional 없이 수동 관리하는 예시
Connection conn = dataSource.getConnection();
try {
    conn.setAutoCommit(false);  // BEGIN

    // 출금
    PreparedStatement ps1 = conn.prepareStatement(
        "UPDATE account SET balance = balance - ? WHERE id = ?");
    ps1.setInt(1, 10000);
    ps1.setString(2, "A");
    ps1.executeUpdate();

    // 입금
    PreparedStatement ps2 = conn.prepareStatement(
        "UPDATE account SET balance = balance + ? WHERE id = ?");
    ps2.setInt(1, 10000);
    ps2.setString(2, "B");
    ps2.executeUpdate();

    conn.commit();  // 성공하면 커밋
} catch (Exception e) {
    conn.rollback();  // 실패하면 롤백
} finally {
    conn.setAutoCommit(true);
    conn.close();
}

AUTOCOMMIT

MySQL의 기본 설정은 AUTOCOMMIT = 1 (활성화)입니다. 이 상태에서는 ** 각 SQL 문이 자동으로 하나의 트랜잭션으로 실행 **됩니다.

SQL
-- AUTOCOMMIT = 1 (기본값)
-- 아래 INSERT는 실행 즉시 커밋된다
INSERT INTO orders (user_id, amount) VALUES (1, 50000);
-- ↑ 내부적으로 BEGIN → INSERT → COMMIT이 자동 실행됨

AUTOCOMMIT 상태에서 명시적 트랜잭션 사용

BEGIN(또는 START TRANSACTION)을 사용하면 AUTOCOMMIT이 ** 일시적으로 비활성화 **됩니다. COMMIT이나 ROLLBACK을 하면 다시 AUTOCOMMIT 모드로 돌아갑니다.

SQL
-- AUTOCOMMIT이 켜져 있어도, BEGIN을 쓰면 명시적 트랜잭션이 시작됨
BEGIN;
INSERT INTO orders (user_id, amount) VALUES (1, 50000);
INSERT INTO order_items (order_id, product_id) VALUES (LAST_INSERT_ID(), 42);
-- 여기서 두 INSERT는 아직 커밋되지 않은 상태
COMMIT;  -- 이때 비로소 둘 다 커밋됨

AUTOCOMMIT 비활성화

세션 단위로 끌 수도 있습니다.

SQL
SET AUTOCOMMIT = 0;

-- 이제 모든 SQL은 명시적 COMMIT 전까지 커밋되지 않는다
INSERT INTO logs (message) VALUES ('작업 시작');
UPDATE inventory SET qty = qty - 1 WHERE product_id = 42;

COMMIT;  -- 여기서 커밋

-- 다음 SQL부터 다시 새 트랜잭션 시작
UPDATE inventory SET qty = qty - 1 WHERE product_id = 43;
ROLLBACK;  -- 위의 UPDATE 취소

공부하다 보면 "AUTOCOMMIT을 끄면 성능이 좋아진다"는 말을 가끔 보는데, 이건 배치 처리처럼 여러 문장을 한 트랜잭션으로 묶을 때 커밋 오버헤드가 줄어서 그렇습니다. 일반적인 웹 애플리케이션에서는 Spring의 @Transactional이 알아서 관리해 주기 때문에 직접 건드릴 일은 거의 없습니다.


암묵적 커밋 (Implicit Commit)

트랜잭션 중에 ** 특정 SQL을 실행하면 MySQL이 자동으로 현재 트랜잭션을 커밋 **합니다. 이게 함정인데, 롤백하려고 했는데 이미 커밋되어 버린 상황이 생길 수 있습니다.

암묵적 커밋이 발생하는 경우

DDL (Data Definition Language)

  • CREATE TABLE, ALTER TABLE, DROP TABLE
  • CREATE INDEX, DROP INDEX
  • TRUNCATE TABLE
  • CREATE DATABASE, DROP DATABASE

** 관리 명령어**

  • LOCK TABLES, UNLOCK TABLES
  • LOAD DATA INFILE
  • SET AUTOCOMMIT = 1 (0에서 1로 바꿀 때)

** 트랜잭션 관련**

  • BEGIN (이미 트랜잭션 진행 중이면, 기존 트랜잭션을 커밋하고 새로 시작)

왜 위험한가

SQL
BEGIN;
UPDATE account SET balance = balance - 10000 WHERE id = 'A';
-- 여기까지는 아직 커밋 안 됨

ALTER TABLE account ADD COLUMN memo VARCHAR(255);
-- ⚠️ 암묵적 커밋 발생! 위의 UPDATE가 자동 커밋됨

UPDATE account SET balance = balance + 10000 WHERE id = 'B';
-- 이 UPDATE는 새로운 트랜잭션에서 실행됨

ROLLBACK;
-- A의 출금은 이미 커밋되어서 롤백 안 됨!
-- B의 입금만 롤백됨 → 돈 증발

이런 상황을 방지하려면 DDL과 DML을 같은 트랜잭션에 섞지 않는 게 원칙 입니다. 스키마 변경은 별도로 수행하세요.


SAVEPOINT

트랜잭션 전체를 롤백하는 대신, 특정 지점까지만 되돌리고 싶을 때 사용합니다.

기본 문법

SQL
SAVEPOINT 세이브포인트_이름;          -- 지점 설정
ROLLBACK TO SAVEPOINT 세이브포인트_이름;  -- 해당 지점까지 되돌리기
RELEASE SAVEPOINT 세이브포인트_이름;      -- 세이브포인트 삭제 (선택)

실제 예시: 배치 주문 처리

여러 건의 주문을 처리하는데, 일부가 실패해도 성공한 건은 유지하고 싶은 경우입니다.

SQL
BEGIN;

-- 주문 1 처리
SAVEPOINT order_1;
INSERT INTO orders (user_id, amount) VALUES (1, 30000);
UPDATE inventory SET qty = qty - 1 WHERE product_id = 101;
-- 주문 1 성공

-- 주문 2 처리
SAVEPOINT order_2;
INSERT INTO orders (user_id, amount) VALUES (2, 50000);
UPDATE inventory SET qty = qty - 1 WHERE product_id = 102;
-- 재고 부족으로 실패 가정
ROLLBACK TO SAVEPOINT order_2;
-- 주문 2만 취소, 주문 1은 살아있음

-- 주문 3 처리
SAVEPOINT order_3;
INSERT INTO orders (user_id, amount) VALUES (3, 20000);
UPDATE inventory SET qty = qty - 1 WHERE product_id = 103;
-- 주문 3 성공

COMMIT;  -- 주문 1과 3만 최종 반영

주의할 점이 있습니다.

  • ROLLBACK TO SAVEPOINT는 해당 세이브포인트 ** 이후 **의 변경만 취소합니다
  • 세이브포인트 자체는 유지되므로 다시 사용 가능합니다
  • COMMIT이나 ROLLBACK(전체)을 하면 모든 세이브포인트가 해제됩니다
  • 같은 이름으로 세이브포인트를 다시 설정하면 이전 것은 덮어쓰기됩니다

트랜잭션과 InnoDB 내부

트랜잭션의 ACID 각 속성이 InnoDB 내부에서 어떤 구조로 구현되는지 간단히 연결해 두겠습니다.

ACID 속성InnoDB 메커니즘역할
AtomicityUndo Log롤백 시 변경 전 데이터로 복원
Consistency제약 조건 + 위 세 속성의 조합데이터 무결성 유지
IsolationMVCC + Lock동시 접근 제어, 스냅샷 읽기
DurabilityRedo Log (WAL)크래시 후 커밋된 데이터 복구

각각의 상세한 동작 방식은 아래 글에서 다룹니다.

  • Undo Log와 Redo Log — 크래시 복구의 핵심 메커니즘
  • InnoDB MVCC — 락 없이 읽기를 처리하는 방법
  • Row Lock, Gap Lock, Next-Key Lock — InnoDB 잠금의 종류

실무 주의사항

트랜잭션은 짧게 유지

긴 트랜잭션이 가져오는 문제는 생각보다 많습니다.

  • **Lock 점유 시간 증가 **: 다른 트랜잭션이 대기하면서 전체 처리량 저하
  • **Undo Log 누적 **: MVCC를 위해 이전 버전을 유지해야 하므로, 긴 트랜잭션은 Undo Log 정리를 막음
  • ** 데드락 확률 증가 **: 여러 리소스를 오래 잡고 있을수록 교착 상태 발생 가능성이 올라감

트랜잭션 안에서 외부 API 호출 금지

JAVA
// ❌ 나쁜 예
@Transactional
public void processOrder(Order order) {
    orderRepository.save(order);
    paymentGateway.charge(order.getAmount());  // 외부 API 호출
    // API 응답이 느리면 트랜잭션이 오래 열려 있게 됨
    // API 타임아웃 시 DB 커넥션도 낭비
}

// ✅ 좋은 예
public void processOrder(Order order) {
    PaymentResult result = paymentGateway.charge(order.getAmount());  // 먼저 호출

    if (result.isSuccess()) {
        orderService.saveOrder(order);  // 트랜잭션은 DB 작업만
    }
}

외부 API 호출은 트랜잭션 밖에서 처리하고, 결과에 따라 DB 작업을 수행하는 게 안전합니다.

데드락 가능성 인지

두 트랜잭션이 서로가 잡고 있는 Lock을 기다리면 데드락이 발생합니다.

PLAINTEXT
트랜잭션 1: A 잠금 획득 → B 잠금 대기
트랜잭션 2: B 잠금 획득 → A 잠금 대기
→ 서로 영원히 기다림 → 데드락

InnoDB는 데드락을 자동 감지해서 한쪽을 강제 롤백합니다. 하지만 애초에 데드락이 덜 발생하도록 ** 리소스 접근 순서를 통일 하는 게 좋습니다. 데드락 관련 상세 내용은 ** 데드락 글을 참고해 주세요.


정리

  • ** 트랜잭션 **은 여러 SQL을 하나의 논리적 작업 단위로 묶는 것
  • ACID 는 트랜잭션이 보장해야 할 네 가지 속성 — 각각 Undo Log, 제약조건, MVCC/Lock, Redo Log로 구현
  • **라이프사이클 **: BEGIN → SQL 실행 → COMMIT 또는 ROLLBACK
  • AUTOCOMMIT 은 기본 활성화 — BEGIN을 쓰면 일시적으로 비활성화
  • **암묵적 커밋 **: DDL을 트랜잭션 중에 실행하면 이전 변경이 자동 커밋됨 — 주의 필요
  • SAVEPOINT: 트랜잭션 내에서 부분 롤백이 가능한 중간 지점
  • 실무에서는 트랜잭션을 짧게, 외부 호출은 밖에서, 데드락 순서를 통일하는 게 핵심
댓글 로딩 중...