MyBatis foreach 구문을 통해 N + 1 문제 해결하기

Belluga·2021년 8월 4일
0

N+1 문제란?

한 번의 쿼리 수행을 위해 N번의 추가적인 쿼리를 수행해야 하는 문제입니다.

주문 생성시 발생하는 N+1 문제

@Transactional
public void placeOrder(CreateOrderRequestDto createOrderRequestDto, AuthMember authMember) {
    Order order = createOrderRequestDto.toEntity(authMember);
    orderMapper.createOrder(order);

    List<OrderProduct> orderProducts = createOrderRequestDto.getOrderProductRequestDtoList().stream().map(
            orderProductRequestDto ->
                    orderProductRequestDto.toEntity(order.getId(), productService.getById(orderProductRequestDto.getProductId()).getFixedPrice()))
            .collect(Collectors.toList());

    orderMapper.createOrderProducts(orderProducts);
}
{
   "orderProductRequestDtoList": [
        {
        "productId": "4",
        "count": "5"
        },
        {
        "productId": "6",
        "count": "5"
        }
    ]
}

주문 항목 리스트 정보를 가지고 주문 생성 기능을 수행시 반복문을 돌며 주문 항목의 수만큼 N번의 Select 쿼리를 수행합니다.

고객이 주문하고자 하는 상품이 시스템에 존재하는지, 주문 시점에 상품의 가격이 얼마인지 확인해야 하기 때문에 N개의 상품 id를 N번의 상품 테이블에 Select 쿼리를 통해 조회하게 됩니다.

왜 문제가 될까?

반복문에서 연쇄적으로 데이터베이스 쿼리를 호출하는 경우 반복적인 Blocking I/O로 인해 요청 쓰레드가 대기하게되며 최종적으로 요청에 대한 응답속도가 느려지게 될 수 있습니다.

foreach 구문을 통해 한번의 쿼리로 해결하기

<select id="getById" parameterType="long" resultType="com.flab.shopnsave.domain.Product">
    SELECT id, product_name, fixed_price, seller_id, sales_yn, create_date
        FROM PRODUCT
        WHERE id = #{id}
</select>

기존에는 상품 id로 상품 정보를 조회하는 쿼리를 N번 호출하였습니다.

<select id="getByIdList" resultType="com.flab.shopnsave.domain.Product">
    SELECT id, product_name, fixed_price, seller_id, sales_yn, create_date
    FROM PRODUCT
    WHERE id IN
    <foreach collection="list" item="item" open="(" close=")" separator=",">
        #{item}
    </foreach>
</select>

MyBatis foreach 구문을 사용하여
N번의 쿼리 호출이 아닌 한 번의 쿼리 호출로 I/O 횟수를 줄여줌으로써 성능을 높일 수 있습니다.

일반적으로 OR 연산자보다 IN 연산자가 실행 속도가 빠르하여 IN 연산자를 사용하였습니다. 이는 추후 알아보도록 하겠습니다.

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductMapper productMapper;

    public Map<Long, Product> getByIdList(List<Long> id) {
        Map<Long, Product> collect = productMapper.getByIdList(id).stream().collect(Collectors.toMap(Product::getId, product -> product));
        return null;
    }
}

상품 관련 Service 레이어에 getByIdList 로직을 추가하였습니다.

@Transactional
public void placeOrder(CreateOrderRequestDto createOrderRequestDto, AuthMember authMember) {
    Order order = createOrderRequestDto.toEntity(authMember);
    orderMapper.createOrder(order);

    // 주문 항목 리스트 내 상품 id 리스트 추출
    List<Long> productIdList = createOrderRequestDto.getOrderProductRequestDtoList().stream().map(
            CreateOrderProductRequestDto::getProductId).collect(Collectors.toList());

    Map<Long, Product> productById = productService.getByIdList(productIdList);

    List<OrderProduct> orderProducts = createOrderRequestDto.getOrderProductRequestDtoList().stream().map(
            orderProductRequestDto ->
                    orderProductRequestDto.toEntity(order.getId(), productById.get(orderProductRequestDto.getProductId()).getFixedPrice()))
            .collect(Collectors.toList());

    orderMapper.createOrderProducts(orderProducts);
}

ProductService의 getByIdList 로직을 사용한 최종 주문 생성 코드는 위와 같습니다.

성능 검증

@Component
@RequiredArgsConstructor
public class TestDataRunner implements ApplicationRunner {

    private final ProductService productService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        for(int i = 1; i <= 100; i++) {
            CreateProductRequestDto createProductRequestDto = CreateProductRequestDto.builder()
                .productName("test_product" + i)
                .fixedPrice(1000)
                .build();
            productService.createProduct(createProductRequestDto, 121);
        }
    }
}

데이터가 10개일때 100개일때 비교 테스트를 진행하기 위해 데이터베이스에 각기 다른 상품 N개를 추가하였습니다.
위와같이 ApplicationRunner 인터페이스를 구현한 클래스를 빈으로 등록해주면 스프링부트 구동시점에 run 메서드를 수행합니다.

http://www.objgen.com/

위 사이트에서는 json 더미 데이터를 생성할 수 있습니다.

placeOrder 메서드 수행 시간을 측정한 결과 데이터 10개일 때 약 1.4배(173 밀리초, 246 밀리초), 데이터 100개일 때 50배 성능이 개선되었음을 확인할 수 있었습니다.(177밀리초, 8935밀리초)

References

https://deveric.tistory.com/74

0개의 댓글