Spring @Transactional 동작, 로그로 확인하기

2025. 8. 24. 07:00·Spring Framework/Spring

 

 

Spring에서 데이터베이스 트랜잭션은 @Transactional로 관리됩니다. 이 애노테이션은 AOP 기반 프록시가 트랜잭션 경계를 만들어 주는 방식으로 동작하며, 애플리케이션은 JPA를 통해 DB에 접근합니다.
이렇게 여러 구성 요소가 겹쳐 DB 트랜잭션을 사용하다 보니 실제 동작을 정확히 이해하지 못한 채 무작정 
@Transactional을 붙이거나 흐름을 파악하지 못해 시행착오를 겪는 경우가 많습니다.
그래서 이번 글에서는 로그를 확인하면서 프레임워크가 트랜잭션을 실제로 어떻게 시작·합류·커밋·롤백하는지 살펴보려고 합니다.

 

TL;DR

  • 결론: REQUIRED → REQUIRED에서 내부 RuntimeException(unchecked exception)은 바깥까지 전파되어 전체 롤백된다.

가장 보편적으로 사용하는 전파 옵션(REQUIRED)의 로그를 살펴보겠습니다. 

실험 환경

  • Spring Boot 3.5, Hibernate 6.6, Java 21
  • 로깅 레벨 설정 
    아래와 같이 로깅 레벨을 설정해 트랜잭션 인터셉터(AOP), 트랜잭션 매니저(JpaTransactionManager), 그리고 Hibernate의 SQL/파라미터 바인딩 로그를 확인합니다.
logging:
  level:
    # @Transactional AOP 인터셉터: 경계 진입/종료/예외 흐름
    # 예: "Getting transaction for ...", "Completing transaction for ..."
    org.springframework.transaction.interceptor: TRACE

    # Hibernate가 실제로 실행한 SQL
    # 예: "select ...", "update wallet set ..."
    org.hibernate.SQL: DEBUG

    # Hibernate 6 파라미터 바인딩 값    
    org.hibernate.orm.jdbc.bind: TRACE    
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE

    # JPA 트랜잭션 매니저: BEGIN/COMMIT/ROLLBACK, rollback-only, 참여/중단/재개(suspend/resume)
    # 예: "Creating new transaction ...", "Initiating transaction rollback"
    org.springframework.orm.jpa.JpaTransactionManager: DEBUG

 

테스트 시나리오

실제 코드 

  • OuterService: @Transactional(REQUIRED)
    • 내부에서 InnerService(required_addAndThrowRuntime) 호출
    • 기존값 0에  +10 수행
  • InnerService: @Transactional(REQUIRED)
    • +100 후,  RuntimeException 던짐 → 전체 롤백 기대0
public class OuterService {
    @Transactional // REQUIRED
    public void outer_calls_required_then_runtime(Long id) {
        repo.findById(id).orElseThrow().plus(10);
        inner.required_addAndThrowRuntime(id, 100); // 전체 롤백 기대
    }
}


public class InnerService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void required_addAndThrowRuntime(Long id, long amt) {
        repo.findById(id).orElseThrow().plus(amt);
        em.flush(); // DB까지 즉시 반영 시도
        throw new RuntimeException("inner required boom");
    }
}

 

테스트 코드 

@Test
@DisplayName("REQUIRED→REQUIRED에서 RuntimeException 전파시 전체 롤백")
void required_required_runtime_rolls_back_all() {
    // Arrange
    Long id = wallet.getId();
    assertThat(wallet.getBalance()).isEqualTo(0);

    // Act
    assertThrows(RuntimeException.class, () -> outerService.outer_calls_required_then_runtime(id));

    // Assert
    Wallet w = walletRepository.findById(id).orElseThrow();
    assertThat(w.getBalance()).isEqualTo(0); // +10, +100 모두 롤백
}

 

 

 

핵심 로그로 보는 실제 동작

플로우 차트로 살펴보기 

아래는 로그 메세지를 플로우차트로 그려보았는데요. 로그 발생 주체(인터셉터, 트랜잭션 매니저, Hibernate 등) 를 기준으로 흐름을 그려보았습니다. 

로그 메세지 확인 

로그의 필요한 부분만 발췌한 축약본입니다. 

# 바깥 트랜잭션 시작(@Transactional REQUIRED)
DEBUG JpaTransactionManager : Creating new transaction ... OuterService.outer_calls_required_then_runtime
DEBUG JpaTransactionManager : Opened new EntityManager [...]
TRACE TransactionInterceptor: Getting transaction for [OuterService...]

# 같은 트랜잭션 합승(REQUIRED→REQUIRED)
DEBUG JpaTransactionManager : Found thread-bound EntityManager [...]
DEBUG JpaTransactionManager : Participating in existing transaction

# 내부 로직에서 실제 UPDATE (flush 발생) — balance=110
DEBUG org.hibernate.SQL     : update wallet set balance=?, owner=? where id=?
TRACE ...jdbc.bind          : binding parameter (1:BIGINT) <- [110]

