우당탕탕

[Spring] @Transactional 분리 시 발생하는 Self Invocation 오류와 그 원인 및 해결 본문

Tech/Spring

[Spring] @Transactional 분리 시 발생하는 Self Invocation 오류와 그 원인 및 해결

모찌모찝 2025. 8. 16. 08:26
@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 관련 트랜잭션 오류로 고민하는 시간을 크게 줄일 수 있다.

Comments