Spring Data Jpa 대신 MyBatis를 쓴다면?

szlee·2025년 1월 12일
//ProductService
public Page<ProductInfo> getProducts(ProductSearchQuery query) {
    Page<Product> products = productRepository.getProducts(query.toPageRequest());

    List<Stock> stocks = stockRepository.getStocks(
            products.getContent().stream()
                    .map(Product::getId)
                    .collect(Collectors.toList())
    );

    Map<Long, Stock> stockMap = stocks.stream()
            .collect(Collectors.toMap(
                    stock -> stock.getProduct().getId(),
                    stock -> stock
            ));

    return products.map(product ->
            ProductInfo.of(product, stockMap.get(product.getId())));
}
// StockRepository
public interface StockRepository {
    Stock getStock(Long productId);

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")})
    @Query("select s from Stock s where s.product.id = :productId")
    Stock getStockWithLock(@Param("productId") Long productId);

    List<Stock> getStocks(Collection<Long> productIds);

    Stock save(Stock stock);

    void deleteAll();

    Optional<Stock> findById(Long id);
}
// StockRepositoryImpl
@Repository
public interface StockRepositoryImpl extends JpaRepository<Stock, Long>, StockRepository {
    @Override
    default Stock getStock(Long productId) {
        return findByProductId(productId);
    }

    @Override
    default List<Stock> getStocks(Collection<Long> productIds) {
        return findByProductIdIn(productIds);
    }

    Stock findByProductId(Long productId);
    List<Stock> findByProductIdIn(Collection<Long> productIds);

    @Override
    default Stock save(Stock stock) {
        return saveAndFlush(stock);
    }

}

요렇게 Spring Data JPA를 사용한 코드가 있다.
하지만 JPA가 아닌 MyBatis를 ORM으로 사용한다면 어떻게 바뀔까?

MyBatis로 변환 시 주요 변경사항은 다음과 같다.

  • Repository 인터페이스 대신 Mapper 인터페이스를 사용
  • JPA 어노테이션 대신 MyBatis 어노테이션 또는 XML 매핑 사용
  • 엔티티 대신 DTO 사용이 일반적

Mapper 인터페이스

@Mapper
public interface StockMapper {
    Stock getStock(Long productId);
    
    @Select("SELECT * FROM stock WHERE product_id = #{productId} FOR UPDATE")
    @Options(timeout = 3000)
    Stock getStockWithLock(@Param("productId") Long productId);
    
    List<Stock> getStocksByProductIds(Collection<Long> productIds);
    
    void insertStock(Stock stock);
    
    void updateStock(Stock stock);
    
    void deleteAll();
    
    Stock findById(Long id);
}

매핑 XML 파일(StockMapper.xml)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 
<mapper namespace="com.example.mapper.StockMapper">
 
   <select id="getStock" resultType="Stock">
       SELECT * FROM stock 
     	WHERE product_id = #{productId}
   </select>
   
   <select id="getStocksByProductIds" resultType="Stock">
       SELECT * FROM stock 
       WHERE product_id IN 
       <foreach item="productId" collection="list" open="(" separator="," close=")">
           #{productId}
       </foreach>
   </select>
   
   <insert id="insertStock" parameterType="Stock">
       INSERT INTO stock (product_id, quantity, created_at, updated_at)
       VALUES (#{productId}, #{quantity}, #{createdAt}, #{updatedAt})
   </insert>
   
   <update id="updateStock" parameterType="Stock">
       UPDATE stock 
       SET quantity = #{quantity},
           updated_at = #{updatedAt}
       WHERE product_id = #{productId}
   </update>
 
</mapper>

그리고 서비스 레이어의 getProducts 메서드는 다음과 같이 변경된다.

public PageInfo<ProductInfo> getProducts(ProductSearchQuery query) {
    // MyBatis에서 페이징 처리
    PageHelper.startPage(query.getPageNum(), query.getPageSize());
    List<Product> products = productMapper.getProducts(query);
    
    // 재고 정보 조회
    List<Stock> stocks = stockMapper.getStocksByProductIds(
        products.stream()
            .map(Product::getId)
            .collect(Collectors.toList())
    );
    
    // 재고 정보 맵 생성
    Map<Long, Stock> stockMap = stocks.stream()
        .collect(Collectors.toMap(
            stock -> stock.getProductId(),
            stock -> stock
        ));
    
    // 결과 변환
    List<ProductInfo> productInfos = products.stream()
        .map(product -> 
            ProductInfo.of(product, stockMap.get(product.getId())))
        .collect(Collectors.toList());
    
    // PageInfo 생성 (MyBatis-PageHelper 사용)
    PageInfo<Product> pageInfo = new PageInfo<>(products);
    return new PageInfo<>(productInfos, pageInfo);
}
@Configuration
public class MyBatisConfig { // 페이징 처리를 위한 설정
    @Bean
    public PageHelper pageHelper() {
        PageHelper pageHelper = new PageHelper();
        Properties properties = new Properties();
        properties.setProperty("reasonable", "true");
        properties.setProperty("supportMethodsArguments", "true");
        properties.setProperty("returnPageInfo", "check");
        properties.setProperty("params", "count=countSql");
        pageHelper.setProperties(properties);
        return pageHelper;
    }
}

주요 변경사항
1. JPA의 Page 대신 MyBatis의 PageHelper 사용
2. JPA Repository 대신 MyBatis Mapper 사용
3. 낙관적/비관적 락킹을 위한 FOR UPDATE 구문 직접 사용
4. 엔티티 매핑 대신 직접 SQL 작성
5. 트랜잭션 관리는 @Transactional 어노테이션으로 동일하게 사용 가능

profile
🌱

0개의 댓글