입문 프로젝트 과제 도전 항목에서도 이 문제를 간단히 다룬 적이 있다.
목록 조회 1번에 연관 데이터 조회 N번이 더해지는 현상
엔티티 정의
Post가 User를 외래 키로 갖는 구조 (다대일 매핑)@Getter
@Entity
@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nickname;
public User(
String nickname
) {
this.nickname = nickname;
}
}
@Getter
@Entity
@Table(name = "posts")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
private User user;
public Post(
String title,
User user
) {
this.title = title;
this.user = user;
}
}
서비스 메서드(조회)
@Transactional(readOnly = true)
public List<String> nPlusOne() {
List<Post> posts = postRepository.findAll();
return posts.stream()
.map(post -> post.getTitle() + " / " + post.getUser().getNickname())
.toList();
}
유저 셋과 각 유저에 대한 게시글 post 요청을 보낸 뒤, 전체 조회를 시도하면 다음과 같은 로그를 얻는다.
Hibernate: select p1_0.id,p1_0.title,p1_0.user_id from posts p1_0
Hibernate: select u1_0.id,u1_0.nickname from users u1_0 where u1_0.id=?
Hibernate: select u1_0.id,u1_0.nickname from users u1_0 where u1_0.id=?
Hibernate: select u1_0.id,u1_0.nickname from users u1_0 where u1_0.id=?
첫 SELECT 요청에서는 Post의 속성인 id와 title, 외래키인 user_id까지만 가져온다.
postRepository.findAll()을 호출한 시점에, Post 안의 User 데이터는 가져오지 않는다. 이는 FetchType.LAZY의 특성이다. 메서드에서 post.getUser()를 호출하는 시점에 DB로부터 데이터를 받아온다.
그렇다고 FetchType.EAGER를 사용한다고 나아지진 않는다. (오히려 더 악화된다) postRepository.findAll()을 통한 조회 쿼리는 변하지 않기 때문에 여전히 Post 조회 시엔 User 데이터를 가져오지 않는데, EAGER 옵션이기 때문에 사용하든 말든 즉시 DB로부터 User 데이터를 추가로 요청하게 된다. 아래와 같이 User에 접근하지 않도록 코드를 교체했을 때, LAZY는 N번의 추가 쿼리를 요청하지 않지만 EAGER는 그대로 1+N 문제를 보이게 된다.
@Transactional(readOnly = true)
public List<String> nPlusOne() {
List<Post> posts = postRepository.findAll();
return posts.stream()
.map(post -> post.getTitle() /*+ " / " + post.getUser().getNickname()*/)
.toList();
}
fetch join연관 엔티티를 같은 쿼리로 함께 가져오는 JPQL 문법
레포지토리
public interface PostRepository extends JpaRepository<Post, Long> {
@Query("select p from Post p join fetch p.user")
List<Post> findAllWithUser();
}
@Query("select p from Post p join p.user")@Query("select p from Post p join fetch p.user")fetch join 적용 후 로그
Hibernate: select p1_0.id,p1_0.title,u1_0.id,u1_0.nickname from posts p1_0 join users u1_0 on u1_0.id=p1_0.user_id
SELECT 요청에서 Post의 속성뿐만 아니라 User의 속성까지 모두 불러오는 것을 확인할 수 있다.