@Transactional 안 먹히는 이유와 동작 원리
@Transactional은 단순히 어노테이션을 붙인다고 동작하는 기능이 아닙니다. Spring의 프록시 기반 AOP를 통해 동작하기 때문에, 특정 조건이 맞지 않으면 트랜잭션이 전혀 적용되지 않습니다.
핵심은 "프록시 객체를 통해 호출되어야 한다"는 점입니다. 이 조건이 깨지면 어노테이션이 있어도 무시됩니다.
Spring AOP 기반 트랜잭션 구조
Spring은 @Transactional이 붙은 메서드를 직접 실행하지 않습니다. 대신 프록시 객체를 만들어서 그 프록시가 트랜잭션을 시작하고 실제 메서드를 호출합니다.
Client → Proxy → Transaction Start → Target Method → Commit/Rollback
이 구조를 이해하지 못하면 "왜 안 되는지"를 계속 감으로 찾게 됩니다.
같은 클래스 내부 호출 (self-invocation)
@Transactional이 가장 많이 실패하는 케이스입니다. 같은 클래스 내부에서 메서드를 호출하면 프록시를 거치지 않습니다.
@Service
public class UserService {
public void outer() {
inner(); // 트랜잭션 적용 안됨
}
@Transactional
public void inner() {
// transactional logic
}
}
이 경우 inner()는 프록시가 아닌 자기 자신을 직접 호출하기 때문에 트랜잭션이 적용되지 않습니다.
이 문제는 서비스 분리로 해결하는 경우가 많습니다.
해결 방법
@Service
public class UserService {
private final InnerService innerService;
public UserService(InnerService innerService) {
this.innerService = innerService;
}
public void outer() {
innerService.inner(); // 프록시를 통해 호출됨
}
}
실무에서는 이 구조로 분리하는 것이 가장 깔끔합니다.
private 메서드에 @Transactional
이 부분도 자주 실수합니다. private 메서드는 프록시가 가로챌 수 없습니다.
@Transactional
private void save() {
// 적용 안됨
}
Spring AOP는 public 메서드 기준으로 동작하기 때문에 private, protected는 적용 대상이 아닙니다.
이 경우는 단순하게 public으로 변경하는 것이 맞습니다.
Spring Bean이 아닌 객체
new 키워드로 직접 생성한 객체에서는 @Transactional이 동작하지 않습니다.
UserService userService = new UserService();
userService.save(); // 트랜잭션 없음
Spring 컨테이너가 관리하지 않는 객체는 프록시가 생성되지 않기 때문에 트랜잭션이 적용될 수 없습니다.
항상 DI를 통해 주입받은 Bean을 사용해야 합니다.
checked exception과 rollback
트랜잭션이 적용되었는데도 롤백이 안 되는 경우도 있습니다.
기본적으로 Spring은 RuntimeException만 롤백합니다.
@Transactional(rollbackFor = Exception.class)
public void save() throws Exception {
throw new Exception();
}
checked exception까지 롤백하려면 rollbackFor를 명시해야 합니다.
이 부분은 테스트 없이 넘어가면 나중에 데이터 정합성 문제로 이어질 수 있습니다.
실무에서 판단 기준
@Transactional이 안 먹히는 문제는 대부분 설정 문제가 아니라 구조 문제입니다.
다음 기준으로 점검하는 것이 빠릅니다.
- 프록시를 통해 호출되는 구조인지 확인
- public 메서드인지 확인
- Spring Bean으로 관리되는 객체인지 확인
- self-invocation 여부 확인
- 예외 타입과 rollback 정책 확인
이 다섯 가지만 확인해도 대부분의 문제는 해결됩니다.
정리
@Transactional은 단순 어노테이션이 아니라 AOP 기반 기능입니다.
프록시를 거쳐야 한다는 전제를 이해하면 대부분의 문제는 구조적으로 설명이 됩니다.
실무에서는 "어디서 호출되느냐"를 먼저 보는 것이 가장 빠른 접근입니다.
'개발 > JAVA' 카테고리의 다른 글
| [JAVA] LazyInitializationException 실무에서 해결한 방법 (0) | 2026.05.15 |
|---|---|
| [JAVA] Embedded Tomcat 기동 실패 원인 분석 (0) | 2026.05.14 |
| [JAVA] Spring Boot 실행은 되는데 API 호출이 안 되는 이유 (0) | 2026.05.12 |
| [JAVA] @Autowired null 문제 실제 원인 (0) | 2026.05.11 |
| [JAVA] ApplicationContext 로딩 실패 문제 해결 경험 (0) | 2026.05.10 |
