Spring JPA(ORM)의 N+1 문제
N + 1?
ORM의 JPA를 사용하다 보면 n+1 문제가 발생하게 된다. 내가 의도치 않은 쿼리가 발생하는데 더불어 여러번 반복된다면 문제가 많아진다. 이러한 문제에 대해서 정리해보자.
흔히 n+1 문제라고 하는것은 연관관계로 매핑된 엔티티를 조회 시 의도치 않게 같은 쿼리가 여러번 실행되는 것을 말한다.
Member Entity
@Entity
@Getter
@Setter
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String userName;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
// 생략...
}
Team Entity
@Entity
@Getter
@Setter
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long teamId;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList();
// 생략...
}
위와 같은 엔티티가 있다고 가정할 때 Team은 여러개의 Member를 가질 수 있다. 이때 List 타입의 Team을 반복문을 통해 members 인스턴스를 조회 시 문제가 발생한다.
List<Team> teams = teamRepository.findAll();
for (Team team : teams) {
System.out.println("members = " + team.members());
}
JPA에는 Fetch Type이 있는데 쿼리 조회 시 즉시 매핑하여 데이터를 가져오는 Eager, 쿼리 조회 후 영속성 단계에서 하위 엔티티를 조회할 때 호출하는 Lazy가 있다. 결국 이 둘다 N+1이 발생하게 되는데 하나의 엔티티에 하위 엔티티를 호출 시 상위 엔티티의 리스트 만큼 JPQL이 다시 호출하기 떄문에 같은 쿼리가 N번 나타난게 된다.
이를 해결하기 위해서 여러가지의 방법이 있다.
📖 Fetch Join(패치 조인)
미리 쿼리로 조인하여 객체 타입으로 가져오는 것이다.
@Query("SELECT t FROM TEAM t JOIN FETCH t.members")
List<Team> findTeamJoinFetch();
List<Team> teams = findTeamJoinFetch();
for (Team team : teams) {
System.out.println("members = " + team.members());
}
결과를 확인해보면 쿼리는 1번만 발생하고 두 테이블을 조인하여 결과를 가져온다. 패치조인에는 단점이 2가지 있다.
JPA
가 제공하는Pageable
기능 사용 불가1:N
관계가 2개인 엔티티를 패치 조인 사용 불가
📖 Betch Size 조절
설정한 size
만큼 데이터를 미리 로딩한다. (JPQL이 where in 사용한다.)
📖 @EntityGraph
어노테이션으로 필드명을 지정하면 해당 필드가 지연 로딩이 아닌 즉시 로딩으로 조회 되는것 이라고 보시면 된다.