트랜잭션 AOP 주의 사항 및 내부 호출 문제

유동우·2025년 3월 7일
0
post-thumbnail

스프링의 트랜잭션 관리는 일반적으로 AOP 프록시를 사용하여 매우 편리하게 제공됩니다.
특히 @Transactional 어노테이션을 메서드나 클래스에 붙이면 자동으로 트랜잭션 관리가 이루어지죠.

하지만 여기에는 내부 호출(self-invocation) 이라는 한 가지 주의해야 할 점이 있습니다.

문제 상황

트랜잭션이 없는 Example 클래스에, 트랜잭션이 적용된 메서드 saveData()와 그렇지 않은 메서드 callExternalApi()가 있다고 가정하겠습니다.

비즈니스 로직 상, 트랜잭션이 없는 메서드가 먼저 호출된 후 트랜잭션 메서드를 호출하는 흐름이라고 해봅시다.

public class Example {

    public void processApiAndSave() {
        callExternalApi();  // 외부 API 호출 (트랜잭션 없음)
        saveData();         // 내부 호출이므로 트랜잭션이 적용되지 않음
    }

    public void callExternalApi() {
        System.out.println("외부 API 호출");
    }

    @Transactional
    public void saveData() {
        printTxStatus();
        // DB 저장 로직
    }

    private void printTrx() {
        boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
        System.out.println("트랜잭션 활성화: " + txActive);
    }
}

위 코드에서 processApiAndSave() 메서드는 트랜잭션이 없는 상태에서 실행됩니다.

내부에서 호출한 saveData()는 비록 @Transactional이 붙어있지만,
내부 호출(self-invocation)이기 때문에 프록시가 적용되지 않아 트랜잭션이 활성화되지 않습니다.

즉, 실행 결과는 다음과 같습니다.

외부 API 호출
트랜잭션 활성화: false

왜 이런 일이 발생할까?

스프링의 트랜잭션 관리 방식은 프록시(Proxy) 를 통해 동작합니다.
프록시는 대상 객체의 메서드를 호출하기 전에 트랜잭션을 시작하고, 메서드 종료 후 트랜잭션을 종료합니다.

하지만 클래스 내부에서 자신의 메서드를 직접 호출할 경우(this 호출), 프록시 객체를 통하지 않고 직접 호출되기 때문에 프록시를 우회하게 됩니다.

따라서 메서드에 @Transactional이 붙어있다고 해도 내부 호출(self-invocation) 시에는 트랜잭션이 적용되지 않습니다.

해결 방법

가장 간단하고 효과적인 해결책은 클래스를 분리하는 것입니다.
트랜잭션이 필요한 로직을 별도의 클래스(빈)로 분리하면, 프록시 객체가 정상적으로 트랜잭션을 처리할 수 있습니다.

public class ApiService {

    private final DatabaseService databaseService;

    public ApiService(DatabaseService databaseService) {
        this.databaseService = databaseService;
    }

    public void processApiAndSave() {
        callExternalApi();              // 트랜잭션 없음
        databaseService.saveData();     // 프록시를 통해 트랜잭션이 적용됨
    }

    private void callExternalApi() {
        System.out.println("외부 API 호출");
    }
}

public class DatabaseService {

    @Transactional
    public void saveData() {
        printTransactionStatus();
    }

    private void printTrx() {
        boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
        System.out.println("트랜잭션 활성화: " + txActive);
    }
}

이렇게 별도의 클래스로 로직을 분리하면, 스프링 프록시가 정상적으로 동작하여 다음과 같은 결과를 얻을 수 있습니다.

외부 API 호출
트랜잭션 활성화: true

정리

스프링의 트랜잭션 AOP를 사용할 때는 프록시의 한계점을 반드시 고려해야 합니다.

내부 호출은 프록시를 거치지 않아 트랜잭션이 적용되지 않으므로,
중요한 트랜잭션 로직은 별도의 클래스로 분리하여 사용하는 것이 가장 좋은 방법이라고 생각합니다

추후에 실무에 투입되어 해당 문제를 직면하는 경우, 포스팅의 내용이 떠오르길 바라며... 🙃

profile
효율적이고 꾸준하게

0개의 댓글

관련 채용 정보