Spring에서 @Transactional이 적용되지 않거나 롤백이 안 되는 대표 원인을 정리했습니다.
AOP 프록시 방식, private/final 메소드, 같은 클래스 내부 호출(self-invocation), 예외 타입에 따른 롤백 규칙, 트랜잭션 매니저 설정, readOnly 및 propagation 옵션 등 실무 체크리스트로 원인을 빠르게 찾을 수 있습니다.
스프링 트랜잭션(@Transactional) 적용 안되는 이유 (AOP 프록시, private 메소드 등)
스프링에서 @Transactional을 붙였는데도 이런 상황이 종종 발생합니다.
- DB가 커밋돼버림 (롤백 안 됨)
- 로그를 보면 트랜잭션이 시작되지 않은 것 같음
- 분명 @Transactional 붙였는데 효과가 없음
대부분은 “설정이 이상한가?”보다 프록시(AOP) 동작 원리를 모르고 코드가 프록시를 우회했기 때문입니다.
1) @Transactional 동작 방식(프록시) 한 줄 요약
스프링의 트랜잭션은 보통 이렇게 동작합니다.
스프링이 빈(Bean)을 만들 때 “프록시 객체”로 감싸고,
프록시를 통해 호출될 때만 트랜잭션(AOP)이 적용된다.
즉, 프록시를 거치지 않고 호출되면 @Transactional은 “붙어 있어도” 적용되지 않습니다.
2) 적용 안 되는 이유 TOP 8 (실무)
3) 원인 1) 같은 클래스 내부 호출(Self-invocation)
가장 흔한 1순위입니다.
@Service
public class OrderService {
public void outer() {
inner(); // 같은 클래스 내부 호출
}
@Transactional
public void inner() {
// DB 작업
}
}
outer()에서 inner()를 호출하면, 이 호출은 프록시를 거치지 않습니다.
즉, 실제로는 “this.inner()” 호출이라 AOP가 개입할 타이밍이 없어요.
✅ 해결 방법
- 트랜잭션 메소드를 다른 빈(Service)으로 분리해서 호출
- 또는 (비추천) AopContext.currentProxy() 사용
가장 추천되는 구조:
@Service
public class OrderService {
private final OrderTxService orderTxService;
public OrderService(OrderTxService orderTxService) { this.orderTxService = orderTxService; }
public void outer() {
orderTxService.inner(); // 프록시를 통해 호출됨
}
}
@Service
public class OrderTxService {
@Transactional
public void inner() {
// DB 작업
}
}
4) 원인 2) private / final 메소드에 붙임
스프링 AOP는 기본적으로 프록시 기반이라서,
private 메소드는 외부에서 프록시로 호출할 수 없습니다.
@Transactional
private void save() { ... } // 적용 안 됨
또한 CGLIB 프록시를 쓰더라도 final 메소드는 오버라이드가 불가해서 적용이 제한될 수 있습니다.
✅ 해결 방법
- 트랜잭션 메소드는 public(권장) 또는 최소한 프록시가 가로챌 수 있는 형태로
- “외부에서 호출되는 진입점”에 트랜잭션을 붙이기
5) 원인 3) @Transactional이 붙은 클래스/메소드가 빈이 아님
아래처럼 new로 직접 생성하면 트랜잭션이 적용되지 않습니다.
OrderService s = new OrderService(); // 스프링 빈 아님 → 프록시 없음
또는 컴포넌트 스캔 대상이 아니면 빈 등록이 안 되어 프록시도 없습니다.
✅ 해결 방법
- @Service, @Component로 빈 등록
- @ComponentScan 범위 확인
6) 원인 4) 예외가 발생했는데 롤백이 안 됨 (Checked Exception)
스프링 트랜잭션 기본 롤백 규칙:
- ✅ RuntimeException / Error → 기본 롤백
- ❌ Checked Exception(예: Exception, IOException 등) → 기본 롤백 안 함
그래서 아래는 롤백이 안 될 수 있습니다.
@Transactional
public void doWork() throws Exception {
throw new Exception("checked"); // 기본 정책상 롤백 안 함
}
✅ 해결 방법: rollbackFor 지정
@Transactional(rollbackFor = Exception.class)
public void doWork() throws Exception { ... }
7) 원인 5) try-catch로 예외를 먹어버림
트랜잭션은 “예외가 밖으로 던져져야” 롤백 판단을 합니다.
@Transactional
public void save() {
try {
// DB 작업
throw new RuntimeException("fail");
} catch (Exception e) {
// 로그만 찍고 끝내면 → 트랜잭션은 정상 종료(커밋)될 수 있음
}
}
✅ 해결 방법
- 예외를 다시 던지기(rethrow)
- 또는 롤백 마킹
catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw e;
}
8) 원인 6) 트랜잭션 전파(Propagation) 설정 실수
예를 들어 REQUIRES_NEW는 새 트랜잭션을 만들고,
NOT_SUPPORTED는 트랜잭션을 아예 중지시킵니다.
자주 헷갈리는 케이스:
- 바깥 트랜잭션에서 호출했는데 안쪽이 REQUIRES_NEW라 따로 커밋됨
- 안쪽이 NOT_SUPPORTED라 트랜잭션 없이 실행됨
✅ 해결 방법
- 기본값 REQUIRED를 유지하고 정말 필요한 경우만 변경
- “롤백 범위”를 의도대로 설계했는지 확인
9) 원인 7) readOnly, flush, autocommit 착시
@Transactional(readOnly = true)에서는
JPA/Hibernate 기준으로 flush가 제한되거나 최적화가 들어가면서 “쓰기”가 기대대로 안 될 수 있습니다.
또, “롤백이 안 된 것처럼 보이는” 착시가 발생하는 경우:
- 예외가 발생했는데 이미 다른 곳에서 commit된 트랜잭션
- autocommit 설정/직접 커넥션 사용(템플릿 우회)
✅ 해결 방법
- readOnly 옵션 확인
- JDBC 직접 사용 시 DataSource/TxManager 라인 확인
10) 원인 8) 멀티 DB/트랜잭션 매니저 지정 문제
데이터소스가 2개 이상이면
- 어떤 TxManager를 쓰는지
- 특정 Mapper/JPA가 어떤 DataSource에 붙는지
헷갈리면서 “트랜잭션이 적용 안 되는 것처럼” 보일 수 있습니다.
✅ 해결 방법
- @Transactional(transactionManager = "xxxTransactionManager") 명시
- MapperScan / SqlSessionFactory / EntityManager 연결 확인
✅ 결론: 가장 빠른 점검 순서
@Transaction이 안 먹는다고 느껴지면 아래 순서로 보면 거의 잡힙니다.
- 같은 클래스 내부 호출(Self-invocation)인지
- 트랜잭션 메소드가 public이고 “외부에서 프록시로 호출되는 진입점”인지
- 클래스가 스프링 빈인지(스캔/주입)
- 예외가 Runtime인지 Checked인지, rollbackFor 필요 여부
- try-catch로 예외를 삼키고 있지 않은지
- propagation/readOnly 설정
- 멀티 DB면 transactionManager 지정
'개발 > ERROR 모음' 카테고리의 다른 글
| Cannot load JDBC driver class 해결 방법 (원인 7가지 체크리스트) (0) | 2026.03.09 |
|---|---|
| Spring Security JWT 401/403 오류 해결 가이드 (필터 순서 포함) (0) | 2026.03.02 |
| MyBatis Invalid bound statement (not found) 해결 방법 (원인 7가지 체크리스트) (0) | 2026.02.24 |
| Spring Boot Whitelabel Error Page 해결 가이드 (404/500 원인별 정리) (0) | 2026.02.17 |
| Spring BeanCreationException 원인 TOP 5 (실무 해결 체크리스트) (0) | 2026.02.10 |