TIL

7_1.트랜잭션 개념과 선언적/프로그래밍 방식 비교

꿀승 2025. 2. 4. 22:46
728x90
반응형
SMALL

학습내용

  1. 트랜잭션 개념 이해
  2. JDBC 트랜잭션 사용
  3. 트랜잭션 관리 방식

학습정리

1. 트랜잭션

  • 개념

    • 데이터베이스에서 여러 작업을 하나의 논리적 작업 단위로 묶어 처리 하는 것
  • 목적

    • 목적은 작업 도중 오류가 발생하더라도 데이터베이스의 무결성과 일관성을 보장
  • 트랜잭션의 주요 기능

    • 트랜잭션 시작

      -- 트랜잭션 시작 (데이터베이스에 따라 BEGIN이나 START TRANSACTION 사용)
      BEGIN;
      -- 또는
      START TRANSACTION;
    • 작업 수행

      -- 주문 시 특정 상품의 재고 감소
      UPDATE products
      SET stock = stock - 1
      WHERE id = 'productId';
      
      -- 예를 들어, 주문 내역을 기록하는 작업도 함께 수행할 수 있습니다.
      INSERT INTO categories (name) VALUES ('카테고리 명');
    • 커밋 또는 롤백

      -- 모든 작업이 성공적으로 수행되었다면:
      COMMIT;
      
      -- 작업 중 오류가 발생한 경우, 아래와 같이 롤백하여 변경 사항을 취소합니다:
      ROLLBACK;
      

2. JDBC 트랜잭션 사용

  • 예시 코드 사용

    1. Repository

      @Slf4j
      @Component
      @RequiredArgsConstructor
      public class CategoryJdbcRepository {
      
        //해당 소스코드는 @Transactional을 사용시에 일어나는 소스코드
        private final DataSource dataSource;
      
        //트랜잭션 적용 전 save
        public Category save(Category category) throws SQLException {
          String sql = "INSERT INTO categories (name) VALUES (?)";
          Connection connection = null;
          PreparedStatement preparedStatement = null;
      
          try {
            connection = dataSource.getConnection();
      
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, category.getName());
            preparedStatement.executeUpdate();
      
            return category;
          } catch (SQLException e) {
            log.error("insert Error", e);
            throw e;
          } finally {
            JdbcUtils.closeResultSet(null);
            JdbcUtils.closeStatement(preparedStatement);
            JdbcUtils.closeConnection(connection);
          }
      
        }
      
        public Category findById(Connection connection, Long categoryId) throws SQLException {
          String sql = "SELECT * FROM categories WHERE id = ?";
          PreparedStatement preparedStatement = null;
          ResultSet resultSet = null;
      
          try {
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setLong(1, categoryId);
            resultSet = preparedStatement.executeQuery();
      
            if (resultSet.next()) {
              return Category.builder()
                  .name(resultSet.getString("name"))
                  .build();
            } else {
              throw new NoSuchElementException("categoryId : " + categoryId);
            }
          } catch (SQLException e) {
            log.error("select Error", e);
            throw e;
          } finally {
            JdbcUtils.closeResultSet(resultSet);
            JdbcUtils.closeStatement(preparedStatement);
          }
      
        }
      
        public void update(Connection connection, Long categoryId, String name) throws SQLException {
          String sql = "UPDATE categories SET name = ? where id = ?";
      
          PreparedStatement preparedStatement = null;
      
          try {
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, name);
            preparedStatement.setLong(2, categoryId);
          } catch (SQLException e) {
            log.error("select Error", e);
            throw e;
          } finally {
            JdbcUtils.closeResultSet(null);
            JdbcUtils.closeStatement(preparedStatement);
          }
      
        }
      
      }
      
    2. Service

      @Slf4j
      @Service
      @RequiredArgsConstructor
      public class CategoryJdbcService {
      
        private final DataSource dataSource;
        private final CategoryJdbcRepository categoryJdbcRepository;
      
        //실제로 스프링내에서 트랜잭션 처리 로직
        //DB연결 -> 오토커밋끄고 -> 로직실행 -> 정상:커밋 / 실패:롤백 -> 오토커밋 다시 true -> DB연결끊기
        public void updateCategory(Long categoryId, String name) throws SQLException {
          Connection connection = dataSource.getConnection();
      
          try {
            connection.setAutoCommit(false);
      
            Category category = categoryJdbcRepository.findById(connection, categoryId);
            if (Objects.nonNull(category)) {
              categoryJdbcRepository.update(connection, categoryId, name);
            }
            connection.commit();
          } catch (SQLException e) {
            connection.rollback();
            throw new IllegalStateException(e);
          } finally {
            connection.setAutoCommit(true);
            connection.close();
          }
      
        }
      
      }
      
    3. Controller

      @RestController
      @RequiredArgsConstructor
      @RequestMapping("category")
      public class CategoryController {
      
        private final CategoryJdbcService categoryJdbcService;
      
        @PatchMapping("/{id}/name")
        public ApiResponse<JSONObject> updateByName(@RequestParam Long id,
            @RequestBody CategoryRequest request) throws SQLException {
          //예외처리는 추가적으로 필요
          categoryJdbcService.updateCategory(id, request.getName());
          return ApiResponse.Success(new JSONObject());
        }
      }

3. 트랜잭션 관리 방식

  • 프로그래밍 방식

    • Spring의PlatformTransactionManager 를 이용하여 트랜잭션 경계를 명시적으로 제어하는 방식

    • 예시 코드

      @Slf4j
      @Service
      @RequiredArgsConstructor
      public class ProductTransactionService {
      
        private final PlatformTransactionManager transactionManager;
        //jpa repository 그대로 사용
        private final ProductRepository productRepository;
      
        public void updateProductStock(Long productId, Integer quantity) {
          TransactionStatus status = transactionManager.getTransaction(
              new DefaultTransactionDefinition());
          try {
            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);
            //@Transactional이 없으니 명시적으로 save
            productRepository.save(product);
            //위에 로직이 트랜잭션으로 묶여 있으면 true
            log.info("isTransaction: {}", TransactionSynchronizationManager.isActualTransactionActive());
      
            transactionManager.commit(status);
          } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
          }
        }
      }
      
  • 선언적 트랜잭션 (@Transactional)

    • 특징
      • 장동 경계처리
      • 간결한 코드
      • 세밀한 제어 옵션
        • propagation : 트랜잭션 전파 규칙 설정
        • isolation : 트랜잭션 격리 수준 설정
        • readOnly : 읽기 전용 트랜잭션으로 최적화
        • rollbackFor : 롤백 대상으로 처리 할 예외를 지정
      • 예외 처리와 롤백
  • 선언적 트랜잭션과 프로그래밍 방식 비교

    항목 선언적 트랜잭션 프로그래밍 방식
    코드 간결성 어노테이션을 사용해 간단히 설정 직접 작성해야 하므로 복잡할 수 있음
    유지보수 트랜잭션 로직이 코드와 분리됨 트랜잭션 로직과 비즈니스 로직 혼합 가능
    유연성 전역 설정으로 동작 제어 가능 세부적인 제어 및 조건 처리 가능
    적용 사례 일반적인 트랜잭션 관리에 적합 복잡한 트랜잭션 로직 구현에 적합

참고자료

728x90
반응형
LIST