트랜잭션(Transaction)은 하나의 작업 단위를 구성하는 연산들의 집합으로, 데이터베이스의 상태를 변화시키는 하나의 논리적 작업 단위를 의미한다. 트랜잭션은 반드시 완전하게 수행되거나 전혀 수행되지 않아야 하며, 이를 보장하기 위해 다음 네 가지 속성, 즉 ACID 원칙을 지켜야 한다.

1. 트랜잭션의 핵심 개념

  • 데이터 무결성 보장: 중간에 문제가 발생해도 전체 작업이 일관된 상태를 유지하도록 함.
  • 예: 은행 송금 시스템
    • A 계좌에서 1000원을 출금하고 -> B 계좌에 1000원을 입금해야 함
    • 출금만 되고 입금이 실패한다면? -> 데이터 불일치 발생
    • -> 하나의 트랜잭션으로 묶어 모두 성공하거나 모두 실패시킴

2. ACID 원칙

1. Atomicity (원자성)

  • 정의: 트랜잭션 내의 작업들이 모두 성공하거나, 모두 실패해야 한다는 원칙.
  • 예시: 온라인 쇼핑몰에서 주문 결제를 처리할 때 (재고 수량 차감 + 결제 정보 저장 + 주문 내역 저장)
    ➡ 이 중 하나라도 실패하면, 다른 작업도 모두 무효화되어야 한다. 예를 들어 결제는 성공했는데 주문 내역 저장이 실패하면 전체 롤백되어야 함.
    실패 시 -> ROLLBACK 수행

2. Consistency (일관성)

  • 정의: 트랜잭션이 실행되기 전과 실행된 후의 데이터는 정의된 제약조건이나 비즈니스 룰을 항상 만족해야 한다는 원칙
  • 예시: 은행 시스템에서 한 계좌에서 1000원이 출금되고 다른 계좌에 1000원이 입금되는 경우
    ➡ 총합 = 출금 전 + 입금 전 -> 트랜잭션 이후에도 총합은 동일해야 함
    ➡ 중간에 어떤 실패로 총합이 달라지면 일관성 위배

3. Isolation (격리성)

  • 정의: 여러 트랜잭션이 동시에 실행될 때, 각 트랜잭션은 서로의 작업이 보이지 않도록 격리되어야 함. 결과적으로 순차적으로 실행된 것과 동일한 결과가 나와야 함.
  • 예시: 사용자 A가 상품 재고를 1개 줄이는 작업 수행 중. 동시에 사용자 B가 같은 상품의 재고를 조회
    ➡ B는 트랜잭션이 완료된 이후의 상태를 봐야 함 (중간 상태를 보면 안 됨)

4. Durability (지속성)

  • 정의: 트랜잭션이 성공적으로 커밋되면, 그 결과는 영구적으로 저장되어야 하며, 시스템 장애가 발생해도 손실되지 않아야 함.
  • 예시: 고객이 은행 계좌에 100만원을 입금하고 “완료” 메시지를 받았음 -> 이후 시스템이 재부팅돼도 반영된 잔액은 그대로 유지되어야 함 ➡ WAL(Write-Ahead Logging), Undo/Redo 로그 등의 매커니즘 필요 ➡ 디스크에 안전하게 기록된 후에야 커밋 처리

🔧 해결 방법 ①: WAL (Write-Ahead Logging)

개념

  • 모든 변경 내용을 실제 데이터에 적용하기 전에 먼저 로그 파일에 기록하는 방식

어떻게 작동하나?

  1. 트랜잭션 시작
  2. 변경될 내용을 로그 파일(WAL)에 먼저 기록 (ex: “A 계좌 -> +100만원”)
  3. 로그 기록 완료되면 -> 실제 데이터에 반영
  4. 커밋

장점

  • 시스템이 꺼지더라도 로그를 복구하면 이전 상태로 되돌릴 수 있음
  • 데이터보다 로그를 먼저 남기기 때문에 일관성 보장

📌 PostgreSQL, MySQL(InnoDB), Oracle 모두 WAL 사용

🔧 해결 방법 ②: Undo / Redo Log

Undo 로그

  • “이전 상태”를 기록해둠 -> 롤백할 때 사용
  • 예: “기존 잔액은 500만원이었다”

Redo 로그

  • “최종 상태”를 기록해둠 -> 시스템 재시작 시 적용
  • 예: “최종적으로 잔액은 600만원이다”

어떻게 보장하나?

  • 트랜잭션 커밋 전까지는 둘 다 디스크에 저장해둔다
  • 이후 시스템이 꺼지면:
    • Undo 로그로 원래대로 복구하거나
    • Redo 로그로 다시 적용함

🔧 해결 방법 ③: 디스크 플러시 (fsync)

개념

  • 트랜잭션 커밋 시, 메모리 버퍼에 있는 데이터를 디스크에 완전히 기록 완료될 때까지 기다림

왜 중요하냐면?

  • 대부분의 DBMS는 성능을 위해 데이터를 일단 메모리 버퍼에만 적어둠
  • 하지만 전원 끊기면 메모리는 사라짐 -> 플러시(fsnyc)를 통해 디스크에 실제로 쓰게 함

비유하자면:

  • “저장” 버튼 눌렀다고 해서 문서가 저장된 게 아니라, 실제로 하드에 쓰여야 진짜 저장된 것

3. 트랜잭션의 동작 과정

  1. BEGIN: 트랜잭션 시작
  2. 작업 수행 (INSERT/UPDATE/DELETE 등)
  3. COMMIT: 변경사항을 확정하고 반영
  4. ROLLBACK: 문제가 생기면 이전 상태로 되돌림

4. 트랜잭션의 격리 수준 (Isolation Level)

