Transaction 알아보기

2025. 9. 15. 23:53·CS/DB

트랜잭션이란?

트랜잭션은 데이터베이스에서 하나의 작업을 이루는 여러 SQL 연산을 하나로 묶은 것이다. 여러 쿼리를 실행하더라도 사용자는 그것을 하나의 작업 단위처럼 다룬다.

 

예를 들어 송금 기능을 생각해보자. A 계좌에서 10만 원을 출금하고, B 계좌에 10만 원을 입금한다고 할 때 이 두 동작이 모두 성공해야만 “송금”이라는 하나의 작업이 완료된다. 만약 하나라도 실패하면 전체가 취소되어야 하며, 이를 보장해주는 것이 트랜잭션이다.

 

전체 흐름을 간단히 보자면, WAS나 데이터베이스 접근 도구에서 DB에 접속한다고 해보자.

이때 DB 서버와 접근 도구 사이에 커넥션(Connection)이 생성되고, 이 커넥션을 통해 DB 서버에 쿼리가 전달된다. DB 서버는 이에 대응되는 세션(Session)을 생성하여 실제 쿼리를 수행한다. 세션은 쿼리를 받는 순간 트랜잭션을 시작하며, 이후 COMMIT 혹은 ROLLBACK을 통해 트랜잭션을 종료한다.

 

커밋(commit): SQL의 변경사항을 데이터베이스에 적용
롤백(rollback): SQL 수행 중 문제가 생기면 변경사항을 되돌림

 

대부분의 데이터베이스에서는 기본적으로 자동 커밋(auto-commit) 모드가 설정되어 있어, SQL 문이 실행되면 곧바로 자동으로 커밋된다. 하지만 이 방식은 트랜잭션을 세밀하게 제어하기 어렵기 때문에, 트랜잭션을 제대로 활용하려면 자동 커밋을 꺼야 한다.

set autocommit true; //자동 커밋 모드 켜기
set autocommit false; //자동 커밋 모드 키기

 

자동 커밋을 끄면, 수동 커밋 모드로 전환되며, 개발자가 명시적으로 commit 또는 rollback을 호출해야 트랜잭션이 종료된다. 한 번 커밋 모드를 설정하면 해당 세션에서는 계속 유지되며, 필요에 따라 중간에 다시 자동 커밋으로 변경하는 것도 가능하다.


트랜잭션 ACID

트랜잭션은 기본적으로 4가지 원칙 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장해야 하고, 그냥 간단하게 트랜잭션 ACID라고 부른다.

 

원자성(Atomicity)

트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 모두 실패해야 한다.

 

지속성(Durability)

트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다

 

트랜잭션은 커밋이 완료되었다고 사용자에게 응답을 보낸 이상, 반드시 DB에 반영되어야 한다. 그런데 커밋 과정 중 DB 서버가 갑자기 다운될 수도 있다. 이 상황에서도 지속성(Durability) 은 보장될 수 있을까?

 

DB는 실제 데이터를 디스크에 쓰기 전에, 먼저 redo log(커밋 로그) 에 변경 사항을 기록한다. 이 로그가 디스크에 안전하게 기록된 시점에만 사용자에게 “커밋 완료” 응답을 보낸다. 따라서 만약 로그까지만 기록되고 실제 데이터 블록을 쓰는 중에 서버가 꺼진다면, DB는 재시작 과정에서 로그를 읽어 미완료 작업을 다시 반영(redo) 하여 커밋 상태를 복구할 수 있다.

 

정리하면 순서는 다음과 같다.

  1. 커밋 로그를 디스크에 기록한다.
  2. 사용자에게 커밋 완료 응답을 보낸다.
  3. 실제 데이터 파일에 변경 내용을 반영한다.

즉, 로그가 기록되지 않았다면 커밋 자체가 성립하지 않은 것이고, DB는 단순히 롤백하면 된다. 반대로, 로그가 기록되었다면 어떤 장애가 나더라도 커밋은 최종적으로 보장된다.

 

 

 

격리성(Isolation)

동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어 동시에 같은 데이터를 수정하지 못하도록 해야 한다. 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(Isolation level)을 선택할 수 있다.

 