# 내부에서 RuntimeException 발생 → 전체 롤백 예약
TRACE TransactionInterceptor: Completing transaction for [InnerService...] after exception: RuntimeException
DEBUG JpaTransactionManager : Participating transaction failed - marking existing transaction as rollback-only
DEBUG JpaTransactionManager : Setting JPA transaction ... rollback-only

# 롤백 수행
DEBUG JpaTransactionManager : Initiating transaction rollback
DEBUG JpaTransactionManager : Rolling back JPA transaction on EntityManager [...]
  • 바깥 트랜잭션 시작
    JpaTransactionManager가 새 트랜잭션을 만들고(Creating new transaction), EntityManager를 열며, TransactionInterceptor가 경계 진입을 잡습니다.
  • 동일 트랜잭션 합승(REQUIRED → REQUIRED)
    같은 스레드에 바인딩된 EntityManager를 재사용하고(Found thread-bound EntityManager), 내부 호출/리포지토리 작업이 기존 트랜잭션에 참여합니다(Participating in existing transaction).
  • 실제 DB 변경 시도(플러시)
    하이버네이트가 UPDATE wallet ... SQL과 바인딩 값을 찍습니다 → flush가 발생해 balance=110이 DB에 반영 시도됨.
  • 내부에서 RuntimeException 발생 → 롤백 예약
    인터셉터가 예외 종료를 기록하고(after exception), 매니저가 트랜잭션을 rollback-only로 표시합니다.
  • 실제 롤백 수행
    Initiating transaction rollback / Rolling back ... 로 전체 트랜잭션 롤백 완료 → 앞서 나간 UPDATE 효과는 되돌려집니다.

UPDATE가 “한 번(=110)”만 찍혔을까?

  • 같은 트랜잭션(+동일 EntityManager)에서 Outer의 +10과 Inner의 +100이 동일 영속성 컨텍스트에 반영됨.
  • flush 시점(로그 확인을 위해 내부 로직에서 강제)에 더티 체킹으로 합산된 값(110) 이 한 번에 UPDATE로 나간 것을 확인.
  • 하지만 mark rollback-only → rollback으로 인해 최종 결과는 0.

기본 전파 옵션인 REQUIRED의 로그를 먼저 살펴보았는데요. 다른 전파 옵션의 동작도 같은 방식으로 해석할 수 있습니다. 예컨대 REQUIRES_NEW에서는 Suspending current transaction / Resuming suspended transaction 로그를, NESTED에서는 savepoint 관련 로그를 확인할 수 있습니다. 

 

저작자표시 비영리 변경금지 (새창열림)

'Spring Framework > Spring' 카테고리의 다른 글

Spring에서 Redis 사용하기 (RedisTemplate)  (3) 2025.08.17
@Transactional 에 대한 것  (2) 2025.08.08
스프링이 자바 빈 등록하는 방법  (1) 2025.06.07
Spring Event 를 활용한 예약 푸쉬 발송  (0) 2023.04.05
Spring Security 디버깅 방법  (0) 2023.03.30
'Spring Framework/Spring' 카테고리의 다른 글
  • Spring에서 Redis 사용하기 (RedisTemplate)
  • @Transactional 에 대한 것
  • 스프링이 자바 빈 등록하는 방법
  • Spring Event 를 활용한 예약 푸쉬 발송
devstep
devstep
웹 백엔드 개발자
  • devstep
    개발 여정
    devstep
  • 전체
    오늘
    어제
    • 분류 전체보기 (91) N
      • Java (24)
      • Spring Framework (17) N
        • Spring (14) N
        • JPA (3)
      • Database (8)
        • RDBMS공통 (1)
        • MySQL (6)
        • Redis (0)
        • Oracle (1)
      • Concept (13)
        • 테스트코드 (4)
        • 클린코드 (2)
        • 성능테스트 (4)
        • 설계 (1)
        • 인증 (1)
        • REST API (1)
      • git (2)
      • Intellij (4)
      • Computer Science (3)
        • 네트워크 (1)
        • 자료구조 (1)
        • 보안 (1)
      • Essay (18) N
        • Learning Essay (10)
        • WIL (8) N
  • 블로그 메뉴

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

  • 공지사항

  • 인기 글

  • 태그

    innodb
    tdd
    applicationcontext
    대칭암호화
    부하테스트도구
    JMeter
    성능테스트
    ClusteredIndex
    자바메모리모델
    DDD
    bean
    블록암호화
    JavaMemoryModel
    storageEngine
    nofile
    보안
    비대칭암호화
    JVM
    테스트코드
    seed
    linux
    component
    인텔리제이실행에러
    aggregate
    단위테스트
    nginx
    클린코드
    springsecurity
    부하테스트
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
devstep
Spring @Transactional 동작, 로그로 확인하기
상단으로

티스토리툴바