여러 사용자가 동시에 데이터베이스를 사용할 때 다른 트랜잭션이 보는 데이터의 일관성을 어디까지 보장할 것인가를 결정한다. 격리 수준이 낮으면 성능은 좋지만, Dirty Read, Non-Repeatable Read, Pahntom Read 같은 문제가 생길 수 있다.

격리 수준 Dirty Read Non-Repeatable Read Phantom Read 성능
Read Uncommitted ✅ 발생 ✅ 발생 ✅ 발생 🔼 빠름
Read Committed ❌ 차단 ✅ 발생 ✅ 발생 🔼 빠름
Repeatable Read ❌ 차단 ❌ 차단 ✅ 발생
(MySQL은 ❌)
⬆ 중간
Serializable ❌ 차단 ❌ 차단 ❌ 차단 🔽 느림

1. Read Uncommited (읽기 허용)

  • 커밋되지 않은 데이터도 다른 트랜잭션이 읽을 수 있음
  • Dirty Read 가능

🔸 예시

  • 트랜잭션 A가 상품 가격을 100 -> 80으로 수정 (커밋 전)
  • 트랜잭션 B가 이걸 읽음 -> 가격이 80으로 보임
  • A가 롤백 -> 실제 가격은 100, B는 잘못된 값을 읽음

🔹 사용 주의: 대부분의 시스템에서는 사용하지 않음

2. Read Commited (커밋된 것만 읽기)

  • 커밋된 데이터만 읽을 수 있음 -> Dirty Read 방지
  • 하지만 Non-Repeatable Read, Phantom Read는 발생

🔸 예시

  • A 트랜잭션에서 고객 이름을 두 번 조회했는데
  • 그 사이 B 트랜잭션이 이름을 바꾸고 커밋함
  • A가 두 번째 조회했을 때 값이 달라짐 -> Non-Repeatable Read 발생

🔹 Oracle, SQL Server의 기본 격리 수준

3. Repeatable Read (반복 읽기 보장)

  • 트랜잭션 내에서 같은 데이터를 두 번 읽으면 결과가 항상 동일함
  • Non-Repeatable Read 차단
  • 하지만 여전히 Phantom Read 가능
    • 단, MySQL InnoDB에서는 Next-Key Locking으로 Phantom Read까지 막음

🔸 예시

  • A 트랜잭션: “가격이 1만 원 이상인 상품” 조회 -> 10건
  • B 트랜잭션이 조건에 맞는 상품 새로 추가하고 커밋
  • A가 다시 조회했을 때 11건 -> Phantom Read 발생

🔹 MySQL의 기본 격리 수준

4. Serializable (직렬화)

  • 가장 높은 격리 수준
  • 모든 트랜잭션을 순차적으로 실행한 것처럼 보이도록 강제
  • 모든 문제 (Dirty/Non-Repeatable/Phantom) 방지
  • 성능은 가장 낮음 (락, 대기 발생 많음)

🔸 예시

  • SELECT도 공유 잠금(Shared Lock)을 걸어서 다른 트랜잭션이 데이터 수정/삽입 불가
  • 동시성이 매우 제한됨

🔹 금융 시스템, 회계 시스템처럼 정합성이 최우선인 환경에서 사용

Dirty Read: 커밋 전 데이터를 읽음
Non-Repeatable Read: 같은 행을 두 번 읽었는데 다름
Phantom Read: 같은 조건인데 행 수가 다름

Next-Key Locking (InnoDB만의 기술)
➡ 행 + 다음 인덱스 간 범위까지 잠금 -> Phantom Read 방지

공유 잠금(Shared Lock): 데이터를 읽을 때 설정 -> 다른 트랜잭션이 쓰기 불가
배타 잠금(Exclusive Lock): 데이터를 수정할 때 설정 -> 다른 트랜잭션이 읽기/쓰기 불가

5. Spring에서 트랜잭션 사용 예시

@Transactional
public void transferMoney(Long fromId, Long toId, int amount) {
    Account from = accountRepository.findById(fromId);
    Account to = accountRepository.findById(toId);

    from.decrease(amount);
    to.increase(amount);
}
  • @Transactional을 사용하면 예외가 발생할 경우 자동으로 rollback됨
  • DB 연결, 커밋, 롤백을 직접 제어할 필요 없이 Spring이 관리

@Transactional프록시(proxy) 기반 AOP 방식으로 동작하기 때문에, 같은 클래스 내부의 메서드 호출에는 트랜잭션이 적용되지 않는다.

@Transactional이 동작할 때, Spring은 트랜잭션 상태와 연결(Connection)을 ThreadLocal에 저장하여 관리한다.
즉, 같은 쓰레드 안에서는 여러 DAO/Repository 호출이 하나의 트랜잭션으로 묶여 동작한다.
반면, 다른 쓰레드로 넘어가면 트랜잭션 컨텍스트는 전달되지 않기 때문에 별도로 트랜잭션을 시작해야 한다.

6. 실무 적용 시 고려사항

  1. 트랜잭션 범위 최소화
    → 너무 길게 유지하면 자원 점유율 상승, Deadlock 위험 증가
  2. 필요한 곳에만 명확히 지정
    → 읽기 작업은 @Transactional(readOnly = true)로 설정하여 성능 최적화 가능
  3. 예외 처리 주의@Transactionalunchecked 예외(RuntimeException) 발생 시에만 rollback → checked 예외까지 rollback 하려면 rollbackFor = Exception.class 지정 필요

Deadlock: 데이터베이스나 운영체제에서 여러 트랜잭션(또는 쓰레드)이 서로 자원을 점유하고 있으면서, 동시에 상대방의 자원을 기다리느라 영원히 진행되지 못하는 상태를 말한다.

댓글남기기