[java] MyBatis에서 N+1 쿼리 문제 해결하기

MyBatis는 Java 개발자들이 SQL 쿼리와 데이터베이스를 효과적으로 다룰 수 있게 도와주는 ORM(Object-Relational Mapping) 프레임워크입니다. 그러나 MyBatis를 사용하다 보면 N+1 쿼리 문제에 직면할 수 있습니다. 이 문제는 한 개의 쿼리로 조회하는 대신, N개의 추가 쿼리가 발생해 성능에 부정적인 영향을 줄 수 있습니다.

N+1 쿼리 문제란?

N+1 쿼리 문제는 다음과 같은 상황에서 발생합니다.

  1. 한 개의 쿼리로 N개의 엔티티를 조회합니다.
  2. 각 엔티티의 연관된 엔티티를 조회하기 위해 추가적인 N개의 쿼리가 실행됩니다.

이렇게 되면 원래의 한 개의 쿼리 대신 N+1개의 쿼리가 실행되므로 성능 저하의 원인이 됩니다.

N+1 쿼리 문제 해결 방법

N+1 쿼리 문제를 해결하는 방법은 크게 두 가지입니다.

1. 페치 조인 사용하기

페치 조인(fetch join)은 MyBatis에서 제공하는 기능으로, 연관된 엔티티를 함께 조회합니다. 이렇게 함으로써 추가적인 쿼리 없이 한 번의 쿼리로 모든 데이터를 조회할 수 있습니다.

@Select("SELECT * FROM orders o JOIN customers c ON o.customer_id = c.id")
@Results({
  @Result(property = "id", column = "id")
  @Result(property = "customer", column = "customer_id", 
          one = @One(select = "com.example.CustomerMapper.findById"))
})
List<Order> findAll();

위의 예시에서는 orders 테이블과 customers 테이블을 연관시키는 페치 조인을 사용하여 한 번의 쿼리로 모든 주문과 주문 고객을 함께 조회합니다.

2. 지연 로딩 사용하기

지연 로딩(lazy loading)은 연관된 엔티티를 실제로 사용할 때까지 로딩을 미루는 방법입니다. 이를 통해 초기 쿼리 수행 시 N+1 쿼리 문제를 해결할 수 있습니다. 다만, 실제로 데이터를 사용할 때 연관된 엔티티를 조회하는 추가적인 쿼리가 실행됩니다.

@Select("SELECT * FROM customers")
List<Customer> findAll();

@Select("SELECT * FROM orders WHERE customer_id = #{customerId}")
List<Order> findOrdersByCustomerId(int customerId);

위의 예시에서는 findAll 메소드로 모든 고객을 조회할 때는 주문 정보를 로딩하지 않습니다. 대신, 실제로 주문 정보가 필요한 경우에는 findOrdersByCustomerId 메소드를 호출하여 연관된 주문 정보를 조회합니다.

결론

MyBatis를 사용할 때 N+1 쿼리 문제에 직면할 수 있습니다. 이 문제를 해결하기 위해 페치 조인과 지연 로딩을 적절히 사용하여 성능 저하를 방지할 수 있습니다. 이를 통해 MyBatis를 더욱 효과적으로 활용할 수 있습니다.

참고 자료: