Rollback & Commit 관리

리본24·2025년 2월 6일

Spring

목록 보기
7/7
post-thumbnail

1. 롤백과 커밋의 개념

트랜잭션은 데이터베이스 작업을 하나의 논리적 단위로 처리하며, 작업의 성공 여부에 따라 데이터 변경 사항을 저장하거나 이전 상태로 복구합니다. 커밋(commit)은 작업이 성공적으로 완료된 경우 변경 사항을 영구적으로 저장하며, 롤백(rollback)은 오류 발생 시 모든 작업을 취소하고 작업 전 상태로 되돌립니다. 이를 통해 데이터의 정합성과 무결성을 유지할 수 있습니다.

1.1 트랜잭션 커밋의 원리

  1. 정상 종료:
    • 트랜잭션 내의 모든 작업(예: 상품 재고 차감 등)이 성공적으로 완료되면 커밋을 통해 변경 사항이 영구적으로 저장됩니다.
    • 예시: 판매 시, 상품의 재고(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 트랜잭션 롤백의 원리

  1. 오류 처리:
    • 트랜잭션 내에서 오류가 발생하면, 해당 작업과 관련된 모든 변경 사항을 롤백하여 이전 상태로 복구합니다.
    • 예시: 판매 과정 중, 재고가 부족한 상황에서 예외가 발생하면 재고 차감 작업 역시 롤백되어 상품의 재고가 원래 상태로 복원됩니다.
        @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);
        }
  2. 데이터 정합성 유지:
    • 작업 도중 발생한 오류로 인해 일부 변경 사항만 저장되는 것을 방지하여, 데이터의 일관성을 유지합니다.
    • 예시: 재고 차감 작업 도중 예외가 발생하면, 이미 변경된 재고 정보는 롤백되어 상품 데이터의 정합성이 보장됩니다.

1.3 롤백 시 발생할 수 있는 문제점

  1. 데이터 정합성 문제 (외부 시스템과의 불일치):
    • 트랜잭션 외부에서 수행되는 작업(예: 외부 알림, 결제 연동 등)은 롤백 대상에 포함되지 않을 수 있어, DB와 외부 시스템 간 상태 불일치가 발생할 수 있습니다.
    • 예시: 상품 판매 후 재고 차감과 동시에 외부 알림 서비스를 호출했을 때, 알림은 성공적으로 전송되었으나 DB 작업 중 예외가 발생하여 롤백되면, 외부 시스템에서는 재고 차감 알림을 받았지만 DB에는 반영되지 않은 상태가 발생할 수 있습니다.
        @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. 복잡한 트랜잭션 관리:
    • 대량 데이터를 처리하거나, 여러 작업이 반복적으로 수행되는 경우 일부 작업이 실패할 때 이미 커밋된 부분을 개별적으로 관리해야 하는 복잡성이 존재합니다.
    • 예시: 여러 상품에 대해 일괄 판매를 처리할 때, 일부 상품의 재고 부족으로 예외가 발생하면, 이미 처리된 다른 상품들의 재고 변경 여부를 개별적으로 검토하고 관리해야 할 수 있습니다.
      @Transactional
      public void processBulkSales(List<Sale> sales) {
          for (Sale sale : sales) {
              Product product = productRepository.findById(sale.getProductId())
                  .orElseThrow(() -> new ProductNotFoundException("상품이 존재하지 않습니다."));
      
              // 재고 차감
              product.reduceStock(sale.getQuantity());
      
              // 특정 조건에서 예외 발생 시, 전체 트랜잭션이 롤백되어 모든 상품의 재고 변경이 취소됨
              if (sale.getQuantity() > product.getStock()) {
                  throw new RuntimeException("일부 상품의 재고 부족으로 처리 실패: " + product.getName());
              }
      
              productRepository.save(product);
          }
      }

트랜잭션의 커밋과 롤백은 데이터 정합성과 무결성을 유지하기 위한 핵심 메커니즘입니다.

  • 커밋은 모든 작업이 정상적으로 완료된 후 변경 사항을 영구적으로 저장하여, 시스템 장애 상황에서도 안전하게 데이터를 보존합니다.
  • 롤백은 오류 발생 시 모든 변경 사항을 취소하여, 데이터의 불완전하거나 잘못된 저장을 방지합니다.

특히, 외부 시스템과의 연동이나 대량 데이터 처리 시에는 추가적인 관리 전략(예: 보상 트랜잭션, 개별 처리 로직 등)이 필요함을 주의해야 합니다.

제공해주신 Product 엔티티reduceStock() 메서드와 함께 위 예시들을 참고하면, 트랜잭션 관리의 중요성과 발생 가능한 문제점들을 보다 쉽게 이해할 수 있습니다.


2. 트랜잭션 롤백 전략

트랜잭션 롤백은 예외 상황에서 데이터의 정합성을 유지하기 위해 모든 변경 사항을 원래 상태로 되돌리는 중요한 메커니즘입니다. Spring은 트랜잭션 내에서 발생하는 예외 유형에 따라 롤백 동작을 자동으로 처리하며, 개발자가 필요에 따라 세부 동작을 제어할 수 있도록 설정을 제공합니다.

2.1 체크 예외(Checked Exception)와 언체크 예외(Unchecked Exception)

1. 체크 예외 (Checked Exception)

  • 특징:
    • 컴파일 시점에서 반드시 예외 처리가 요구됩니다.
    • Spring에서는 기본적으로 체크 예외 발생 시 트랜잭션을 롤백하지 않습니다.
    • 예외를 처리하지 않으면 컴파일 오류가 발생합니다.
  • 예시:
    • SQLException, IOException
  • Spring의 기본 동작:
    • 체크 예외가 발생해도 트랜잭션은 롤백되지 않으므로, 명시적으로 rollbackFor 속성을 설정해야 합니다.
  • 예제 (Product 엔티티 활용):
    @Slf4j
    @Service
    @RequiredArgsConstructor
    public class ProductService {
    
        private final ProductRepository productRepository;
    
        /**
         * 체크 예외(CustomCheckedException)가 발생하면, rollbackFor에 의해 트랜잭션이 롤백됩니다.
         */
        @Transactional(rollbackFor = CustomCheckedException.class)
        public void updateProductPrice(Long productId, BigDecimal newPrice) throws CustomCheckedException {
            Product product = productRepository.findById(productId)
                .orElseThrow(() -> new ProductNotFoundException("상품을 찾을 수 없습니다."));
    
            product.setPrice(newPrice);
    
            // 예외 발생 조건: 음수 가격은 허용하지 않음 (체크 예외)
            if(newPrice.compareTo(BigDecimal.ZERO) < 0) {
                throw new CustomCheckedException("가격은 음수가 될 수 없습니다.");
            }
    
            productRepository.save(product);
        }
    }
    // CustomCheckedException.java
    public class CustomCheckedException extends Exception {
        public CustomCheckedException(String message) {
            super(message);
        }
    }

2. 언체크 예외 (Unchecked Exception)

  • 특징:
    • 런타임 시 발생하며, 강제 처리가 필요하지 않습니다.
    • Spring에서는 기본적으로 언체크 예외 발생 시 트랜잭션을 롤백합니다.
  • 예시:
    • NullPointerException, IllegalArgumentException
  • Spring의 기본 동작:
    • 별도의 설정 없이도 언체크 예외가 발생하면 트랜잭션이 롤백됩니다.
  • 예제 (Product 엔티티 활용):
    @Slf4j
    @Service
    @RequiredArgsConstructor
    public class ProductService {
    
        private final ProductRepository productRepository;
    
        /**
         * 언체크 예외(IllegalArgumentException)가 발생해도 기본적으로 트랜잭션은 롤백되지만,
         * noRollbackFor 속성을 통해 롤백하지 않도록 설정할 수 있습니다.
         */
      @Transactional(noRollbackFor = IllegalArgumentException.class)
      public void reduceProductStockNoRollback(Long productId, int quantity) {
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new ServiceException(ServiceExceptionCode.NOT_FOUND_PRODUCT));
    
        // 재고 부족 시 IllegalArgumentException 발생 (언체크 예외)
        if (product.getStock() < quantity) {
          throw new IllegalArgumentException("재고가 부족합니다.");
        }
    
        product.reduceStock(quantity);
        productRepository.save(product);
      }
    }
    

