from Dictionary - Transactional
## 코드
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default -1;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String value() default "";
}
정의
@Transactional은 선언적으로 트랜잭션 관리를 가능하게 해주는 annotation 기반 메타데이터다.- 트랜잭션 경계(begin/ commit/ rollback)을 자동으로 설정하는 AOP annotation이다.
- 메소드, 클래스 선두에 붙일 수 있다.
@Target({ElementType.METHOD, ElementType.TYPE}) - 정상 종료 시 commit, 비정상 종료 시 rollback을 자동으로 수행한다.
- 트랜잭션 제어 코드를 매 번 작성하지 않더라도 일관적인 트랜잭션 처리를 보장받을 수 있게 해준다.
- DB의 트랜잭션이 아닌 트랜잭션 관리 추상화 계층에서 동작하는 논리적 트랜잭션이다.
선언적?
- spring은 transaction 관리에 대해서 두 가지 방법을 제시한다.
- 프로그래밍 방식: 코드 내 직접 트랜잭션 경계를 설정
- 선언적 방식 :
@Transactional로 Spring이 AOP를 통해서 자동 적용
동작
- client 호출
- AOP Proxy 객체가 가로챈다.
- JDK Dynamic Proxy(Interface 기반)
- CGLIB (bytecode 조작)
- TransactionInterceptor advisor 등록
- invoke로 처리한다. ```java package org.springframework.transaction.interceptor;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.lang.Nullable;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionManager;
@SuppressWarnings(“serial”)
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}
//후략 }
```java
public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// If the transaction attribute is null, the method is non-transactional.
TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
final TransactionManager tm = determineTransactionManager(txAttr);
// Reactive 관련
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager rtm) {
boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
boolean hasSuspendingFlowReturnType = isSuspendingFunction &&
COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName());
ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
Class<?> reactiveType =
(isSuspendingFunction ? (hasSuspendingFlowReturnType ? Flux.class : Mono.class) : method.getReturnType());
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(reactiveType);
if (adapter == null) {
throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type [" +
method.getReturnType() + "] with specified transaction manager: " + tm);
}
return new ReactiveTransactionSupport(adapter);
});
return txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, rtm);
}
//PlatformTransactionManager를 설정한다.
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
// 동기식 트랜잭션 처리 (get/commit/rollback)
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked. retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
if (retVal != null && txAttr != null) {
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null) {
// Future, Vavr 타입 처리 (비동기 완료 검사)
if (retVal instanceof Future<?> future && future.isDone()) {
try {
future.get();
}
catch (ExecutionException ex) {
if (txAttr.rollbackOn(ex.getCause())) {
status.setRollbackOnly();
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
else if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
//
else {
Object result;
final ThrowableHolder throwableHolder = new ThrowableHolder();
// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
try {
result = cpptm.execute(txAttr, status -> {
TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
try {
Object retVal = invocation.proceedWithInvocation();
if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
return retVal;
}
catch (Throwable ex) {
if (txAttr.rollbackOn(ex)) {
// A RuntimeException: will lead to a rollback.
if (ex instanceof RuntimeException runtimeException) {
throw runtimeException;
}
else {
throw new ThrowableHolderException(ex);
}
}
else {
// A normal return value: will lead to a commit.
throwableHolder.throwable = ex;
return null;
}
}
finally {
cleanupTransactionInfo(txInfo);
}
});
}
catch (ThrowableHolderException ex) {
throw ex.getCause();
}
catch (TransactionSystemException ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
ex2.initApplicationException(throwableHolder.throwable);
}
throw ex2;
}
catch (Throwable ex2) {
if (throwableHolder.throwable != null) {
logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
}
throw ex2;
}
// Check result state: It might indicate a Throwable to rethrow.
if (throwableHolder.throwable != null) {
throw throwableHolder.throwable;
}
return result;
}
}
}
- PlatformTransactionManager가 직접 트랜잭션을 열고/ 닫고/ 롤백
PlatformTransactionManager?
- Spring의 tx 처리 전략을 추상화한 인터페이스
| 구현체 이름 | 대상 기술 스택 | 주요 특징 | 비고 |
|---|---|---|---|
| DataSourceTransactionManager | JDBC | 순수 JDBC용 트랜잭션 매니저, Connection 직접 제어 | 가장 단순, autoCommit=false 직접 |
| JpaTransactionManager | JPA (Hibernate 등) | EntityManager 기반 트랜잭션, JPA flush 연동 | JPA 전용, 스프링 부트에서 기본 선택 |
| HibernateTransactionManager | Hibernate Native | Hibernate Session 기반 직접 제어 | JPA 미사용 시 사용, Spring 6에서 deprecated 예정 |
| JtaTransactionManager | JTA (분산 트랜잭션) | XA 트랜잭션, 2PC 지원, Atomikos/Narayana 연동 가능 | Java EE, 대규모 분산 시스템용 |
| ChainedTransactionManager | 다중 데이터소스 | 여러 TransactionManager 묶어서 순차 처리 | best-effort 1PC, 완전한 원자성 없음 |
| ReactiveTransactionManager | WebFlux + R2DBC | 논블로킹 트랜잭션, Mono/Flux 전파 | JDBC와 완전 별개 |
모드 proxy vs. aspjectj
@Configuration
@EnableTransactionManagement(mode = AdviceMode.PROXY) // 기본값
public class TransactionConfig { }
Proxy
- 런타임 프록시 생성(JDK DynamicProxy/ CGLIB)
- 기본 값
- 프록시 기반 AOP
- Spring이 대상 bean을 proxy 객체로 감싼다.
@Transactional이 붙은 메소드를 호출하면 -> 프록시가 가로채서 트랜잭션 처리- 실제 클래스는 따로 수정하지 않는다. 단지, 호출 경로를 조작한다.
- 프록시 객체가 중간에 interceptor를 삽입
- AOP 적용 시점 : RUNTIME
- 내부 포출(
this.method())은 프록시를 거치지 않기에 트랜잭션 적용이 안딘다.ASPECTJ
- 위빙 기반 AOP
- 대상 클래스의 byteCode에 AOP로직을 직접 삽입
- 프록시 없이도 동작하며,
this.method()같은 내부 호출조 적용된다. - 위빙 시점
- Compile-Time Weaving : .class 파일에 삽입
- Load-Time weaving : JVM 시작 시
- Post-compile weaving : .class 빌드 후 aspect 적용
Proxy 기반 내부 호출
- 프록시는 클라이언트가 호출할 때 프록시에 트랜잭션을 덮어 씌우는 개념이다.
- 따라서 프록시 내부에서 메소드 호출은 프록시의 advice(TransactionInterceptor)를 거치지 않는다.
- 따라서 내부 호출은
@Transactional이 적용되지 않는다.내부 호출 해결
- aspectj 모드로 변경
- 서비스 분리
- AOP 강제 적용
- 내부에서 현재 프록시를 얻어서 호출
Annotation의 속성 값들
| 속성명 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| propagation | Propagation | REQUIRED | 트랜잭션 전파 방식 |
| isolation | Isolation | DEFAULT | DB 격리 수준 |
| timeout | int | -1 | 트랜잭션 제한 시간 (초), -1이면 무제한 |
| readOnly | boolean | false | 읽기 전용 힌트 |
| rollbackFor | Class<?>[] | 없음 | 롤백할 예외 타입 지정 |
| noRollbackFor | Class<?>[] | 없음 | 롤백하지 않을 예외 타입 지정 |
| value | String | ”” | 트랜잭션 이름 (로깅 또는 관리용 태그 등) |
Propagation
| 값 | 설명 | | ————- | ————————————— | | REQUIRED | 트랜잭션이 존재하면 참여, 없으면 새로 시작 (기본값) | | REQUIRES_NEW | 기존 트랜잭션 무조건 중단하고 새 트랜잭션 시작 | | NESTED | 부모 트랜잭션 안에서 savepoint 기반 하위 트랜잭션 시작 | | SUPPORTS | 트랜잭션 있으면 참여, 없으면 그냥 트랜잭션 없이 실행 | | NOT_SUPPORTED | 기존 트랜잭션 일시 중단, 트랜잭션 없이 실행 | | NEVER | 트랜잭션이 있으면 예외 발생 | | MANDATORY | 트랜잭션이 없으면 예외 발생, 무조건 있어야 함 |
Isolation
(org.springframework.transaction.annotation.Isolation)
| 값 | DB 명칭 | 설명 |
|---|---|---|
| DEFAULT | DB 기본값 따름 | RDBMS마다 다름 (MySQL은 REPEATABLE READ) |
| READ_UNCOMMITTED | 읽기 허용 | 커밋되지 않은 데이터도 읽을 수 있음 (가장 낮은 수준) |
| READ_COMMITTED | 커밋된 데이터만 | 다른 트랜잭션이 커밋한 데이터만 읽음 (Oracle 기본값) |
| REPEATABLE_READ | 반복 읽기 보장 | 같은 쿼리 결과가 항상 동일함 (MySQL 기본값) |
| SERIALIZABLE | 완전 직렬화 수준 | 동시성 거의 없음, 가장 엄격 (성능 저하 큼) |
트랜잭션에 따른 허용 정도
| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| READ_UNCOMMITTED | ✅ | ✅ | ✅ |
| READ_COMMITTED | ❌ | ✅ | ✅ |
| REPEATABLE_READ | ❌ | ❌ | ✅ |
| SERIALIZABLE | ❌ | ❌ | ❌ |
readOnly
- DB 또는 ORM에게 쓰기 없이 읽기만 할 예정이라는 힌트를 준다.
- JPA에서는 flush를 안하게 최적화 한다.
- MySQL의 경우 SELECT에
(SET TRANSACTION READ ONLY)힌트를 준다.
그 외
Q1. @Transactional(readOnly = true)인데도 update 쿼리가 나가는 이유
- readOnly=true가 SQL 실행을 제한하는 목적은 아니다.
- flush를 내부적으로 처리하지 않는다.
- 그러나 entityManager로 flush(), save() 등을 명시하면 에러 없이 Update 등이 나간다.
-> A1. Hibernate flush 모드를 FlushMode.MANUAL로 설정
Q2. 예외를 던졌는데 rollback이 안되는 경우
- @Transactional 내에서 예외가 발생했지만 DB에 commit
-> A2. 예외에 따라 다르다.
| 예외 타입 | 기본 롤백 여부 | | ——————————————— | ———— | | RuntimeException (e.g., NullPointerException) | ✅ 롤백 | | Error (e.g., OutOfMemoryError) | ✅ 롤백 | | Checked Exception (e.g., IOException) | ❌ 커밋됨 | | 예외를 try-catch로 잡고 처리 | ❌ 커밋됨 |
Q3. private 메소드는 왜 @Transactional을 못 붙이는가?
-> A3. AOP 기반 프록시 구조 문제
| 개념 | 설명 | | —————————- | ————————————– | | 프록시 기반 AOP | Spring은 @Transactional을 프록시 객체로 감쌈 | | 자기 자신 호출 (this.method()) | 프록시를 거치지 않음 → 트랜잭션 로직 실행 안 됨 | | private 메서드 | 프록시에서 호출 불가 → 애초에 인터셉트 대상이 아님 |
Q4. @TransactionalEventListner ?
- Spring에서 제공하는 트랜잭션 후 처리용 이벤트 리스너 어노테이션
- DB 트랜잭션이 성공적으로 커밋된 후 특정 작업을 하고 싶을 때 사용
동작 방식
- 이벤트 발생 : ApplicationEventPublisher.publishEvent() 호출
- 트랜잭션 감지 : Spring이 현재 트랜잭션에 이벤트 등록
- commit : TransactionSynchronizationManager를 통해서 리스너 실행
- listener 실행: @TransactionalEventListener 메소드 실행
동작 제어 시점
| 값 | 의미 | | —————— | ———————– | | AFTER_COMMIT (기본값) | 트랜잭션 커밋 후 실행 | | BEFORE_COMMIT | 커밋 직전 실행 | | AFTER_ROLLBACK | 롤백 시 실행 | | AFTER_COMPLETION | 트랜잭션 종료 시 (커밋/롤백 무관) 실행 |