[JAVA] @Transactional 안 먹히는 상황 정리

Spring에서 @Transactional이 붙어 있는데도 트랜잭션이 적용되지 않는 상황은 생각보다 자주 마주합니다. 단순 설정 문제가 아니라 동작 방식에 대한 이해가 부족해서 생기는 경우가 많습니다.

@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 기반 기능입니다.

프록시를 거쳐야 한다는 전제를 이해하면 대부분의 문제는 구조적으로 설명이 됩니다.

실무에서는 "어디서 호출되느냐"를 먼저 보는 것이 가장 빠른 접근입니다.