[Spring] @Transactional 동작하지 않는 경우
환경 : Spring Boot 3.x, Kotlin 1.9, JPA
1. 원인
private/protected메서드에 적용- 같은 클래스 내부에서 호출 (self-invocation, 프록시 우회)
- checked exception 발생 — 기본 롤백 대상 아님
readOnly = true인데 데이터 변경 시도- propagation 옵션 잘못 설정
2. 해결 방법
2.1) public 메서드로 선언
Spring AOP 프록시는 public 메서드만 가로챔. private, protected, package-private 은 어드바이스가 적용되지 않음
// 동작 안 함
@Service
class MemberService(private val memberRepository: MemberRepository) {
@Transactional
private fun saveInternal(member: Member) { // private → 무시
memberRepository.save(member)
}
}
// 정상
@Service
class MemberService(private val memberRepository: MemberRepository) {
@Transactional
fun save(member: Member) { // public
memberRepository.save(member)
}
}
2.2) 자기호출 회피 — 별도 빈으로 분리
@Cacheable, @Async와 동일한 문제. 같은 클래스 내부에서 호출하면 프록시를 거치지 않아 어드바이스가 무시됨
// 동작 안 함
@Service
class OrderService(private val orderRepository: OrderRepository) {
fun createOrder(request: OrderRequest) {
validate(request)
save(request) // 내부 호출 → @Transactional 무시
}
@Transactional
fun save(request: OrderRequest) {
orderRepository.save(Order.from(request))
}
}
// 정상 — 별도 빈으로 분리
@Service
class OrderService(private val orderTransactionService: OrderTransactionService) {
fun createOrder(request: OrderRequest) {
validate(request)
orderTransactionService.save(request) // 외부 빈 호출
}
}
@Service
class OrderTransactionService(private val orderRepository: OrderRepository) {
@Transactional
fun save(request: OrderRequest) {
orderRepository.save(Order.from(request))
}
}
2.3) checked exception 롤백 — rollbackFor 명시
기본 롤백 대상은 RuntimeException과 Error만. checked exception(예: IOException, 사용자 정의 BusinessException : Exception)은 던져도 커밋됨
// 동작 안 함 — IOException 던져도 커밋됨
@Transactional
fun process() {
memberRepository.save(member)
throw IOException("외부 호출 실패") // 롤백 안 됨
}
// 정상 — Exception 전체 롤백
@Transactional(rollbackFor = [Exception::class])
fun process() {
memberRepository.save(member)
throw IOException("외부 호출 실패") // 롤백됨
}
Kotlin은 모든 예외를 unchecked 로 처리하지만, Java 라이브러리에서 던지는 checked exception 은 여전히 위 규칙을 따름. 확실히 하려면 rollbackFor = [Exception::class] 명시
2.4) propagation 옵션 정리
가장 자주 헷갈리는 3가지
// REQUIRED (기본값) — 기존 트랜잭션에 참여, 없으면 새로 생성
@Transactional(propagation = Propagation.REQUIRED)
fun save(member: Member) { ... }
// REQUIRES_NEW — 항상 새 트랜잭션 (기존 것 잠시 보류)
// 부모가 롤백돼도 살아남아야 하는 로그 기록 등에 사용
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun writeAuditLog(log: AuditLog) { ... }
// NESTED — savepoint 기반 부분 롤백 가능
// 부모 롤백 시 같이 롤백, 자식만 롤백 시 부모는 살아남음
@Transactional(propagation = Propagation.NESTED)
fun trySave(member: Member) { ... }
주의 — REQUIRES_NEW 도 자기호출 시 동일하게 무시됨. 같은 클래스에서 호출하면 propagation 전체가 의미 없어짐
@Service
class OrderService {
@Transactional
fun createOrder() {
save()
writeLog() // 자기호출 → REQUIRES_NEW 무시, 같은 트랜잭션에서 실행
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun writeLog() { ... }
}
3. 트랜잭션 적용 여부 디버깅
의심될 때는 코드와 로그로 직접 확인
3.1) 코드로 확인
import org.springframework.transaction.support.TransactionSynchronizationManager
@Service
class OrderService {
@Transactional
fun process() {
val active = TransactionSynchronizationManager.isActualTransactionActive()
val name = TransactionSynchronizationManager.getCurrentTransactionName()
println("트랜잭션 활성: $active, 이름: $name")
}
}
active = false 면 어드바이스가 적용되지 않은 것
3.2) 로그로 확인
# application.yml
logging:
level:
org.springframework.transaction.interceptor: TRACE
정상 적용 시 출력
Getting transaction for [com.example.OrderService.createOrder]
Completing transaction for [com.example.OrderService.createOrder]
위 로그가 안 찍히면 어드바이스 자체가 적용되지 않은 것. 1번 원인부터 점검
※ 대부분의 케이스가 자기호출 또는 private 메서드. 디버깅 전에 먼저 의심할 것
'Java' 카테고리의 다른 글
| @Cacheable 캐시가 동작하지 않는 경우 (0) | 2026.04.07 |
|---|---|
| [Spring] Profile 기준 Property 구분 적용 (0) | 2023.06.28 |
| [SpringBoot] Gradle 변수 Property 활용 (0) | 2022.05.30 |
| [MyBatis] List 형식 멤버 변수 조회 (0) | 2021.02.12 |
| [Spring] Web Cache 적용 (0) | 2020.12.19 |











