우당탕탕
[Spring] @Transactional 분리 시 발생하는 Self Invocation 오류와 그 원인 및 해결 본문
@Transactional 분리 시 발생하는 Self Invocation 오류와 그 원인 및 해결
Self Invocation과 @Transactional의 관계
Spring에서 트랜잭션을 선언할 때 흔히 사용하는 방식은 바로 @Transactional 어노테이션을 메서드나 클래스에 붙이는 것이다.
이 어노테이션 덕분에 해당 메소드의 실행 시작과 함께 트랜잭션이 열리고, 작업이 끝나면 커밋 또는 롤백이 자동으로 처리된다. 그런데 개발하다 보면 이런 상황이 자주 생긴다.
“한 서비스 내 메소드A에서 메서드 B를 호출하는데, B에만 @Transactional을 붙였더니 트랜잭션이 동작하지 않는 것 같다?”
“@Transactional(propagation=REQUIRES_NEW)로 새로운 트랜잭션을 만들었는데 동작을 안 해요!”
위와 같은 증상들은 대부분 Self Invocation 때문인데, 아래에서 동작 구조를 살펴보자.
Proxy 기반의 트랜잭션 처리 방식
Spring에서 @Transactional은 프록시(Proxy)를 활용해서 트랜잭션을 관리한다.
서비스 빈이 생성될 때, 실제 객체 앞에 프록시 객체가 하나 더 만들어진다. 이 프록시가 public 메서드 호출을 가로채서 트랜잭션을 열고 닫는 작업을 대신해 준다.
• 외부에서 서비스의 @Transactional 메서드를 호출하면 → 프록시가 호출을 받아 트랜잭션 open 및 비즈니스 처리 후 close.
• 내부에서 this. 메서드()로 직접 호출하면 → 프록시를 거치지 않고 실제 객체가 바로 동작. 트랜잭션 처리 X.
예시 코드
@Service
public class MyService {
public void outer() {
// 내부 호출. 프록시를 사용하지 않고 B가 직접 실행됨.
innerTransactional();
}
@Transactional
public void innerTransactional() {
// 트랜잭션이 걸리길 기대했지만 실제로는 동작하지 않음!
...
}
}
위 코드에서 outer()에서 innerTransactional()을 직접 호출할 때 트랜잭션이 제대로 걸리지 않는 이유는, Spring의 프록시가 개입할 수 있는 구조가 아니기 때문이다. 내부 메서드 호출(this로 호출)은 직접 오브젝트가 실행하므로 프록시의 트랜잭션 관리가 적용되지 않는다.
분리하여 해결하는 방법
이 문제를 해결하기 위해서는 트랜잭션이 필요한 메서드를 별도의 빈(서비스 등)으로 분리해야 한다. 즉, 클래스 A에서 클래스 B로 메서드 이동 후 B에 @Transactional 부여.
@Service
public class ServiceA {
private final ServiceB serviceB;
public void outer() {
serviceB.innerTransactional();
}
}
@Service
public class ServiceB {
@Transactional
public void innerTransactional() {
// 트랜잭션이 정상적으로 시작됨
...
}
}
이렇게 하면 outer()가 ServiceB의 메서드를 호출하면서, 프록시를 통해 트랜잭션이 제대로 적용된다.
흔히 경험하는 오류 및 실패 상황
Self Invocation으로 인해 @Transactional이 동작하지 않을 때, 예상했던 롤백이나 커밋이 이루어지지 않아 데이터 정합성 오류, 일관성 문제, 혹은 예외 처리가 누락되는 현상이 발생할 수 있다. 이를 방치하면 운영 중 심각한 장애로 이어질 수 있다.
• DB 트랜잭션이 실제로 열리지 않음 → 롤백되지 않음
• 예외 발생 시 catch로 예외가 흘러가도 트랜잭션이 종료되지 않음
@Transactional(propagation = REQUIRES_NEW) 일 때도 Self Invocation 문제가 동일하게 발생할까?
Spring의 트랜잭션 관리에서 @Transactional(propagation = REQUIRES_NEW)는 기존 트랜잭션과 완전히 별개의 새 트랜잭션을 생성한다.
그래서 “트랜잭션이 안 열릴까 걱정은 없는 건가? “라고 생각하는 경우가 많지만, Proxy 기반 트랜잭션 동작 방식 때문에 Self Invocation 문제는 동일하게 발생한다.
왜 여전히 문제가 생길까?
"프록시를 안 타면 어떤 propagation을 사용해도 트랜잭션은 생성되지 않는다! 반드시 빈 분리 후 @Transactional을 붙이자."
@Transactional(propagation = REQUIRES_NEW)가 붙은 메서드라도, 같은 클래스에서 자기 자신을 this로 호출하면 프록시를 타지 않는다. 즉,
• 내부 메서드 호출이면 → 프록시를 경유하지 않고, Spring 컨테이너가 트랜잭션을 생성/관리하지 못함.
• 외부 빈을 통해 호출해야만 → 프록시가 감싸줘서 진짜로 새로운 트랜잭션이 만들어짐.
결론
Spring에서 @Transactional은 반드시 외부 프록시를 통한 메서드 호출이 필요함을 기억해야 한다. 내부 메소드 호출이라면, 구조를 분리해서 별도의 빈/서비스에서 트랜잭션을 선언함으로써 의도한 대로 트랜잭션 처리가 이루어지도록 설계하자.
• 실무에서는 서비스/리포지토리 단계를 명확히 나누고, 비즈니스 로직과 트랜잭션 경계를 굳이 분리할 때 빈을 나눠두는 습관이 중요하다.
이 구조를 정확히 이해한다면 self invocation 관련 트랜잭션 오류로 고민하는 시간을 크게 줄일 수 있다.
'Tech > Spring' 카테고리의 다른 글
[Spring] 빈 라이프사이클과 초기화 실수 feat. @PostConstruct, @Lazy, 순환참조 (3) | 2025.08.17 |
---|---|
[동시성제어 4-1편] Redis + Lua Script를 활용한 분산락 (3) | 2025.08.08 |
[동시성 제어 3편] 낙관적 락(Optimistic Lock) - @Version 어노테이션을 활용한 락 (4) | 2025.08.06 |
[동시성 제어 2편] 비관적 락(Pessimistic Lock) - JPA 스프링으로 경험해보는 실전 가이드 (1) | 2025.08.05 |
[동시성 제어 1편] 동시성 제어란? - 데이터가 꼬이지 않는 백엔드의 첫걸음 (4) | 2025.07.31 |