본문 바로가기

Java/Spring

REQUIRES_NEW 사용 시 외부, 내부 트랜잭션의 독립 실행 테스트

더보기

REQUIRES_NEW는 외부 트랜잭션과 내부 트랜잭션이 서로 독립된 물리 트랜잭션을 구성합니다.

따라서 이론적으로는 내부 트랜잭션에서 롤백이 발생하더라도 외부 트랜잭션에 영향을 주지 않습니다. 실제로도 외부 트랜잭션과 독립적으로 동작하는지 테스트합니다.

테스트 코드 작성 및 동작 확인

설정 클래스에서 OuterTransactionInnerTransaction 두 개의 Bean을 등록합니다.

외부 트랜잭션에서는 전파 속성으로 REQUIRED를 지정합니다.

그리고 트랜잭션 범위로 지정된 메소드에서는 내부 트랜잭션 메소드를 호출합니다.

public class OuterTransaction
{
	protected InnerTransaction inner;
	
	public OuterTransaction(InnerTransaction inner)
	{
		this.inner = inner;
	}
	
	@Transactional(propagation = Propagation.REQUIRED)
	public void outer()
	{
		System.out.println("OuterTransaction.outer()");
		inner.inner();
	}
}

내부 트랜잭션에서는 전파 속성으로 REQUIRES_NEW를 지정합니다.

내부 트랜잭션에서는 RuntimeException을 임의로 발생시켜 트랜잭션을 롤백합니다.

public class InnerTransaction
{
	@Transactional(propagation = Propagation.REQUIRES_NEW)
	public void inner()
	{
		System.out.println("InnerTransaction.inner()");
		throw new RuntimeException();
	}
}

애플리케이션을 실행하고 출력되는 로그를 확인합니다.

더보기

트랜잭션에 대한 디버깅을 위해 Logback 모듈을 사용하여 트랜잭션 동작 로그를 DEBUG 모드로 표시합니다.

build.gradle 파일에서 다음 의존성을 추가합니다.

dependencies {
    implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25'
    implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'
}

src/main/resources에서 logback.xml 파일을 생성합니다.

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d %5p %c{2} - %m%n</pattern>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="stdout" />
    </root>

    <logger name="org.springframework.jdbc" level="DEBUG" />
</configuration>

내부 트랜잭션이 RuntimeException으로 롤백되더라도 외부 트랜잭션은 정상적으로 커밋되어야합니다.

실제로도 기대한것처럼 동작하는지 로그를 살펴봅니다.

2023-12-19 01:19:07,925 DEBUG o.s.j.d.DataSourceTransactionManager - Acquired Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@294bdeb4]]] for JDBC transaction
2023-12-19 01:19:07,928 DEBUG o.s.j.d.DataSourceTransactionManager - Switching JDBC Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@294bdeb4]]] to manual commit
OuterTransaction.outer()
2023-12-19 01:19:07,938 DEBUG o.s.j.d.DataSourceTransactionManager - Suspending current transaction, creating new transaction with name [chap08.InnerTransaction.inner]
2023-12-19 01:19:07,938 DEBUG o.s.j.d.DataSourceTransactionManager - Acquired Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@aa21042]]] for JDBC transaction
2023-12-19 01:19:07,938 DEBUG o.s.j.d.DataSourceTransactionManager - Switching JDBC Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@aa21042]]] to manual commit
InnerTransaction.inner()
2023-12-19 01:19:07,947 DEBUG o.s.j.d.DataSourceTransactionManager - Initiating transaction rollback
2023-12-19 01:19:07,947 DEBUG o.s.j.d.DataSourceTransactionManager - Rolling back JDBC transaction on Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@aa21042]]]
2023-12-19 01:19:07,952 DEBUG o.s.j.d.DataSourceTransactionManager - Releasing JDBC Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@aa21042]]] after transaction
2023-12-19 01:19:07,952 DEBUG o.s.j.d.DataSourceUtils - Returning JDBC Connection to DataSource
2023-12-19 01:19:07,953 DEBUG o.s.j.d.DataSourceTransactionManager - Resuming suspended transaction after completion of inner transaction
2023-12-19 01:19:07,953 DEBUG o.s.j.d.DataSourceTransactionManager - Initiating transaction rollback
2023-12-19 01:19:07,953 DEBUG o.s.j.d.DataSourceTransactionManager - Rolling back JDBC transaction on Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@294bdeb4]]]
2023-12-19 01:19:07,957 DEBUG o.s.j.d.DataSourceTransactionManager - Releasing JDBC Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@294bdeb4]]] after transaction
2023-12-19 01:19:07,958 DEBUG o.s.j.d.DataSourceUtils - Returning JDBC Connection to DataSource
코드 비고
Line 1:3 Switching JDBC Connection 외부 트랜잭션을 시작합니다.
OuterTransaction.outer()
Line 4:7 Suspending current transaction, creating new transaction with name 외부 트랜잭션을 일시 중지하고 내부 트랜잭션을 시작합니다.
Switching JDBC Connection
InnerTransaction.inner()
Line 8 Initiating transaction rollback 내부 트랜잭션을 롤백합니다.
Line 12:13 Resuming suspended transcation after completion of inner transaction 외부 트랜잭션을 재개하고 롤백합니다.
Initiating transaction rollback

