본문 바로가기
Dev/Spring Framework

『Spring Boot』 선언적 트랜잭션(Declarative Transaction)

by 세대교체 2025. 1. 12.

선언적 트랜잭션 (Declarative Transaction)에 대해 알아보기 전에 트랜잭션과 비교를 한번 해보자.

   
선언적 트랜잭션 설정 파일이나 어노테이션을 통해 코드에 트랜잭션 로직을 직접 작성하지 않고 선언적으로 트랜잭션을 처리하는 방식.
트랜잭션 Java 코드 내에서 트랜잭션 관리 객체를 직접 사용하여 트랜잭션 시작, 커밋, 롤백을 명시적으로 관리하는 방식.

 

간단하게 말하자면 선언적 트랜잭션을 사용하면 PlatformTransactionManager와 TransactionTemplate을 사용하여 코드로 트랜잭션을 제어하는 귀찮은 작업을 하지 않아도 된다. 물론 복잡한 트랜잭션 제어가 필요한 상황이라면 @Transactional 어노테이션에서 지원해 주는 다양한 속성들을 활용해야 되지만 말이다.

 

스프링에서 선언적 트랜잭션 처리 방법은 크게 XML 기반 설정과 어노테이션 기반 설정 두 가지로 나뉘는데 보통 어노테이션 기반의 설정을 한다. 스프링 부트를 사용하면 굳이 XML 설정 없이도 트랜잭션을 관리할 수 있기 때문이다.

 

@EnableTransactionManagement 어노테이션을 사용하여 트랜잭션 관리를 활성화하고 @Transactional을 메서드 또는 클래스에 선언하여 트랜잭션을 적용하면 된다. 

 

 

동작 원리

@Transactional 어노테이션은 스프링의 AOP(Aspect-Oriented Programming)를 기반으로 동작한다. 스프링은 해당 어노테이션이 적용된 클래스나 메서드에 대해 프록시 객체를 생성하고, 메서드 호출 시 프록시를 통해 트랜잭션을 시작하고 종료한다.

 

트랜잭션 처리의 흐름은 아래와 같이 표로 정리했다.

   
1️⃣ 프록시 객체 생성 @Transactional이 선언된 클래스는 프록시 패턴을 통해 프록시 객체가 생성된다.
2️⃣ 프록시 객체의 메서드 호출 시 트랜잭션 처리 1️⃣ 메서드 호출 시 트랜잭션 시작
2️⃣ 메서드 종료 시 예외가 없으면 Commit
3️⃣ RuntimeException 발생 시 Rollback

 

스프링 @Transactional에서 트랜잭션 커밋 및 롤백 처리 규칙은 예외의 타입에 따라 달라지는데, 기본적으로 CheckedException이 발생하면 스프링은 커밋(Commit) 처리한다. 예외가 발생했을 때 트랜잭션을 롤백(Rollback) 하려면 rollbackFor 속성을 명시해야 한다. 반대로 UncheckedException (RuntimeException, Error)은 별도의 속성을 명시하지 않아도 Rollback 처리된다.

 

 

한계

뭐 당연히 선언적 트랜잭션도 만능은 아니기에 몇 가지 한계를 가지는데 아래와 같다. 한번 쭉 읽어보자.

 

메서드 접근 제한자

@Transactional은 기본적으로 public 메서드에서만 적용된다. 즉, private, protected, package-private 메서드에서는 동작하지 않는다. public 접근 제어자가 아닌 메서드에 트랜잭션을 적용하려면 AspectJ 모드를 사용해야 한다.

 

@Transactional 미적용 문제 (Self-Invocation Issue)

@Transactional은 Spring AOP에 의해 프록시 객체가 생성되고, 프록시 객체를 통해 메서드가 호출될 때만 트랜잭션 기능이 적용되는 메커니즘이다. 이때, 클래스 내부에서 this.메서드명() 형태로 같은 클래스의 다른 메서드를 호출하면 프록시를 거치지 않기 때문에 트랜잭션이 적용되지 않는다. 이 개념을 생각보다 모르는 사람이 많다.

 

outerMethod가 호출될 때 innerMethod를 내부적으로 호출하지만 innerMethod는 this.innerMethod()로 호출되기 때문에 프록시를 거치지 않는다. 따라서 innerMethod에 적용된 @Transactional이 동작하지 않고 트랜잭션이 적용되지 않는다.

 

 

해결 방법 1️⃣, 별도의 빈을 생성하여 호출하기

 

HelperService라는 별도의 빈을 생성하고 innerMethod를 해당 빈에서 호출하도록 변경했다. 이러면 HelperService는 프록시 객체로 트랜잭션 기능을 수행하며 innerMethod가 호출될 때 트랜잭션이 올바르게 적용된다.

 

해결 방법 2️⃣, 자기 자신을 주입하여 호출하기 (Proxy Bean Self Injection)

 

