Spring · 2025-10-10 · 2분 읽기

Spring @Transactional 동작 원리와 흔한 실수들

목차
  • 1. 기본 동작 원리
  • 2. Self-invocation 문제
  • 3. private 메서드 문제
  • 4. readOnly = true
  • 5. 전파 옵션 (Propagation)
  • 6. 롤백 규칙

1. 기본 동작 원리

Spring은 AOP 프록시로 트랜잭션을 관리한다.

Spring 트랜잭션 흐름

클라이언트 → 프록시 (트랜잭션 시작) → 실제 빈 → 프록시 (커밋/롤백)

그래서 프록시를 거치지 않으면 트랜잭션이 동작하지 않는다.

2. Self-invocation 문제

같은 클래스 내에서 메서드를 호출하면 프록시를 거치지 않는다.

@Service
public class OrderService {
 
    public void placeOrder(OrderRequest req) {
        // this.processPayment()는 프록시를 거치지 않는다
        // → @Transactional이 무시됨
        processPayment(req);
    }
 
    @Transactional
    public void processPayment(OrderRequest req) {
        // ...
    }
}

해결책: 별도 클래스로 분리하거나, ApplicationContext에서 직접 빈을 가져온다.

@Service
@RequiredArgsConstructor
public class OrderService {
    private final PaymentService paymentService;
 
    public void placeOrder(OrderRequest req) {
        paymentService.processPayment(req);  // 프록시 통과
    }
}

3. private 메서드 문제

@Transactional  // 동작 안 함
private void saveLog(String message) {
    // ...
}

프록시는 상속을 통해 만들어지기 때문에 private 메서드는 오버라이드할 수 없다. public 또는 protected로 바꿔야 한다.

4. readOnly = true

@Transactional(readOnly = true)
public List<Post> getPosts() {
    return postRepository.findAll();
}
  • Hibernate flush 모드가 MANUAL로 설정됨 → dirty checking 생략
  • DB 커넥션 최적화 (read replica 라우팅 가능)
  • 조회 전용 메서드에는 반드시 붙이자

5. 전파 옵션 (Propagation)

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendNotification(Long userId) {
    // 부모 트랜잭션과 독립적으로 동작
    // 부모가 롤백되어도 이 메서드는 커밋됨
    notificationRepository.save(...);
}
옵션설명
REQUIRED (기본)기존 트랜잭션 있으면 참여, 없으면 생성
REQUIRES_NEW무조건 새 트랜잭션 생성
NESTED중첩 트랜잭션 (savepoint)
NOT_SUPPORTED트랜잭션 없이 실행

6. 롤백 규칙

기본적으로 RuntimeExceptionError만 롤백된다.

// IOException은 롤백 안 됨
@Transactional
public void upload(MultipartFile file) throws IOException {
    fileRepository.save(...);
    fileStorage.upload(file);  // IOException 발생 → 롤백 안 됨!
}
 
// 명시적으로 지정
@Transactional(rollbackFor = Exception.class)
public void upload(MultipartFile file) throws IOException {
    // ...
}

'Spring 🍃' 카테고리의 다른 글

  • 이 카테고리에 다른 글이 없습니다.