트랜잭션에서 발생할 수 있는 다양한 현상

격리 수준을 알아보기 전에 트랜잭션의 다양한 현상에 대해서 알아보자

 

더티 리드

아직 커밋되지 않은 트랜잭션의 변경 사항을 다른 트랜잭션이 읽어버리는 현상이다.

예를 들어, 트랜잭션 A가 어떤 값을 5 → 10으로 수정했지만 아직 커밋하지 않은 상태라고 하자. 이때 트랜잭션 B가 해당 데이터를 조회했는데 값이 10으로 보였다면, 이는 더티 리드에 해당한다. 문제는, 이후 트랜잭션 A가 롤백을 해버리면 실제 데이터는 다시 5로 되돌아간다는 점이다. 하지만 트랜잭션 B는 존재하지 않는 값 10을 근거로 로직을 수행했을 수 있기 때문에, 잘못된 결과를 낼 위험이 있다.

 

팬텀 리드

한 트랜잭션 안에서 같은 조건으로 여러 번 조회했을 때, 새로운 행(row)이 나타나거나 사라지는 현상이다.

예를 들어, 트랜잭션 T1이 가격 < 10000인 상품 목록을 조회했더니 5건이 나왔다. 그런데 동시에 트랜잭션 T2가 가격이 9000원인 상품을 새로 추가하고 커밋했다. 이후 T1이 같은 조건으로 다시 조회하자 이번에는 6건이 나온다. 이것이 바로 팬텀 리드다. 겉으로 보면 단순히 “두 번 조회하면 되지 않나?” 싶을 수 있다. 하지만 사용자 입장에서는 같은 조건으로 조회했는데 결과가 달라지는 건 혼란을 주며, 이를 근거로 추가 로직을 실행했을 때 데이터 불일치나 비즈니스 오류가 발생할 수 있다.

 

격리 수준

앞서 살펴본 것처럼 동시에 실행되는 트랜잭션들은 더티 리드, 팬텀 리드와 같은 다양한 이상 현상을 일으킬 수 있다. 하지만 모든 트랜잭션을 완벽하게 격리해버리면 동시성이 크게 떨어지고 성능이 나빠진다.

그래서 데이터베이스는 격리 수준(Isolation Level) 이라는 개념을 두어, 데이터 정합성과 성능 사이에서 균형을 선택할 수 있도록 한다. ANSI SQL 표준에서는 네 가지 격리 수준을 정의하고 있으며, DBMS마다 기본값도 조금씩 다르다.

 

READ UNCOMMITTED

커밋되지 않은 데이터를 다른 트랜잭션에서도 읽을 수 있기 때문에 더티 리드(Dirty Read) 가 발생할 수 있다. 격리 수준 중 가장 낮은 단계로, 동시성은 좋지만 데이터 정합성 측면에서는 안정성이 떨어진다.

 

READ COMMITTED (많은 DBMS의 기본값, 예: Oracle)

커밋된 데이터만 읽을 수 있으므로 더티 리드(Dirty Read) 는 발생하지 않는다. 하지만 같은 쿼리를 한 트랜잭션 안에서 여러 번 실행할 경우, 그 사이에 다른 트랜잭션이 데이터를 수정하고 커밋했다면 결과가 달라질 수 있다. 이를 반복 불가능한 읽기(Non-repeatable Read) 라고 한다.

 

REPEATABLE READ (MySQL 기본값)

한 트랜잭션 안에서 같은 조건으로 같은 데이터를 반복 조회하면 항상 동일한 결과를 보장한다. 즉, 반복 불가능한 읽기(Non-repeatable Read) 는 방지할 수 있다. 하지만 새로운 행이 삽입되면 이전에는 없던 데이터가 조회 결과에 추가될 수 있는데, 이를 팬텀 리드(Phantom Read) 라고 한다.

 

SERIALIZABLE

가장 엄격한 격리 수준으로, 트랜잭션을 마치 순차적으로 하나씩 실행하는 것과 동일하게 보장한다. 따라서 더티 리드(Dirty Read), 반복 불가능한 읽기(Non-repeatable Read), 팬텀 리드(Phantom Read) 가 모두 방지된다. 하지만 그만큼 동시성이 크게 떨어지고 성능 저하가 심하다는 단점이 있다.

 