내부 트랜잭션은 REQUIRES_NEW로 지정되어 있기 때문에 외부 트랜잭션과 완전히 독립될 것으로 기대했습니다.

하지만 실제로는 내부 트랜잭션이 롤백되자 외부 트랜잭션이 함께 롤백되고 있습니다.

외부 트랜잭션과 독립되도록 코드 수정

사실 내부 트랜잭션의 롤백이 외부 트랜잭션에 영향을 미친것은 아닙니다.

단순히 외부 메소드에서 RuntimeException을 catch했기 때문에 스스로도 트랜잭션을 롤백하는 것입니다.

자바에서는 예외가 발생했을 때 별도의 예외 처리를 하지 않으면 콜스택을 하나씩 제거하면서 최초 호출된 곳까지 전달되기 때문입니다.

public class OuterTransaction
{
	protected InnerTransaction inner;
	
	public OuterTransaction(InnerTransaction inner)
	{
		this.inner = inner;
	}
	
	@Transactional(propagation = Propagation.REQUIRED)
	public void outer()
	{
		System.out.println("OuterTransaction.outer()");
		try {
			inner.inner();
		} catch (RuntimeException e) {
			System.out.println("OuterTransaction.inner() exception");
		}
	}
}

외부 메소드에서 내부 메소드 호출 코드를 예외 처리하고 애플리케이션을 실행합니다.

2023-12-19 01:34:35,402 DEBUG o.s.j.d.DataSourceTransactionManager - Acquired Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@294bdeb4]]] for JDBC transaction
2023-12-19 01:34:35,405 DEBUG o.s.j.d.DataSourceTransactionManager - Switching JDBC Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@294bdeb4]]] to manual commit
OuterTransaction.outer()
2023-12-19 01:34:35,422 DEBUG o.s.j.d.DataSourceTransactionManager - Suspending current transaction, creating new transaction with name [chap08.InnerTransaction.inner]
2023-12-19 01:34:35,422 DEBUG o.s.j.d.DataSourceTransactionManager - Acquired Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@aa21042]]] for JDBC transaction
2023-12-19 01:34:35,422 DEBUG o.s.j.d.DataSourceTransactionManager - Switching JDBC Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@aa21042]]] to manual commit
InnerTransaction.inner()
2023-12-19 01:34:35,431 DEBUG o.s.j.d.DataSourceTransactionManager - Initiating transaction rollback
2023-12-19 01:34:35,431 DEBUG o.s.j.d.DataSourceTransactionManager - Rolling back JDBC transaction on Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@aa21042]]]
2023-12-19 01:34:35,436 DEBUG o.s.j.d.DataSourceTransactionManager - Releasing JDBC Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@aa21042]]] after transaction
2023-12-19 01:34:35,436 DEBUG o.s.j.d.DataSourceUtils - Returning JDBC Connection to DataSource
2023-12-19 01:34:35,436 DEBUG o.s.j.d.DataSourceTransactionManager - Resuming suspended transaction after completion of inner transaction
OuterTransaction.inner() exception
2023-12-19 01:34:35,437 DEBUG o.s.j.d.DataSourceTransactionManager - Initiating transaction commit
2023-12-19 01:34:35,437 DEBUG o.s.j.d.DataSourceTransactionManager - Committing JDBC transaction on Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@294bdeb4]]]
2023-12-19 01:34:35,441 DEBUG o.s.j.d.DataSourceTransactionManager - Releasing JDBC Connection [ProxyConnection[PooledConnection[com.mysql.cj.jdbc.ConnectionImpl@294bdeb4]]] after transaction
2023-12-19 01:34:35,441 DEBUG o.s.j.d.DataSourceUtils - Returning JDBC Connection to DataSource
코드 비고
Line 1:3 Switching JDBC Connection 외부 트랜잭션을 시작합니다.
OuterTransaction.outer()
Line 4:7 Suspending current transaction, creating new transaction with name 외부 트랜잭션을 일시 중지하고 내부 트랜잭션을 시작합니다.
Switching JDBC Connection
InnerTransaction.inner()
Line 8 Initiating transaction rollback 내부 트랜잭션을 롤백합니다.
Line 12:14 Resuming suspended transcation after completion of inner transaction 외부 트랜잭션을 재개하고 커밋합니다.
Initiating transaction commit

이번에는 기대한 것처럼 외부 트랜잭션이 내부 트랜잭션 롤백과 독립되어 커밋되는 것을 확인 할 수 있습니다.

정리 및 복습

  • REQUIRES_NEW는 외부 트랜잭션과 내부 트랜잭션이 서로 독립된 물리 트랜잭션을 구성합니다.
  • 따라서 내부 트랜잭션에서 롤백이 발생하더라도 외부 트랜잭션에 영향을 주지 않습니다.
  • 하지만 실제 동작은 내부 트랜잭션에서 RuntimeException과 함께 롤백되었을 때 외부 트랜잭션이 함께 롤백됩니다.
  • 자바에서는 예외가 발생했을 때 별도의 예외 처리를 하지 않으면 콜스택을 하나씩 제거하면서 최초 호출된 곳까지 전달됩니다.
  • 따라서 외부 메소드에서 내부 메소드 호출 코드를 예외 처리하는 것으로 서로 독립시킬 수 있습니다.