//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로 변환 시 주요 변경사항은 다음과 같다.
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 어노테이션으로 동일하게 사용 가능