물론 상황에 맞게 격리 수준을 설정하는 것이 가장 이상적이다. 하지만 실제로 백엔드 개발자가 직접 이 단계를 세세하게 조정하는 경우는 많지 않다. 보통은 Spring이 기본 격리 수준을 적절히 설정해주기 때문이다.

 

 

일관성(Consistency)

모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 

 

여기서 일관성 있는 데이터베이스가 상태가 무엇일까? 나는 트랜잭션의 일관성(Consistency) 이라는 개념이 조금 헷갈렸다.
간단히 정리하면, 데이터베이스가 정의한 무결성 제약 조건을 위반하지 않는 상태를 의미한다.

 

예를 들어 사용자가 온라인 쇼핑몰에 주문을 넣는다고 하자. 그런데 사용자 정보가 존재하지 않는다면, 주문 테이블의 user_id 외래 키(FK)는 반드시 사용자 테이블을 참조해야 한다는 규칙을 깨뜨리게 된다. 이런 경우 데이터베이스는 해당 트랜잭션을 거부하고, 기존의 무결한 상태를 그대로 유지한다.

 

즉, 트랜잭션이 성공하든 실패하든 상관없이, 최종적으로 데이터베이스는 항상 제약 조건을 지키는 정상적인 상태를 유지해야 한다는 것이 일관성의 의미다.

 


트랜잭션 상태

ACID 원칙이 실제로 지켜지려면, 트랜잭션이 지금 어느 시점에 있는지(시작/진행/커밋 대기/롤백 예약)와 최종적으로 어떻게 끝났는지(커밋/롤백/자동 롤백)를 추적해야 한다. 로직이 어디에서 어떤 이유로 종료됐는지를 알아야 재시도·보상·후처리를 정확히 실행할 수 있다.

 

활성 (Active)

트랜잭션이 시작되면 데이터베이스에서 읽기와 쓰기 작업을 할 수 있는 상태가 된다. 이때의 모든 변경 사항은 아직 확정되지 않았고, 필요하다면 언제든 롤백할 수 있다. 즉, 작업이 진행 중인 단계다.

 

부분 완료 (Partially Committed)

트랜잭션의 모든 연산이 끝났을 때, 실제로 데이터베이스에 반영되기 직전 상태다. 내부적으로는 커밋을 준비하는 단계이며, 이 순간 장애가 발생하면 최종 커밋은 되지 않고 롤백될 수 있다.

 

완료 (Committed)

커밋이 정상적으로 끝난 상태다. 이제 트랜잭션에서 수행한 모든 변경 사항이 데이터베이스에 확정되고, 다른 트랜잭션에서도 조회할 수 있게 된다. 트랜잭션이 성공적으로 종료된 단계다.

 

실패 (Failed)

트랜잭션 실행 중 오류가 발생하거나 중단되어 더 이상 진행할 수 없는 상태다. 이 상태에서는 트랜잭션이 성공적으로 커밋될 수 없으며, 롤백 절차가 필요하다.

 

철회 (Aborted)

실패한 트랜잭션에 대해 롤백이 완료된 상태다. 모든 변경 사항이 취소되고 데이터베이스는 트랜잭션 시작 전과 같은 상태로 되돌아간다. 이후 이 트랜잭션은 종료되며 더 이상 작업을 할 수 없다.

 

개념적으로는 이 다섯 가지 상태로 정리할 수 있다. 다만 상태가 전이될 때 트랜잭션 자체와 트랜잭션 AOP 등 부가 메커니즘이 어떻게 대응·작동하는지는 별도 절에서 자세히 다루도록 하겠다.


트랜잭션 전파

