본문 바로가기
개발/ERROR 모음

Spring @Transactional 적용 안 되는 이유 총정리 (AOP 프록시, private, self 호출)

by chansungs 2026. 3. 16.
728x90
반응형

Spring에서 @Transactional이 적용되지 않거나 롤백이 안 되는 대표 원인을 정리했습니다.

AOP 프록시 방식, private/final 메소드, 같은 클래스 내부 호출(self-invocation), 예외 타입에 따른 롤백 규칙, 트랜잭션 매니저 설정, readOnly 및 propagation 옵션 등 실무 체크리스트로 원인을 빠르게 찾을 수 있습니다.

 

스프링 트랜잭션(@Transactional) 적용 안되는 이유 (AOP 프록시, private 메소드 등)

스프링에서 @Transactional을 붙였는데도 이런 상황이 종종 발생합니다.

  • DB가 커밋돼버림 (롤백 안 됨)
  • 로그를 보면 트랜잭션이 시작되지 않은 것 같음
  • 분명 @Transactional 붙였는데 효과가 없음

대부분은 “설정이 이상한가?”보다 프록시(AOP) 동작 원리를 모르고 코드가 프록시를 우회했기 때문입니다.


1) @Transactional 동작 방식(프록시) 한 줄 요약

스프링의 트랜잭션은 보통 이렇게 동작합니다.

스프링이 빈(Bean)을 만들 때 “프록시 객체”로 감싸고,
프록시를 통해 호출될 때만 트랜잭션(AOP)이 적용된다.

즉, 프록시를 거치지 않고 호출되면 @Transactional은 “붙어 있어도” 적용되지 않습니다.


2) 적용 안 되는 이유 TOP 8 (실무)


3) 원인 1) 같은 클래스 내부 호출(Self-invocation)

가장 흔한 1순위입니다.

@Service
public class OrderService {

    public void outer() {
        inner(); // 같은 클래스 내부 호출
    }

    @Transactional
    public void inner() {
        // DB 작업
    }
}

 

outer()에서 inner()를 호출하면, 이 호출은 프록시를 거치지 않습니다.
즉, 실제로는 “this.inner()” 호출이라 AOP가 개입할 타이밍이 없어요.

✅ 해결 방법

  • 트랜잭션 메소드를 다른 빈(Service)으로 분리해서 호출
  • 또는 (비추천) AopContext.currentProxy() 사용

가장 추천되는 구조:

@Service
public class OrderService {
    private final OrderTxService orderTxService;
    public OrderService(OrderTxService orderTxService) { this.orderTxService = orderTxService; }

    public void outer() {
        orderTxService.inner(); // 프록시를 통해 호출됨
    }
}

@Service
public class OrderTxService {
    @Transactional
    public void inner() {
        // DB 작업
    }
}

 


4) 원인 2) private / final 메소드에 붙임

스프링 AOP는 기본적으로 프록시 기반이라서,
private 메소드는 외부에서 프록시로 호출할 수 없습니다.

 
@Transactional
private void save() { ... } // 적용 안 됨
 

또한 CGLIB 프록시를 쓰더라도 final 메소드는 오버라이드가 불가해서 적용이 제한될 수 있습니다.

✅ 해결 방법

  • 트랜잭션 메소드는 public(권장) 또는 최소한 프록시가 가로챌 수 있는 형태로
  • “외부에서 호출되는 진입점”에 트랜잭션을 붙이기

5) 원인 3) @Transactional이 붙은 클래스/메소드가 빈이 아님

아래처럼 new로 직접 생성하면 트랜잭션이 적용되지 않습니다.

OrderService s = new OrderService(); // 스프링 빈 아님 → 프록시 없음

 

또는 컴포넌트 스캔 대상이 아니면 빈 등록이 안 되어 프록시도 없습니다.

✅ 해결 방법

  • @Service, @Component로 빈 등록
  • @ComponentScan 범위 확인

6) 원인 4) 예외가 발생했는데 롤백이 안 됨 (Checked Exception)

스프링 트랜잭션 기본 롤백 규칙:

  • ✅ RuntimeException / Error → 기본 롤백
  • ❌ Checked Exception(예: Exception, IOException 등) → 기본 롤백 안 함

그래서 아래는 롤백이 안 될 수 있습니다.

 
@Transactional
public void doWork() throws Exception {
    throw new Exception("checked"); // 기본 정책상 롤백 안 함
}
 

✅ 해결 방법: rollbackFor 지정

 
@Transactional(rollbackFor = Exception.class)
public void doWork() throws Exception { ... }

7) 원인 5) try-catch로 예외를 먹어버림

트랜잭션은 “예외가 밖으로 던져져야” 롤백 판단을 합니다.

@Transactional
public void save() {
    try {
        // DB 작업
        throw new RuntimeException("fail");
    } catch (Exception e) {
        // 로그만 찍고 끝내면 → 트랜잭션은 정상 종료(커밋)될 수 있음
    }
}
 
 

✅ 해결 방법

  • 예외를 다시 던지기(rethrow)
  • 또는 롤백 마킹
catch (Exception e) {
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    throw e;
}

 

 

8) 원인 6) 트랜잭션 전파(Propagation) 설정 실수

예를 들어 REQUIRES_NEW는 새 트랜잭션을 만들고,
NOT_SUPPORTED는 트랜잭션을 아예 중지시킵니다.

자주 헷갈리는 케이스:

  • 바깥 트랜잭션에서 호출했는데 안쪽이 REQUIRES_NEW라 따로 커밋됨
  • 안쪽이 NOT_SUPPORTED라 트랜잭션 없이 실행됨

✅ 해결 방법

  • 기본값 REQUIRED를 유지하고 정말 필요한 경우만 변경
  • “롤백 범위”를 의도대로 설계했는지 확인

9) 원인 7) readOnly, flush, autocommit 착시

@Transactional(readOnly = true)에서는
JPA/Hibernate 기준으로 flush가 제한되거나 최적화가 들어가면서 “쓰기”가 기대대로 안 될 수 있습니다.

또, “롤백이 안 된 것처럼 보이는” 착시가 발생하는 경우:

  • 예외가 발생했는데 이미 다른 곳에서 commit된 트랜잭션
  • autocommit 설정/직접 커넥션 사용(템플릿 우회)

✅ 해결 방법

  • readOnly 옵션 확인
  • JDBC 직접 사용 시 DataSource/TxManager 라인 확인

10) 원인 8) 멀티 DB/트랜잭션 매니저 지정 문제

데이터소스가 2개 이상이면

  • 어떤 TxManager를 쓰는지
  • 특정 Mapper/JPA가 어떤 DataSource에 붙는지
    헷갈리면서 “트랜잭션이 적용 안 되는 것처럼” 보일 수 있습니다.

✅ 해결 방법

  • @Transactional(transactionManager = "xxxTransactionManager") 명시
  • MapperScan / SqlSessionFactory / EntityManager 연결 확인

✅ 결론: 가장 빠른 점검 순서

@Transaction이 안 먹는다고 느껴지면 아래 순서로 보면 거의 잡힙니다.

  1. 같은 클래스 내부 호출(Self-invocation)인지
  2. 트랜잭션 메소드가 public이고 “외부에서 프록시로 호출되는 진입점”인지
  3. 클래스가 스프링 빈인지(스캔/주입)
  4. 예외가 Runtime인지 Checked인지, rollbackFor 필요 여부
  5. try-catch로 예외를 삼키고 있지 않은지
  6. propagation/readOnly 설정
  7. 멀티 DB면 transactionManager 지정
728x90
반응형