05 스프링롤백
05 스프링롤백
1. Note
- 익숙하지 않은데, 뭔가 익숙해져야만 하는 롤백
- 같은 비즈니스로직 선상에서 필요한 것들 잘 체크
- 무엇을 롤백하고 무엇을 롤백안할지,
- 로직에서 반드시 커밋해야하는 이력은 노롤백
- 잘못된 건들은 롤백!
2. 트랜잭션,커밋, 롤백
1. 트랜잭션 커밋
- 트랜잭션 내의 모든 작업(예: 상품 재고 차감 등)이 성공적으로 완료되면 커밋을 통해 변경 사항이 영구적으로 저장
- 소스흐름
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// 판매 시, 상품의 재고(stock)를 차감한 후 정상적으로 트랜잭션이 커밋되면 // 상품 정보(Product)의 재고 변경 내역이 데이터베이스에 반영 @Transactional public void sellProduct(Long productId, int quantity) { // 상품 조회: 존재하지 않으면 예외 발생 Product product = productRepository.findById(productId) .orElseThrow(() -> new ServiceException(ServiceExceptionCode.NOT_FOUND_PRODUCT)); // 재고 차감: 엔티티 내 reduceStock 메서드를 활용 product.reduceStock(quantity); // 변경 사항 저장: 트랜잭션 커밋 시점에 DB에 반영됨 productRepository.save(product); }
2. 트랜잭션 롤백
- 트랜잭션 내에서 오류가 발생하면, 해당 작업과 관련된 모든 변경 사항을 롤백하여 이전 상태로 복구
소스 흐름
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// 판매 과정 중, 재고가 부족한 상황에서 예외가 발생하면 // 재고 차감 작업 역시 롤백되어 상품의 재고가 원래 상태로 복원됨 @Transactional public void sellProductWithError(Long productId, int quantity) { // 상품 조회 Product product = productRepository.findById(productId) .orElseThrow(() -> new ServiceException(ServiceExceptionCode.NOT_FOUND_PRODUCT)); // 재고 부족 체크: 재고가 부족하면 예외 발생하여 트랜잭션 롤백 if (product.getStock() < quantity) { throw new ServiceException(ServiceExceptionCode.OUT_OF_STOCK_PRODUCT); } // 재고 차감 product.reduceStock(quantity); // 변경 사항 저장: 예외가 없으면 커밋되어 DB에 반영됨 productRepository.save(product); }
3. 롤백에서 파생된 문제
1. 데이터 정합성 문제 (외부 시스템과의 불일치)
- 트랜잭션 외부에서 수행되는 작업(예: 외부 알림, 결제 연동 등)은 롤백 대상에 포함되지 않을 수 있음,
- DB와 외부 시스템 간 상태 불일치가 발생할 가능성이 존재함
- 소스 흐름
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
@Transactional public void sellProductAndNotify(Long productId, int quantity) { Product product = productRepository.findById(productId) .orElseThrow(() -> new ServiceException(ServiceExceptionCode.NOT_FOUND_PRODUCT)); // 상품 수량 차감 product.reduceStock(quantity); // 트랜잭션 외부에서 실행되는 외부 알림 서비스 호출 (문자 발송 완료)<- 롤백이 안되는 부분 notificationService.sendStockReductionNotification(product); // 이후 DB 저장 작업에서 예외 발생 시 전체 트랜잭션 롤백 productRepository.save(product); }
2. 복잡한 트랜잭션 관리
- 대량 데이터를 처리하거나, 여러 작업이 반복적으로 수행되는 경우
- 일부 작업이 실패할 때 이미 커밋된 부분을 개별적으로 관리해야 하는 복잡성이 존재
소스 흐름
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
@Transactional public void processBulkSales(List<Sale> sales) { //동시에 여러 상품을 처리할때 for (Sale sale : sales) { Product product = productRepository.findById(sale.getProductId()) .orElseThrow(() -> new ProductNotFoundException("상품이 존재하지 않습니다.")); // 재고 차감 product.reduceStock(sale.getQuantity()); // 작업 중 특정 상품이 예외가 발생할 경우 트랜잭션이 전체 롤백됨 <- 1개 상품떄문에 전체 롤백 if (sale.getQuantity() > product.getStock()) { throw new RuntimeException("일부 상품의 재고 부족으로 처리 실패: " + product.getName()); } productRepository.save(product); } }
4. 롤백 커스터마이징
1. rollbackFor
- 기본적으로 롤백되지 않는 체크 예외가 발생했을 때 강제로 트랜잭션을 롤백시키고 싶을 때 사용
- 소스흐름
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// 보통 Checked Exception은 catch로 잡아서 보통 롤백X // rollbackFor을 통해서 롤백을 진행하고, // 예외 자체는 Throw 밖으로 전파됨 @Transactional(rollbackFor = CustomCheckedException.class) public void updateProductPrice(Long productId, BigDecimal newPrice) throws CustomCheckedException { Product product = productRepository.findById(productId) .orElseThrow(() -> new ProductNotFoundException("상품을 찾을 수 없습니다.")); log.info("상품 가격을 {}에서 {}로 변경 시도.", product.getPrice(), newPrice); product.setPrice(newPrice); productRepository.save(product); // 변경 사항을 우선 DB에 반영 // 예외 발생 조건: 음수 가격은 허용하지 않음 (체크 예외) if (newPrice.compareTo(BigDecimal.ZERO) < 0) { throw new CustomCheckedException("가격은 음수가 될 수 없습니다."); } } // CustomCheckedException.java (Checked Exception) public class CustomCheckedException extends Exception { public CustomCheckedException(String message) { super(message); } }
2. noRollbackFor
- 기본적으로 롤백되는 언체크 예외가 발생했음에도 불구하고, 트랜잭션을 커밋하고 싶을 때 사용
- 소스 흐름
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// 보통 uncheked Exception은 자동으로 롤백됨 // 그런데 커밋된 작업이 로그나 메인 비즈니스로직과 관련이 적은 부분이라면 // 롤백을 굳이 필요하지 않을 수도 있음.(굉장한 주의는 필요함) // 이럴경우 롤백을 하지 않고, 예외를 밖으로 전파함. @Transactional(noRollbackFor = IllegalArgumentException.class) public void reduceProductStockNoRollback(Long productId, int quantity) { Product product = productRepository.findById(productId) .orElseThrow(() -> new ProductNotFoundException("상품을 찾을 수 없습니다.")); // 이 예제에서는 예외 발생 전 다른 DB 작업을 수행했다고 가정합니다. // ex) logRepository.save(new Log("재고 차감 시도...")); // 재고 부족 시 IllegalArgumentException 발생 (언체크 예외) if (product.getStock() < quantity) { throw new IllegalArgumentException("재고가 부족합니다."); } product.reduceStock(quantity); productRepository.save(product); }
This post is licensed under CC BY 4.0 by the author.