트랜잭션 전파(Propagation) 는 이미 실행 중인 트랜잭션이 있는 상황에서 새로운 트랜잭션 요청이 들어왔을 때, 이 둘을 어떻게 연결할지 결정하는 규칙을 말한다. 스프링을 예로 들면 어떤 메서드에서 @Transactional을 사용했는데, 그 메서드가 또 다른 @Transactional 메서드를 호출할 때 기존 트랜잭션을 이어서 쓸지, 끊고 새로 만들지, 아예 트랜잭션 없이 실행할지를 정하는 방식이 전파다.

 

물리 트랜잭션과 논리 트랜잭션

사실 우리가 알고 있던 트랜잭션은 즉시 물리적으로 적용되는 게 아니라, 애플리케이션에서 논리 트랜잭션으로 수행되고, 실제로는 여러 논리 트랜잭션이 모여 하나의 물리 트랜잭션으로 실행되는 구조다.

 

스프링에서 @Transactional을 붙이면 AOP 프록시가 메서드 호출을 감싸면서 논리 트랜잭션을 시작한다. 하지만 실제 DB 커넥션 단에서는 매번 새로운 트랜잭션이 열리는 것이 아니라, 여러 논리 트랜잭션이 모여 하나의 물리 트랜잭션을 공유하게 된다. 따라서 외부 메서드에서 이미 트랜잭션이 시작되어 있다면, 내부 메서드의 @Transactional은 별도의 커넥션을 생성하지 않고 기존 물리 트랜잭션에 합류한다. 이렇게 합류된 모든 로직은 결국 하나의 물리 트랜잭션으로 묶여 DB에 한꺼번에 반영된다. 이 합류된 물리 트랜잭션에 있던 로직들이 한꺼번에 DB에 적용되는 것이다.

 

그림을 통해 다시 보자면 로직 A가 실행되고, 그 안에서 로직 B가 호출되며 두 메서드 모두 @Transactional이 적용되어 있다고 하자. 먼저 시작된 트랜잭션을 외부 트랜잭션, 그 안에서 실행되는 추가 @Transactional을 내부 트랜잭션이라 부른다. 내부 트랜잭션은 겉으로는 독립적인 것처럼 보이지만, 실제로는 동일한 물리 트랜잭션을 공유한다(기본 전파 REQUIRED). 따라서 내부 트랜잭션에서 예외가 발생하면 외부 트랜잭션까지 함께 영향을 받아, 최종적으로 전체 물리 트랜잭션이 롤백된다.

 

마지막으로 물리 트랜잭션은 DB 커넥션 단위에서 실제로 열리고 commit 또는 rollback으로 종료되는 단위이며, 이것만이 실제 데이터베이스에 영향을 준다. 여러 논리 트랜잭션이 한 물리 트랜잭션 위에서 동작하기 때문에, 그중 일부라도 실패하면 최종적으로 물리 트랜잭션 전체가 롤백된다.

 

전파의 종류

논리·물리 트랜잭션의 전파를 생각해보면 뭔가 의문점이 들 것이다. 코드에 붙인 @Transactional이 무조건 다른 트랜잭션과 묶여 실행될까? 따로따로 적용되면 안되는걸까..?

 

물론 따로따로 할 수도 있다!

 

어떤 전파(Propagation) 를 선택했는지에 따라, 기존 트랜잭션에 합류할지, 새로 열지, 혹은 비트랜잭션으로 돌릴지가 달라진다.

 

옵션 기존 트랜잭션 없음 기존 트랜잭션 있음
REQUIRED 새로 시작 합류
REQUIRES_NEW 새로 시작 기존 중단, 새로 시작
SUPPORTS 트랜잭션 없이 실행 합류
MANDATORY 예외 발생 합류
NOT_SUPPORTED 트랜잭션 없이 실행 기존 중단 후 비트랜잭션 실행
NEVER 트랜잭션 없이 실행 예외 발생
NESTED 새로 시작 세이브포인트 생성

 

스프링에서 트랜잭션 전파 옵션은 총 7가지가 있지만, 기본값은 REQUIRED다. 따라서 보통은 새로운 메서드에서 @Transactional이 선언되더라도, 이미 실행 중인 트랜잭션이 있으면 그 안으로 논리 트랜잭션이 합류하게 된다. 이때 처음 시작된 트랜잭션을 신규 트랜잭션(외부 트랜잭션)이라고 부르고, 이후 합류하는 것들을 내부 트랜잭션이라고 부를 수 있다.

 

