// 일반 JPQL — N+1 문제 발생@Query("SELECT p FROM Post p")List<Post>findAll();// fetch join JPQL — author를 함께 한 번에 조회@Query("SELECT p FROM Post p JOIN FETCH p.author")List<Post>findAllWithAuthor();// 실제 실행 SQL// SELECT p.*, u.*// FROM posts p// INNER JOIN users u ON p.author_id = u.id
// Post → User 다대일 관계@EntitypublicclassPost{@ManyToOne(fetch =FetchType.LAZY)@JoinColumn(name ="author_id")privateUser author;}// fetch join으로 author를 한 번에 조회@Query("SELECT p FROM Post p JOIN FETCH p.author WHERE p.status = :status")List<Post>findByStatusWithAuthor(@Param("status")String status);
// Post → Comments 일대다 관계@EntitypublicclassPost{@OneToMany(mappedBy ="post", fetch =FetchType.LAZY)privateList<Comment> comments;}// 컬렉션 fetch join@Query("SELECT DISTINCT p FROM Post p JOIN FETCH p.comments WHERE p.id = :id")Optional<Post>findByIdWithComments(@Param("id")Long id);// DISTINCT 필수 — OneToMany는 조인 시 Post가 N배로 중복 반환됨
// 위험: 컬렉션 fetch join + 페이징 → HHH90003004 경고// → JPA가 모든 데이터를 메모리에 올려놓고 페이징 처리@Query("SELECT p FROM Post p JOIN FETCH p.comments")Page<Post>findAll(Pageable pageable);// 위험// 안전한 방법 1: @BatchSize로 분리@Query("SELECT p FROM Post p")Page<Post>findAll(Pageable pageable);// Post만 페이징@OneToMany(mappedBy ="post")@BatchSize(size =100)privateList<Comment> comments;// 별도 IN 쿼리로 조회// 안전한 방법 2: DTO 직접 조회@Query("SELECT new dto.PostSummary(p.id, p.title, COUNT(c)) "+"FROM Post p LEFT JOIN p.comments c GROUP BY p.id, p.title")Page<PostSummary>findSummaries(Pageable pageable);
// 단일 연관(ManyToOne)은 여러 개 동시 fetch join 가능@Query("SELECT p FROM Post p JOIN FETCH p.author JOIN FETCH p.category")List<Post>findAllWithAuthorAndCategory();// 컬렉션(OneToMany)은 2개 이상 동시 fetch join 불가 — MultipleBagFetchException// 아래 코드는 예외 발생@Query("SELECT p FROM Post p JOIN FETCH p.comments JOIN FETCH p.tags")List<Post>findAll();// MultipleBagFetchException!// 해결: @BatchSize로 첫 번째 컬렉션만 fetch join, 나머지는 배치
// @EntityGraph로 동일한 효과 (간단한 경우)@EntityGraph(attributePaths ={"author"})List<Post>findByStatus(String status);