lazy loading이면 다 해결되나?
서론
JPA를 공부하며 연관관계를 설정할 때, 이런 생각이 들었다.
“N+1 문제? 그냥 모든 연관관계 FetchType.LAZY로 걸어두면 되는 거 아닌가?”
그렇게 생각하고 엔티티마다 LAZY를 설정하고 테스트를 통과시켰다.
하지만 실제 데이터 목록 조회 쿼리가 나갈 때 SQL 로그를 보니 **1번의 쿼리로 10개 게시글을 가져온 뒤, 각 댓글을 가져오느라 10번의 추가 쿼리가 쏘아지는 끔찍한 현상**을 목격했다.
이 글은 JPA N+1 문제의 발생 원인과 Fetch Join을 이용해 성능을 극대화한 실무 경험을 담고 있다.
부족한 내용이 있을 수 있지만, 나처럼 막 시작한 사람에게는 도움이 될 거라 생각한다.
📚 N+1 문제 발생 원인과 해결책
N+1 문제는 JPQL이 개별 객체 단위로 지연 로딩을 수행할 때 발생하는 데이터베이스 쿼리 비효율성입니다.
해결 전략
1. Fetch Join (패치 조인)
- 연관된 엔티티를 한 번의 조인(JOIN) 쿼리로 몽땅 함께 조회해 옴
- N+1 문제가 근본적으로 발생하지 않음
public interface BoardRepository extends JpaRepository {
@Query("SELECT b FROM Board b JOIN FETCH b.comments WHERE b.status = 'ACTIVE'")
List findAllWithComments();
}
2. @BatchSize 설정
- 지연 로딩 시 지정한 사이즈만큼 IN 쿼리로 모아서 가져옴
- 컬렉션 페이징 처리 시 Fetch Join의 중복 데이터 문제를 우회하는 탁월한 방법
정리하며
- LAZY 설정만으로는 N+1 문제를 막을 수 없으며, 객체 그래프 탐색 시 추가 쿼리가 나간다.
- 단건 조회에는 Fetch Join, 컬렉션 페이징 조회에는 @BatchSize를 적절히 조합하는 것이 베스트다.
SQL 로그를 꼼꼼히 확인하며 JPA 성능을 최적화하는 감각을 키우게 된 경험이었다.
함께 읽으면 좋은 글
- Redis Cache Stampede 현상 극복기: 캐시 적용하면 DB 부하 끝 아닌가?
- PostgreSQL FOR UPDATE SKIP LOCKED: DB 락 경합 없이 고성능 작업 큐 구축하기
- Spring Boot와 Redisson 분산 락: @Transactional만 걸면 되는 거 아니야?
공식 문서와 확인 자료
아래 1차 자료를 기준으로 명령 동작과 적용 조건을 다시 확인할 수 있습니다.