Spring Data JPA를 사용하면서 "@Transactional이 정말 필요한가?"라는 의문이 생길수 있습니다. "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이 필수적입니다.
@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);
// 모든 작업이 하나의 트랜잭션으로 처리됩니다
// 어느 한 지점에서 실패하면 모든 변경사항이 롤백됩니다
}
}
JPA의 변경 감지 기능은 트랜잭션 범위 내에서만 동작합니다.
@Service
public class ProductService {
@Transactional
public void updateProductPrice(Long productId, BigDecimal newPrice) {
Product product = productRepository.findById(productId).get();
product.setPrice(newPrice);
// save() 호출이 없어도 변경사항이 자동으로 반영됩니다
}
}
JPA의 지연 로딩은 트랜잭션 범위 내에서만 정상적으로 동작합니다.
@Service
public class OrderService {
@Transactional
public void processOrder(Long orderId) {
Order order = orderRepository.findById(orderId).get();
// 트랜잭션 내에서는 지연 로딩이 정상 동작
order.getOrderItems().forEach(item -> {
item.process();
});
}
}
@Service
public class CompositeService {
@Autowired
private OrderService orderService;
@Transactional(propagation = Propagation.REQUIRED)
public void processBusinessLogic() {
// 기존 트랜잭션이 있으면 참여하고, 없으면 새로 시작
orderService.createOrder();
}
}
@Service
public class InventoryService {
@Transactional(isolation = Isolation.SERIALIZABLE)
public void updateStock(Long productId, int quantity) {
// 동시성 제어가 중요한 로직의 경우
// 적절한 격리 수준을 설정
}
}
@Service
public class PaymentService {
@Transactional(rollbackFor = PaymentFailedException.class)
public void processPayment(Long orderId) {
try {
// 결제 처리 로직
} catch (PaymentFailedException e) {
// 특정 예외 발생 시 롤백
throw e;
}
}
}
앞서 설명한 것처럼 JpaRepository의 기본 메서드들은 내부적으로 트랜잭션 처리가 되어 있습니다. 하지만 실제 비즈니스 로직에서는 다음과 같은 이유로 별도의 트랜잭션 관리가 필요합니다:
JPA Repository를 사용할 때 @Transactional의 역할을 제대로 이해하는 것은 매우 중요합니다. 단순히 데이터를 저장하고 조회하는 것을 넘어서, 실제 비즈니스 로직에서는 여러 작업이 하나의 트랜잭션으로 처리되어야 하는 경우가 많습니다.
실무에서는 다음 사항들을 항상 고려해야 합니다: