Mybatis/성능 및 유지보수 팁
N+1 문제 해결
DEVLIB
2025. 4. 17. 10:21
728x90
N+1 문제란?
문제 설명
- 1개의 쿼리(N)를 날려서 엔티티 목록을 조회하고,
- 각각의 엔티티마다 추가 쿼리(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 절을 활용한 다건 조회
두 단계 방식
- 사용자 목록을 먼저 가져온 후,
- 사용자 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