Post

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.