2.2 특정 예외 발생 시 롤백 설정

Spring의 @TransactionalrollbackFornoRollbackFor 속성을 통해 트랜잭션 롤백 동작을 제어할 수 있습니다.

1. 롤백 설정 (rollbackFor)

  • 설명:
    • 기본 동작에서 롤백되지 않는 체크 예외도 지정된 예외에 대해 트랜잭션을 롤백할 수 있도록 설정합니다.
  • 예제 (앞서 소개한 updateProductPrice 메서드 참조):
    @Transactional(rollbackFor = CustomCheckedException.class)
    public void updateProductPrice(Long productId, BigDecimal newPrice) throws CustomCheckedException {
        // Product 엔티티를 통해 가격 업데이트 수행
        // 새 가격이 음수면 CustomCheckedException 발생 -> 트랜잭션 롤백
    }
    

2. 롤백 제외 설정 (noRollbackFor)

  • 설명:
    • 기본 동작에서 롤백되는 언체크 예외에 대해 트랜잭션 롤백을 제외할 수 있도록 설정합니다.
  • 예제 (reduceProductStockNoRollback 메서드 참조):
    @Transactional(noRollbackFor = IllegalArgumentException.class)
    public void reduceProductStockNoRollback(Long productId, int quantity) {
        // 재고가 부족할 경우 IllegalArgumentException 발생하더라도 트랜잭션은 롤백되지 않음
    }

3. 테스트 코드 예제

@SpringBootTest
public class ProductTransactionTest {

    @Autowired
    private ProductService productService;

    @Autowired
    private ProductRepository productRepository;

    @Test
    void testRollbackForCheckedException() {
        // 기존 가격으로 복원하기 위한 Product 초기 상태 저장
        Long productId = 1L;
        Product originalProduct = productRepository.findById(productId)
            .orElseThrow(() -> new ProductNotFoundException("상품을 찾을 수 없습니다."));
        BigDecimal originalPrice = originalProduct.getPrice();

        // 음수 가격 업데이트 시도 -> CustomCheckedException 발생 및 트랜잭션 롤백
        assertThrows(CustomCheckedException.class, () -> {
            productService.updateProductPrice(productId, new BigDecimal("-10.00"));
        });

        // 롤백이 정상적으로 수행되어 가격이 변경되지 않았음을 확인
        Product productAfter = productRepository.findById(productId)
            .orElseThrow(() -> new ProductNotFoundException("상품을 찾을 수 없습니다."));
        assertEquals(originalPrice, productAfter.getPrice());
    }

    @Test
    void testNoRollbackForUncheckedException() {
        Long productId = 2L;
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new ProductNotFoundException("상품을 찾을 수 없습니다."));
        int originalStock = product.getStock();

        // 재고 부족 상태에서 reduceProductStockNoRollback 호출
        // IllegalArgumentException이 발생하더라도 noRollbackFor에 의해 트랜잭션은 커밋됨
        assertThrows(IllegalArgumentException.class, () -> {
            productService.reduceProductStockNoRollback(productId, originalStock + 10);
        });

        // 트랜잭션이 롤백되지 않았으므로, 재고가 변경되었는지 확인 (어플리케이션 로직에 따라 다를 수 있음)
        Product productAfter = productRepository.findById(productId)
            .orElseThrow(() -> new ProductNotFoundException("상품을 찾을 수 없습니다."));
        // 재고가 변경되지 않아야 정상이라면, 별도의 처리가 필요할 수 있음.
        assertEquals(originalStock, productAfter.getStock());
    }
}

