DEVLIB 2025. 4. 17. 10:21
728x90

N+1 문제란?

문제 설명

  1. 1개의 쿼리(N)를 날려서 엔티티 목록을 조회하고,
  2. 각각의 엔티티마다 추가 쿼리(1)를 반복해서 실행하게 되는 구조

예: 사용자 리스트를 조회하고, 각 사용자에 대한 게시글 목록을 다시 조회할 때

SELECT * FROM users;             -- 1회
SELECT * FROM posts WHERE user_id = 1;  -- N회
SELECT * FROM posts WHERE user_id = 2;
...

결과적으로 N + 1개의 SQL이 실행되며, 데이터가 많아질수록 성능이 급격히 저하됩니다.


해결 방법 1: JOIN + collection 매핑

예: 사용자 1:N 게시글 목록

<resultMap id="userWithPosts" type="User">
  <id property="id" column="user_id"/>
  <result property="name" column="user_name"/>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="title" column="post_title"/>
  </collection>
</resultMap>

<select id="findUsersWithPosts" resultMap="userWithPosts">
  SELECT
    u.id AS user_id, u.name AS user_name,
    p.id AS post_id, p.title AS post_title
  FROM users u
  LEFT JOIN posts p ON u.id = p.user_id
</select>

1개의 SQL로 모든 데이터를 가져와서 MyBatis가 객체 조합을 자동으로 처리합니다.


해결 방법 2: IN 절을 활용한 다건 조회

두 단계 방식

  1. 사용자 목록을 먼저 가져온 후,
  2. 사용자 ID 리스트로 게시글 목록을 IN 조건으로 조회
<resultMap id="userWithPosts" type="User">
  <id property="id" column="user_id"/>
  <result property="name" column="user_name"/>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="title" column="post_title"/>
  </collection>
</resultMap>

<select id="findUsersWithPosts" resultMap="userWithPosts">
  SELECT
    u.id AS user_id, u.name AS user_name,
    p.id AS post_id, p.title AS post_title
  FROM users u
  LEFT JOIN posts p ON u.id = p.user_id
</select>

 

Java 측 코드 (Service 계층)

List<User> users = userMapper.findAll();
List<Long> ids = users.stream().map(User::getId).collect(Collectors.toList());
List<Post> posts = postMapper.findByUserIds(ids);
 

두 번의 쿼리만 실행되므로 N+1 → 1+1로 성능 개선됨


해결 방법 3: 캐시 또는 지연 로딩 전략

  • lazyLoadingEnabled = true를 설정하고, 꼭 필요할 때만 하위 데이터를 가져오게 하는 방법
  • 다만 MyBatis는 JPA처럼 강력한 지연 로딩 컨트롤은 어렵기 때문에 JOIN 전략이 주력입니다

언제 어떤 방식 쓸까?

상황 해결 방법
1:N 조인 가능 JOIN + collection 매핑 (권장)
JOIN 시 데이터 중복 부담 IN 절 + Java merge 전략
복잡한 관계 또는 모듈 분리 구조 지연 로딩 or 단건 쿼리 유지

성능 주의사항

  • JOIN 시 데이터 중복이 심할 경우, 실제 반환 row 수가 많아지므로 상황에 따라 IN 방식이 더 유리
  • 페이징 + JOIN은 조합이 어려우므로 1+1 방식 + Java 매핑 전략 추천

마무리 요약

방법  특징 추천 상황
JOIN + <collection> 1개의 SQL로 객체 리스트 조합 일반적인 1:N 관계
IN + foreach 2번 쿼리로 대량 조회 대응 조건 많은 경우
Lazy Loading 필요한 순간만 로딩 데이터 규모 작고 단순한 구조
LIST