본문 바로가기

spring

@Transactional 이해하기 - 내부 동작 및 주의사항 포함

@Transactional이란?
: spring에서 지원하는 트랜잭션을 선언적으로 처리하기 위한 어노테이션
주로 DB 작업을 수행하는 Service 계층에서 사용되며, @Transactional이 적용된 메서드를 실행하면 
트랜잭션이 시작되고 메서드 정상적 종료 시 commit / 예외 발생 시 rollback
→ 이러한 흐름을 자동으로 처리해줌 !

 

 

내부 동작 원리

 

@Transactional은 프록시 기반 AOP 기술로 동작

즉, 실제 서비스 객체를 감싸는 프록시 객체가 생성되고 이 프록시가 메서드 호출을 가로채 트랜잭션을 관리

→ 프록시가 메서드 실행 전후에 트랜잭션 시작 / 커밋 / 롤백 수행

프록시(Proxy)란?
: 객체를 감싸서 실제 객체 호출 전/후에 부가적인 처리를 하는 중간 객체

Spring에서는 대표적으로 두 가지 방식으로 프록시를 만든다 !
- JDK 동적 프록시 : 인터페이스를 구현한 경우, 인터페이스 기반
- CGLIB 프록시 : 인터페이스가 없는 경우, 클래스 기반
** @EnableTransactionalManagement(proxyTargetClass = true) 설정하면 → CGLIB 강제 사용
AOP(Aspect-Oriented Programming)이란?
: 핵심 로직 외에 반복되는 관심사(예: 트랜잭션, 로깅, 보안 등)를 별도의 관심사(Aspect)로 분리하여 처리하는 프로그래밍 방식

AOP는 스프링에서 프록시를 통해 구현된다 !
→ @Transactional은 트랜잭션이라는 공통 관심사를 AOP로 처리하는 대표적인 예

 

 

사용 위치

 

클래스 또는 메서드 레벨에서 사용 가능

@Transactional
public class SomeService {
    public void doSomething() {}
}
  • 해당 클래스의 모든 public 메서드에 개별적으로 트랜잭션 적용
public class SomeService {
    @Transactional
    public void doSomething() {}
}
  • 클래스 레벨에 선언 후 메서드 레벨에 다시 @Transactional을 선언하면?
    → 클래스 레벨보다 메서드 레벨이 우선됨
    예) 클래스에는 readOnly = true, 메서드에는 readOnly = false인 경우 : 메서드의 설정이 적용됨

 

 

⚠️ 주의할 점

 

1. 내부 메서드 호출 시 트랜잭션 적용이 안 되는 이유

@Service
public class MyService {
    @Transactional
    public void outer() {
        inner(); // ← 트랜잭션 적용 안 됨!
    }

    @Transactional
    public void inner() {
        // 트랜잭션이 적용되어야 할 로직
    }
}

 

  • @Transactional은 프록시 객체를 통해 호출될 때만 작동한다 !
  • 즉, 트랜잭션 적용은 다음과 같이 동작해야 함
    어디선가 호출 → 프록시(MyServiceProxy) → 실제 MyService → inner() → 트랜잭션 처리
  • 어노테이션을 서비스 클래스에 붙여도, 메서드에 붙여도 결국 서비스 프록시 객체가 메서드 호출을 가로채서 트랜잭션을 시작/종료
  • 하지만 위 예시에서 inner()는 outer() 내부에서 자기 자신(this)으로 직접 호출되고 있음
  • 프록시가 proxy.outer() 같은 식으로 직접 호출해야 트랜잭션이 적용되지만, outer() 안에서 this.inner()를 호출하도록 설계 시 프록시를 통하지 않고 호출하는 것이므로 inner()는 AOP가 적용되지 않아 트랜잭션이 적용되지 않는다 !!

 

해결 방법)

가장 추천되는 방식 1가지 - 분리된 서비스로 빼서 처리하기

@Service
public class OuterService {
    private final InnerService innerService;
    
    @Transactional
    public void outer() {
        innerService.inner(); // 프록시 거치므로 트랜잭션 정상 적용됨
    }
}

@Service
public class InnerService {

    @Transactional
    public void inner() {
        // 트랜잭션 적용됨
    }
}
  • outer() 내부에서 innerService.inner()를 호출 → innerService를 통해 호출되므로 프록시 객체를 통해 실행되어 트랜잭션 적용
  • 하지만 ! Spring의 propagation 속성 기본값은 REQUIRED이므로, inner()는 별도의 트랜잭션을 새로 시작하지 않고, outer()이 시작한 트랜잭션 안에서 같이 실행됨
  • 둘 중 하나라도 예외가 나면 전체 트랜잭션이 롤백되는 구조

만약 outer() 내부에서 호출하는 inner()를 별도의 트랜잭션으로 처리하고 싶다면?

inner() 메서드 트랜잭션의 propagation 속성을 REQUIRES_NEW로 설정하기

 

Propagation.REQUIRES_NEW
: 트랜잭션의 전파 속성 중 하나, 기존 트랜잭션과 별도로 트랜잭션을 시작하고 싶을 때 사용됨
즉, 기존 트랜잭션이 rollback되더라도 이 작업은 커밋하고 싶을 때 !
→ 기존 트랜잭션은 일시 중단된 후, 해당 트랜잭션이 완료되면 다시 진행됨

예) 예외가 발생하여 rollback되더라도 로그는 별도로 남기고 싶을 때 saveLog 메소드에 적용
  • 참고) REQUIRES_NEW 속성은 기존 예시와 같은 "같은 서비스 내부 호출 문제"를 해결하지 못함
  • 새로운 트랜잭션을 시작하지만, 프록시를 통한 호출이 아닐 경우에는 적용조차 되지 않기 때문 !

 

2. private 메서드에는 적용되지 않음

  • Spring AOP는 프록시 기반이기 때문에 private 메서드를 가로챌 수 없기 때문

 

3. 프록시 방식이기 때문에 Bean으로 등록된 곳에서 호출되어야 적용됨

 

 

⚠️ 롤백 관련 주의 사항

 

Spring의 기본 롤백 조건은 RuntimeException과 Error

  • RuntimeException 또는 Error 발생 시에는 자동 롤백이 되지만,
  • Checked Exception은 롤백되지 않음 → 명시적으로 설정 필요

모든 Exception에 대해 강제로 롤백하고 싶다면 아래와 같이 지정해야 함

@Transactional(rollbackFor = Exception.class)

 

 

더티 체킹 (Dirty Checking)

 

메서드가 트랜잭션으로 관리되는 경우, save() 등의 메서드 호출 없이도 변경된 필드를 감지하고 자동으로 DB에 반영되는 기능

→ JPA가 트랜잭션 커밋 시점에 엔티티의 변경 여부를 감지하여 UPDATE 쿼리 생성

→ 더티 체킹을 사용하려면 해당 메서드는 반드시 트랜잭션이어야 함 !

 

[spring] - 더티 체킹(Dirty Checking) - 개념 및 주의사항 / @DynamicUpdate