트랜잭션의 대표적인 예로 자주 언급되는 송금 기능을 살펴보자. 먼저 로직 A가 송금을 처리하면서 새로운 트랜잭션을 시작한다. 이후 실행되는 레포지토리 계층의 메서드들도 모두 이 트랜잭션에 포함된다. 이어서 로직 C가 단순히 로그를 저장하더라도, 전파 속성이 기본값(REQUIRED)이라면 별도의 트랜잭션을 만들지 않고 기존 물리 트랜잭션에 함께 묶여 실행된다.

 

 

예시를 곱씹어 보면 물리 트랜잭션과 논리 트랜잭션의 차이가 분명히 드러난다. 만약 로그 저장(로직 C)도 기본 전파 속성(REQUIRED)으로 같은 물리 트랜잭션에 묶여 있다면, 로그 저장이 실패했을 때 송금(로직 A)까지 함께 롤백되어 버린다. 하지만 사용자 입장에서는 “단순히 로그가 기록되지 않았을 뿐인데, 정작 중요한 송금까지 실패한다”는 것은 납득하기 어려운 결과다.

 

이런 경우 사용할 수 있는 전파 속성이 REQUIRES_NEW다.

로그 저장(로직 C)에 REQUIRES_NEW를 적용하면, 송금(로직 A)과 그 결과를 DB에 반영하는 과정(로직 B)은 하나의 물리 트랜잭션으로 묶여서 함께 성공/실패한다. 반면 로그 저장(로직 C)은 별도의 물리 트랜잭션에서 실행되므로, 로그 저장이 실패하더라도 본질적인 비즈니스 로직인 송금에는 영향을 주지 않는다.

 

따라서 각 비즈니스 로직이 같은 트랜잭션에 묶여도 되는지, 분리되어야 하는지부터 판단하고, 그 결정에 맞춰 적절한 트랜잭션 전파 옵션을 선택해야 한다.

 

출처: https://youtu.be/b0s9RzKyHN0?si=I4fi1QsVlT2ZVMTD

 

'CS > DB' 카테고리의 다른 글

DB 성능 문제, 구조를 나누는 것부터 시작하기  (0) 2025.12.28
DBCP가 뭐지..?  (0) 2025.12.28
데이터베이스 성능을 저하시키는 N+1 쿼리 문제란?  (0) 2025.10.03
Enum VS VARCHAR  (0) 2025.09.15
DB에서 상속은 어떻게 나타내는가?  (0) 2025.09.14
'CS/DB' 카테고리의 다른 글
  • DBCP가 뭐지..?
  • 데이터베이스 성능을 저하시키는 N+1 쿼리 문제란?
  • Enum VS VARCHAR
  • DB에서 상속은 어떻게 나타내는가?
baeminn
baeminn
새로운 기술을 익히는 데 그치지 않고, 실제 문제 해결에 적용하며 구조와 한계를 함께 고민해왔습니다. 서비스 설계, 공간 데이터 분석, 도구 개발 등 다양한 프로젝트를 통해 문제 해결에 필요한 기술을 직접 선택하고 적용해 왔으며, 하나의 역할에 머무르지 않고 필요한 영역까지 책임지는 개발자로 성장하고자 합니다!
  • baeminn
    BaeLog
    baeminn
  • 전체
    오늘
    어제
    • 분류 전체보기 N
      • Programming
      • Java
      • CS
        • DB
        • OS
      • Web
      • Spring
      • GeoInfo
      • Infra N
        • Kubernates
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 깃허브
  • 공지사항

  • 인기 글

  • 태그

    n+1
    select
    AOP
    sharding
    epoll
    poll
    dbcp
    Datebase
    partitioning
    쉬운코드
    db
    spring
    OOP
    I/O
    matcher
    replicaton
    데이터베이스
    MAP
    OS
    redis
    멀티플렉싱
    우테코
    spring aop
    Java
    set
    io
    infra
    database
    hash
    cs
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
baeminn
Transaction 알아보기
상단으로

티스토리툴바