JPA Repository와 @Transactional

Kim jisu·2025년 2월 3일

spring

목록 보기
3/5

JPA Repository와 @Transactional: 제대로 알고 사용하기

들어가며

Spring Data JPA를 사용하면서 "@Transactional이 정말 필요한가?"라는 의문이 생길수 있습니다. "JpaRepository가 알아서 트랜잭션을 관리하지 않나?"라는 생각이 들 수도 있습니다.

JpaRepository의 트랜잭션

먼저 흔히 오해하는 부분부터 짚어보겠습니다. JpaRepository의 기본 메서드들(save, findById 등)은 실제로 내부적으로 트랜잭션 처리가 되어 있습니다. 하지만 이것만으로는 실제 비즈니스 로직을 안전하게 처리하기에 충분하지 않습니다. 다음 예시를 통해 살펴보겠습니다:

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentRepository paymentRepository;

    // 잘못된 예시
    public void processOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).get();
        order.setStatus(OrderStatus.PROCESSING);
        orderRepository.save(order);  // 트랜잭션 1

        Payment payment = new Payment(order);
        paymentRepository.save(payment);  // 트랜잭션 2
        
        // 각각의 save가 별도의 트랜잭션으로 처리됩니다
    }
}

이 코드의 문제점은 각각의 Repository 메서드가 독립적인 트랜잭션으로 실행된다는 것입니다. 만약 payment 저장 중에 문제가 발생하면, 이미 저장된 order의 상태는 롤백되지 않습니다.

@Transactional이 필요한 이유

1. 원자성(Atomicity) 보장

여러 데이터베이스 작업이 하나의 작업 단위로 처리되어야 할 때, @Transactional이 필수적입니다.

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentRepository paymentRepository;

    @Transactional
    public void processOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).get();
        order.setStatus(OrderStatus.PROCESSING);
        
        Payment payment = new Payment(order);
        paymentRepository.save(payment);
        
        // 모든 작업이 하나의 트랜잭션으로 처리됩니다
        // 어느 한 지점에서 실패하면 모든 변경사항이 롤백됩니다
    }
}

2. 변경 감지(Dirty Checking)

JPA의 변경 감지 기능은 트랜잭션 범위 내에서만 동작합니다.

@Service
public class ProductService {
    @Transactional
    public void updateProductPrice(Long productId, BigDecimal newPrice) {
        Product product = productRepository.findById(productId).get();
        product.setPrice(newPrice);
        // save() 호출이 없어도 변경사항이 자동으로 반영됩니다
    }
}

3. 지연 로딩(Lazy Loading)

JPA의 지연 로딩은 트랜잭션 범위 내에서만 정상적으로 동작합니다.

@Service
public class OrderService {
    @Transactional
    public void processOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).get();
        // 트랜잭션 내에서는 지연 로딩이 정상 동작
        order.getOrderItems().forEach(item -> {
            item.process();
        });
    }
}

실전에서의 @Transactional 활용

1. 트랜잭션 전파 속성 활용

@Service
public class CompositeService {
    @Autowired
    private OrderService orderService;
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void processBusinessLogic() {
        // 기존 트랜잭션이 있으면 참여하고, 없으면 새로 시작
        orderService.createOrder();
    }
}

2. 격리 수준 제어

@Service
public class InventoryService {
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void updateStock(Long productId, int quantity) {
        // 동시성 제어가 중요한 로직의 경우
        // 적절한 격리 수준을 설정
    }
}

3. 예외 처리와 롤백

@Service
public class PaymentService {
    @Transactional(rollbackFor = PaymentFailedException.class)
    public void processPayment(Long orderId) {
        try {
            // 결제 처리 로직
        } catch (PaymentFailedException e) {
            // 특정 예외 발생 시 롤백
            throw e;
        }
    }
}

JpaRepository의 트랜잭션 vs 비즈니스 로직의 트랜잭션

앞서 설명한 것처럼 JpaRepository의 기본 메서드들은 내부적으로 트랜잭션 처리가 되어 있습니다. 하지만 실제 비즈니스 로직에서는 다음과 같은 이유로 별도의 트랜잭션 관리가 필요합니다:

  1. 작업의 원자성: 여러 데이터베이스 작업이 하나의 단위로 처리되어야 합니다.
  2. 데이터 일관성: 모든 변경사항이 함께 성공하거나 함께 실패해야 합니다.
  3. JPA 기능 활용: 변경 감지, 지연 로딩 등 JPA의 주요 기능들이 정상적으로 동작해야 합니다.
  4. 성능 최적화: 하나의 트랜잭션으로 묶음으로써 불필요한 데이터베이스 커넥션 생성을 줄일 수 있습니다.

마치며

JPA Repository를 사용할 때 @Transactional의 역할을 제대로 이해하는 것은 매우 중요합니다. 단순히 데이터를 저장하고 조회하는 것을 넘어서, 실제 비즈니스 로직에서는 여러 작업이 하나의 트랜잭션으로 처리되어야 하는 경우가 많습니다.

실무에서는 다음 사항들을 항상 고려해야 합니다:

  • 비즈니스 로직의 범위에 따른 적절한 트랜잭션 경계 설정
  • 트랜잭션 전파 속성의 이해와 활용
  • 동시성 제어를 위한 격리 수준 설정
  • 예외 발생 시의 롤백 전략
profile
Dreamer

0개의 댓글