이와 같이, Product 엔티티를 활용한 예제 코드를 통해 체크 예외와 언체크 예외에 따른 트랜잭션 롤백 동작을 세밀하게 제어할 수 있습니다.

  • rollbackFor를 통해 기본적으로 롤백되지 않는 체크 예외도 트랜잭션 롤백 대상으로 지정할 수 있으며,
  • noRollbackFor를 통해 특정 언체크 예외에 대해서는 롤백을 제외하는 설정이 가능합니다.

이를 통해 비즈니스 로직에 맞는 유연한 트랜잭션 관리가 가능해집니다.


3. 커밋 관리

트랜잭션 커밋은 작업이 성공적으로 완료된 경우 변경 사항을 데이터베이스에 영구적으로 저장하는 과정입니다. 작업의 성격과 데이터 양에 따라 전체 커밋부분 커밋 방식을 선택하여 효율성을 극대화할 수 있습니다.

3.1 부분 커밋과 전체 커밋의 차이

1. 전체 커밋

  • 개념:
    • 트랜잭션 내 모든 작업이 성공적으로 완료된 후, 한 번에 커밋합니다.
  • 장점:
    • 트랜잭션 단위가 명확하며, 작업 도중 예외가 발생하면 모든 변경 사항이 롤백됩니다.
  • 단점:
    • 대량 데이터 처리 시 메모리 사용량이 증가할 수 있고, 성능에 영향을 줄 수 있습니다.
  • 적용 사례:
    • 소규모 상품 리스트를 처리하거나, 모든 작업의 성공 여부를 보장해야 하는 경우.
  • 예제:
    @Transactional
    public void updateProductPrices(List<Product> products, BigDecimal priceIncrement) {
        for (Product product : products) {
          product.setPrice(product.getPrice().add(priceIncrement));
          productRepository.save(product);
        }
      }

2. 부분 커밋

  • 개념:
    • 작업을 일정 단위로 나누어, 각 단위마다 커밋합니다.
  • 장점:
    • 대량 데이터 처리 시 메모리 사용량을 줄이고 성능을 최적화할 수 있습니다.
  • 단점:
    • 일부 작업이 실패하더라도, 이미 커밋된 데이터는 롤백되지 않아 데이터 정합성을 별도로 관리해야 합니다.
  • 적용 사례:
    • 대량 상품 데이터 처리, 배치 업데이트, 대량 등록 등.
  • 예제:
    @Service
    public class ProductBatchService {
    
        @PersistenceContext
        private final EntityManager entityManager;
    
        /**
         * 대량의 제품 목록에 대해 재고를 감소시키는 작업을 배치 단위로 처리합니다.
         * 각 배치가 끝날 때마다 flush()와 clear()를 호출하여 메모리 사용량을 최적화합니다.
         */
      @Transactional
      public void batchUpdateProductStock(List<Product> productList, int stockDecrease) {
        int batchSize = 10;
    
        for (int i = 0; i < productList.size(); i++) {
          Product product = productList.get(i);
          // 판매 후 재고 감소 처리
          product.reduceStock(stockDecrease);
          entityManager.merge(product);
    
          // 배치 단위마다 커밋 및 영속성 컨텍스트 초기화
          if ((i + 1) % batchSize == 0) {
            flushAndClear();
          }
        }
    
        // 잔여 데이터 처리
        flushAndClear();
      }
    
      private void flushAndClear() {
        entityManager.flush(); // 변경 사항을 데이터베이스에 반영
        entityManager.clear(); // 영속성 컨텍스트 초기화 (캐시된 엔티티 제거)
      }
    }
    
profile
기록하고 소화해보자! 소화가 안되거나 까먹으면 다시 꺼내서 보자! 오늘의 나는 어제의 나보다 강하다!

0개의 댓글