@Transactional 동작 원리와 롤백

한 줄 정의

Spring은 @Transactional을 통해 AOP 기반으로 트랜잭션을 관리하며, 프록시가 메서드 실행 전후에 트랜잭션 시작, 커밋, 롤백을 자동으로 처리한다.

실무에서 왜 중요한가

Spring 트랜잭션을 제대로 이해하지 못하면 다음 문제가 생긴다.

  • @Transactional을 붙였는데 롤백이 되지 않는다.
  • 같은 클래스 내부 호출에서 트랜잭션이 적용되지 않는다.
  • checked 예외가 발생했는데 롤백되지 않는 이유를 모른다.
  • 트랜잭션 전파 설정을 잘못해서 의도하지 않은 커밋이나 롤백이 발생한다.
  • readOnly 트랜잭션의 의미와 효과를 모른다.

@Transactional 동작 원리

@Transactional은 AOP 프록시 기반으로 동작한다.

외부 호출 → 프록시 → 트랜잭션 시작 → 실제 메서드 실행 → 커밋 or 롤백
@Service
public class OrderService {
 
    @Transactional
    public void createOrder(OrderRequest request) {
        orderRepository.save(new Order(request));
        paymentService.pay(request.getPaymentInfo());
        // 예외 발생 시 전체 롤백
    }
}

프록시가 메서드 시작 전에 트랜잭션을 열고, 정상 완료 시 커밋, 예외 발생 시 롤백한다.

롤백 규칙

// 기본: unchecked 예외(RuntimeException)만 롤백
@Transactional
public void process() {
    throw new RuntimeException(); // 롤백 O
}
 
@Transactional
public void process() throws IOException {
    throw new IOException(); // 롤백 X (checked 예외)
}
 
// checked 예외도 롤백하려면 명시
@Transactional(rollbackFor = Exception.class)
public void process() throws IOException {
    throw new IOException(); // 롤백 O
}
예외 타입기본 롤백변경 방법
RuntimeException (unchecked)OnoRollbackFor로 제외 가능
Exception (checked)XrollbackFor로 롤백 대상 추가
ErrorO-

트랜잭션 전파 (Propagation)

@Transactional(propagation = Propagation.REQUIRED)
public void outerMethod() {
    innerService.innerMethod();
}
전파 옵션동작실무 사용
REQUIRED (기본값)기존 트랜잭션이 있으면 참여, 없으면 새로 생성대부분
REQUIRES_NEW항상 새 트랜잭션 생성, 기존 트랜잭션 일시 중지독립 로깅, 알림
SUPPORTS기존 트랜잭션이 있으면 참여, 없으면 트랜잭션 없이 실행드물게 사용
NOT_SUPPORTED트랜잭션 없이 실행, 기존 트랜잭션 일시 중지드물게 사용

REQUIRES_NEW 활용

@Service
public class OrderService {
 
    @Transactional
    public void createOrder(OrderRequest request) {
        orderRepository.save(new Order(request));
        notificationService.sendNotification(request); // 알림 실패해도 주문은 유지
    }
}
 
@Service
public class NotificationService {
 
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendNotification(OrderRequest request) {
        // 별도 트랜잭션 → 실패해도 주문 트랜잭션에 영향 없음
    }
}

readOnly 트랜잭션

@Transactional(readOnly = true)
public List<OrderResponse> getOrders() {
    return orderRepository.findAll().stream()
        .map(OrderResponse::from)
        .toList();
}
  • JPA에서 변경 감지(dirty checking)를 생략해서 성능이 향상된다.
  • DB에 따라 읽기 전용 쿼리 최적화가 적용될 수 있다.
  • 조회 전용 메서드에는 readOnly = true를 습관적으로 붙인다.

@Transactional이 동작하지 않는 경우

@Transactional은 AOP 프록시 기반이므로 내부 호출, private 메서드, final 클래스 등에서 동작하지 않을 수 있다.

구체적인 실패 케이스 6가지와 진단 플로우는 transactional-pitfalls에서 다룬다.

자주 나는 실수

  • checked 예외에서 롤백이 안 되는 것을 모르고 rollbackFor를 설정하지 않는다.
  • readOnly = true를 조회 메서드에 붙이지 않는다.
  • 트랜잭션 범위를 너무 넓게 잡아서 DB 커넥션 점유 시간이 길어진다.

핵심 요약

Spring의 @Transactional은 AOP 프록시 기반으로 동작합니다. 프록시가 메서드 실행 전에 트랜잭션을 시작하고, 정상 완료 시 커밋, 예외 발생 시 롤백합니다.

기본적으로 unchecked 예외(RuntimeException)만 롤백합니다. checked 예외도 롤백하려면 rollbackFor = Exception.class를 설정해야 합니다.

프록시 제약으로 동작하지 않는 경우는 transactional-pitfalls를 참고합니다. 조회 전용 메서드에는 readOnly = true를 설정해서 변경 감지를 생략하는 것이 좋습니다.

꼬리 질문

점검 퀴즈

아래 문항은 개념을 실제로 설명할 수 있는지 점검하기 위한 것이다. 선택지를 누르면 정답 여부와 이유가 표시된다.

객관식Spring @Transactional의 기본 롤백 규칙으로 맞는 것은?

OXREQUIRES_NEW는 기존 트랜잭션에 참여하지 않고 별도 트랜잭션을 시작한다.

객관식readOnly = true의 실무적 의미로 가장 적절한 것은?

관련 문서