우아한 기술블로그에서 설명하지 않은 부분이 헷갈려서 이 글을 쓰게 됐다. 위의 블로그를 읽고 생각하게 된 것은, “RuntimeException 이 발생하면 무조건 rollback 이 발생한다.” 였다. 하지만 이는 사실이 아니었다.
결론을 먼저 말하자면 아래와 같다.
@Transactional
이 붙은 함수가 완료되기 전에 catch Exception 이 있으면 롤백은 일어나지 않는다.@Transactional
이 붙은 함수가 완료된 후에 catch Exception 이 있으면 롤백이 일어난다.
결론 도출을 위한 코드
테스트를 위해 아래와 같이 kotlin 코드를 작성했다.
@Service
@Transactional
class OuterService(
private val memberRepository: MemberRepository,
private val innerService: InnerService,
) {
fun tryCatchAndThrow() {
try {
memberRepository.save(Member())
throw RuntimeException("Outer: intentionally thrown")
} catch (e: RuntimeException) {
println(e)
}
}
fun outerTryCatchAndInnerThrow() {
try {
innerService.`throw`()
} catch (e: RuntimeException) {
println(e)
}
}
fun innerTryCatchAndInnerThrow() {
innerService.tryCatchAndThrow()
}
}
@Service
@Transactional
class InnerService(
private val memberRepository: MemberRepository,
) {
fun `throw`() {
memberRepository.save(Member())
throw RuntimeException("Inner: intentionally thrown")
}
fun tryCatchAndThrow() {
try {
memberRepository.save(Member())
throw RuntimeException("Inner: intentionally thrown")
} catch (e: Exception) {
println(e)
}
}
}
InnerService 를 참조하는 클래스는 OuterSerivce 뿐이라고 가정하고 OuterService 에 대해 아래와 같이 테스트 코드를 작성했다.
@SpringBootTest
@ActiveProfiles("test")
@TestConstructor(autowireMode = ALL)
@Import(TestcontainersConfiguration::class)
class OuterServiceTest(
private val outerService: OuterService,
private val memberRepository: MemberRepository,
) {
@BeforeEach
fun tearDown() {
memberRepository.deleteAll()
}
@Test
fun `innerService 존재 X, outerService 예외 발생 - catch 하면 rollback X`() {
assertDoesNotThrow { outerService.tryCatchAndThrow() }
assertTrue(memberRepository.findAll().isNotEmpty())
}
@Test
fun `innerService 존재 O, innerService 예외 발생 - innerService 가 catch 하면 rollback X`() {
assertDoesNotThrow { outerService.innerTryCatchAndInnerThrow() }
assertTrue(memberRepository.findAll().isNotEmpty())
}
@Test
fun `innerService 존재 O, innerService 예외 발생 - outerServide 가 catch 하더라도 rollback O`() {
assertThrows<UnexpectedRollbackException> { outerService.outerTryCatchAndInnerThrow() }
assertTrue(memberRepository.findAll().isEmpty())
}
}
위 테스트 코드를 보면 트랜잭션이 끝나기 전에 catch Exception 한다면 예외가 발생하지 않고, 결과적으로 롤백이 발생하지 않음을 확인할 수 있다.
로그 확인
아래는 rollback 이 발생하는 유일한 테스트 함수 innerService 존재 O, innerService 예외 발생 - outerServide 가 catch 하더라도 rollback O
의 로그다.
o.h.e.t.internal.TransactionImpl : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
o.h.e.t.internal.TransactionImpl : begin
o.h.e.t.internal.TransactionImpl : committing
o.h.e.t.internal.TransactionImpl : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
o.h.e.t.internal.TransactionImpl : begin
cResourceLocalTransactionCoordinatorImpl : JDBC transaction marked for rollback-only (exception provided for stack trace)
java.lang.Exception: exception just for purpose of providing stack trace
java.lang.RuntimeException: Inner: intentionally thrown
o.h.e.t.internal.TransactionImpl : committing
cResourceLocalTransactionCoordinatorImpl : On commit, transaction was marked for roll-back only, rolling back
o.h.e.t.internal.TransactionImpl : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
o.h.e.t.internal.TransactionImpl : begin
o.h.e.t.internal.TransactionImpl : committing
아래는 rollback 이 발생하지 않는 테스트 함수의 로그로, 테스트 메서드 innerService 존재 X, outerService 예외 발생 - catch 하면 rollback X
의 로그다.
2024-12-10T22:37:34.342+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
2024-12-10T22:37:34.342+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : begin
2024-12-10T22:37:34.345+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : committing
2024-12-10T22:37:34.348+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
2024-12-10T22:37:34.348+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : begin
java.lang.RuntimeException: Outer: intentionally thrown
2024-12-10T22:37:34.350+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : committing
2024-12-10T22:37:34.357+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
2024-12-10T22:37:34.357+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : begin
2024-12-10T22:37:34.360+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : committing
아래는 rollback 이 발생하지 않는 테스트 함수의 로그로, 테스트 메서드 innerService 존재 O, innerService 예외 발생 - innerService 가 catch 하면 rollback X
의 로그다.
2024-12-10T22:37:34.364+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
2024-12-10T22:37:34.365+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : begin
2024-12-10T22:37:34.369+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : committing
2024-12-10T22:37:34.376+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
2024-12-10T22:37:34.376+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : begin
java.lang.RuntimeException: Inner: intentionally thrown
2024-12-10T22:37:34.378+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : committing
2024-12-10T22:37:34.382+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
2024-12-10T22:37:34.382+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : begin
2024-12-10T22:37:34.384+09:00 DEBUG 10375 --- [ Test worker] o.h.e.t.internal.TransactionImpl : committing
테스트 환경 설정
build.gradle.kts
plugins {
val kotlinVersion = "1.9.25"
kotlin("jvm") version kotlinVersion
kotlin("plugin.spring") version kotlinVersion
id("org.springframework.boot") version "3.3.5"
id("io.spring.dependency-management") version "1.1.6"
kotlin("plugin.jpa") version kotlinVersion
}
group = "org.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
runtimeOnly("com.mysql:mysql-connector-j")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:mysql")
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(17)
}
테스트 컨테이너 설정
@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
@Bean
@ServiceConnection
fun mysqlContainer(): MySQLContainer<*> {
return MySQLContainer(DockerImageName.parse(MYSQL_8_0))
}
private object Constant {
const val MYSQL_8_0 = "mysql:8.0"
}
}
application-test.yml
spring:
jpa:
hibernate:
ddl-auto: create
logging:
level:
ROOT: warn
org.hibernate.engine.transaction.internal: DEBUG
org.hibernate.resource.transaction.backend.jdbc: DEBUG
부록: Propagation 에 따른 테스트
IneerService Transactional Propagation 유형에 따라 테스트 코드의 성공 실패 여부를 체크해보았다. 실패해는 경우에는 취소선 (strikethrough) 을 그어서 표시했다.
테스트 메서드: innerService 존재 X, outerService 예외 발생 - catch 하면 rollback X
REQUIRED
SUPPORTS
MANDATORY
REQUIRES_NEW
NOT_SUPPORTED
NEVER
NESTED
테스트 메서드: innerService 존재 O, innerService 예외 발생 - innerService 가 catch 하면 rollback X
REQUIRED
SUPPORTS
MANDATORY
REQUIRES_NEW
NOT_SUPPORTED
NEVER
NESTED
테스트 메서드: innerService 존재 O, innerService 예외 발생 - outerServide 가 catch 하더라도 rollback O
REQUIRED
SUPPORTS
MANDATORY
REQUIRES_NEW
NOT_SUPPORTED
NEVER
NESTED