반응형
Spring @Transactional이 내부 메소드 호출에서 동작하지 않는 이유
Spring Boot에서 @Transactional을 사용할 때 한 가지 흔히 겪는 함정이 있습니다.
바로, 같은 클래스 내에서 메소드를 호출하면 트랜잭션이 적용되지 않는다는 점입니다.
이번 포스팅에서는 이 현상이 왜 발생하는지, 그리고 어떻게 해결할 수 있는지 실제 예제를 통해 자세히 알아보겠습니다.
🧪 상황 예제: 트랜잭션이 안 먹는다?
@Service
public class AService {
@Transactional
public void methodA() {
methodB(); // 내부 호출
}
public void methodB() {
// 예외 발생
throw new RuntimeException("예외 발생!");
}
}
이 코드를 보면, methodA()는 @Transactional이 붙어 있어 트랜잭션이 시작되고,
그 안에서 호출되는 methodB()에서 예외가 발생하니 당연히 롤백될 것 같죠?
하지만 실제 실행해보면...
👉 롤백되지 않고 커밋됩니다!
🤔 왜 이런 일이 벌어질까?
이유는 Spring이 @Transactional을 프록시 기반 AOP로 구현하고 있기 때문입니다.
Spring의 트랜잭션 처리 흐름은 이렇게 됩니다:
- Spring이 @Transactional이 붙은 클래스를 감싸는 프록시 객체를 만듭니다.
- 외부에서 해당 메소드를 호출하면 프록시가 감지하여 트랜잭션을 시작합니다.
- 하지만, 같은 클래스 안에서 메소드를 호출하면 프록시를 거치지 않고 this.method() 형태로 직접 호출하게 됩니다.
- 따라서 트랜잭션을 적용할 기회를 놓쳐버립니다!
📌 즉, 프록시가 개입해야 트랜잭션이 적용되는데, 내부 호출은 프록시를 생략하므로 적용되지 않음!
💡 쉽게 말해서?
마치 건물 입구에 감시카메라(프록시)가 달려 있다고 생각해보세요.
- 외부에서 들어오면 감시카메라가 감지해서 트랜잭션을 시작함
- 하지만 내부에서 옆방으로 이동하면 감시카메라가 그걸 감지 못함
→ 트랜잭션 안 걸림
🛠 해결 방법
✅ 방법 1. 메소드를 다른 클래스로 분리
@Service
public class BService {
@Transactional
public void methodB() {
throw new RuntimeException("예외 발생!");
}
}
@Service
public class AService {
private final BService bService;
public AService(BService bService) {
this.bService = bService;
}
public void methodA() {
bService.methodB(); // 외부 호출 → 프록시 적용됨!
}
}
✅ 방법 2. ApplicationContext를 통해 프록시 자신을 가져와 호출
@Service
public class AService {
private final ApplicationContext context;
public AService(ApplicationContext context) {
this.context = context;
}
public void methodA() {
AService proxy = context.getBean(AService.class);
proxy.methodB(); // 프록시를 통한 호출
}
@Transactional
public void methodB() {
throw new RuntimeException("예외 발생!");
}
}
✅ 결론
@Transactional은 프록시를 거쳐야만 동작한다!
내부에서 자기 메소드를 호출하면 프록시를 거치지 않으므로 트랜잭션이 동작하지 않는다.
같은 클래스 내 메소드 간 호출에는 트랜잭션이 적용되지 않는다는 점,
항상 기억하고 설계하시길 바랍니다.
🔁 정리
구분트랜잭션 적용됨?설명
외부에서 AService.methodA() 호출 | ✅ 적용됨 | 프록시가 감지함 |
AService.methodA() → 내부에서 methodB() 호출 | ❌ 적용 안 됨 | 프록시를 거치지 않음 |
AService → BService.methodB() 호출 | ✅ 적용됨 | 외부 클래스 프록시 통과 |
AService → context.getBean(AService.class).methodB() 호출 | ✅ 적용됨 | 프록시를 수동으로 거침 |