N + 1 문제란?
•
정의: 연관된 엔티티를 조회할 때, 1개의 쿼리(N=1)로 시작하지만 연관된 데이터를 각각 조회하면서 N개의 쿼리가 추가로 실행되는 문제.
•
예시: 게시글 10개를 조회 → 각 게시글의 작성자 정보를 지연 로딩 → 총 11개의 쿼리 실행
원인: 지연 로딩(Lazy Loading)
•
JPA는 성능 최적화를 위해 @ManyToOne, @OneToMany 관계를 기본적으로 LAZY로 설정
•
하지만 지연 로딩된 객체를 루프에서 꺼내면 N번의 쿼리가 추가 실행됨 → N + 1 발생
해결 가이드라인
1.
Fetch Join 사용 (JPQL)
@Query("SELECT p FROM Post p JOIN FETCH p.member")
List<Post> findAllWithMember();
Java
복사
•
JOIN FETCH를 사용해 연관 객체를 한 번에 함께 조회
•
가장 일반적인 해결 방법
2.
EntityGraph 사용 (Spring Data JPA)
@EntityGraph(attributePaths = {"member"})
@Query("SELECT p FROM Post p")
List<Post> findAllWithMember()
Java
복사
•
쿼리는 명시하지 않고 연관 엔티티를 함께 조회
•
동적 쿼리가 필요 없을 때 사용
3.
FetchType 설정 주의
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
Java
복사
•
기본적으로 LAZY 유지 (EAGER는 오히려 성능 악화 및 순환 참조 위험)
•
직접 사용하는 시점에서 JOIN FETCH 또는 EntityGraph로 해결
4.
QueryDSL 사용 시 fetchJoin
queryFactory
.selectFrom(post)
.join(post.member, member).fetchJoin()
.fetch();
Java
복사
•
QueryDSL 사용 시에도 fetchJoin 명시
•
동적 쿼리에 유리
5.
배치 사이즈 설정 (컬렉션 처리)
컬렉션 조회 시 N + 1 발생 → Hibernate의 in-query 방식으로 줄이기
# application.yml
spring:
jpa:
properties:
hibernate.default_batch_fetch_size: 100
YAML
복사
@OneToMany(mappedBy = "post")
private List<Comment> comments
Java
복사
•
컬렉션이 있을 경우 설정한 batch size만큼 IN 쿼리로 처리
•
즉시 로딩 아님, 지연 로딩 시 효율적 로딩
6.
DTO 직접 조회 (JPQL or QueryDSL)
@Query("SELECT new com.example.dto.PostDto(p.id, p.title, m.name) FROM Post p JOIN p.member m")
List<PostDto> findAllPostDto();
Java
복사
•
필요한 데이터만 조회하여 DTO로 직접 매핑
•
오버페치 방지 + 성능 최적화
7.
캐시 활용
•
Redis 같은 캐시를 통해 자주 조회되는 연관 객체 캐싱
•
지연 로딩된 객체를 반복 조회 시 캐시 활용 가능