성자를 통해 자기 자신을 프록시 형태로 주입하여 self.innerMethod()로 메서드를 호출한다. 이 방식은 this.innerMethod()가 아닌 self.innerMethod()로 호출하기 때문에 프록시를 거치며 트랜잭션이 정상적으로 동작한다.

 

 

해결 방법 3️⃣, @TransactionalEventListener 사용 (이벤트 발행 방식)

 

ApplicationEventPublisher를 사용하여 트랜잭션 이벤트를 발행한다. @TransactionalEventListener를 통해 이벤트를 처리하면, 이벤트 리스너 메서드는 새로운 트랜잭션을 생성하거나 기존 트랜잭션을 사용할 수 있다.

 

 

트랜잭션 모드: Proxy Mode vs AspectJ Mode

Mode 특징 동작 방식
Proxy 기본 설정 모드. Spring AOP를 이용하여 프록시 객체 생성. public 메서드에 대해서만 트랜잭션 적용 가능.
AspectJ AspectJ를 이용하여 바이트코드 조작 방식으로 동작. 모든 메서드에 트랜잭션 적용 가능.

 

Proxy 모드스프링 AOP를 기반으로 프록시 객체를 생성하여 트랜잭션을 적용하는 방식이다. 기본적으로 public 메서드에 대해서만 트랜잭션을 적용할 수 있으며, 내부 호출 시에는 AOP 프록시를 거치지 않아 트랜잭션이 동작하지 않는 문제가 발생할 수 있다.

 

AspectJ 모드바이트코드 조작 방식으로 모든 메서드(public, private, protected)에 트랜잭션을 적용할 수 있다. 스프링 AOP 프록시 방식과 달리 바이트코드를 조작하기 때문에 self-invocation 문제가 발생하지 않는다.

 

설정 코드

 

서비스 코드

 

publicMethod()와 privateMethod() 모두 트랜잭션이 적용된다. AspectJ 모드에서는 바이트코드 조작 방식이기 때문에 내부 메서드 호출 시에도 트랜잭션이 정상적으로 적용된다.

 

 

@Transactional 설정 옵션

속성 타입 설명
propagation Propagation 트랜잭션 전파 방식 (REQUIRED, REQUIRES_NEW, NESTED 등) 설정.
isolation Isolation 데이터베이스 트랜잭션 격리 수준 (READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ 등).
readOnly boolean 읽기 전용 트랜잭션 설정 (true 시 데이터 변경 작업은 예외 발생).
timeout int (초) 트랜잭션 타임아웃 (기본값: -1 = 무제한).
rollbackFor Class<?>[] 롤백할 예외 클래스 배열.
noRollbackFor Class<?>[] 롤백하지 않을 예외 클래스 배열.

 

트랜잭션 전파(Propagation)

   
REQUIRED 기존 트랜잭션이 있으면 이를 사용하고, 없으면 새로운 트랜잭션을 생성한다.
REQUIRES_NEW 항상 새로운 트랜잭션을 생성하며, 기존 트랜잭션이 있으면 이를 일시 정지시킨다.
NESTED 중첩된 트랜잭션을 생성한다. 부모 트랜잭션이 롤백되면 중첩 트랜잭션도 롤백되지만 중첩 트랜잭션의 롤백은 부모 트랜잭션에 영향을 주지 않는다.

 

트랜잭션 전파메서드가 호출될 때 기존 트랜잭션을 사용할지, 새로운 트랜잭션을 생성할지 등을 결정하는 속성이다.

 

격리 수준(Isolation)

격리 수준동시에 실행되는 트랜잭션 간의 데이터 격리 정도를 설정하는 속성이다.

   
READ_UNCOMMITTED 다른 트랜잭션에서 커밋되지 않은 변경사항도 읽을 수 있다.
READ_COMMITTED 다른 트랜잭션에서 커밋된 데이터만 읽을 수 있다.
REPEATABLE_READ 같은 트랜잭션 내에서 같은 데이터를 여러 번 읽어도 항상 동일한 결과를 반환한다.
SERIALIZABLE 가장 높은 수준의 격리로, 트랜잭션을 직렬화하여 동시 실행을 방지한다.

 

 

 

다중 Transaction Manager

스프링은 하나 이상의 PlatformTransactionManager를 사용할 수 있다. @Transactional("managerName")을 통해 특정 트랜잭션 매니저를 지정할 수 있다. 일반적으로 서로 다른 데이터 소스(예: order DB, account DB)에 대해 별도 매니저를 설정할 때 사용된다.

 


출처

https://docs.spring.io/spring-framework/reference/data-access.html

 

Data Access :: Spring Framework

Spring’s comprehensive transaction management support is covered in some detail, followed by thorough coverage of the various data access frameworks and technologies with which the Spring Framework integrates.

docs.spring.io