Search
Duplicate
📒

[JPA 기본] 08-3. n+1문제

상태
완료
수업
JPA
주제
JPA
4 more properties
참고

JPA의 N+1문제

NOTE
앞서 즉시/지연 로딩으로 N+1을 임시적으로 해결했지만 이는 본질적인 문제해결방법은 아니다.
@Entity public class Member{ // 즉시 로딩을 사용하는 경우 @OneToMany(fetch = FetchType.Lazy) @JoinColumn(name="TEAM_ID") private Team team; // ... }
Java
복사
Member(1) - Team(N)의 관계라고 가정한다.
FetchType을 Lazy로 설정한다면 Member을 호출할때는 프록시 객체만 호출하므로 N+1문제가 해결된것처럼 보이지만, 실제 Team데이터를 사용하게되면 결국 N+1 문제가 발생하게된다.

N+1이 발생하는 이유는 무엇인가?

NOTE
Member와 Order과 관련되있다 해도 Member만 호출함
jpaRepositry에 정의한 인터페이스 메서드를 실행하면 JPA는 메서드 이름을 분석해서 JPQL을 생성하고 실행하게된다.
JPQL은 SQL에 종속되지 않고 Entity 객체와 필드 이름을 가지고 쿼리를한다.
그렇기 때문에 findAll()이란 메소드를 수행할 때, 해당 Entity를 조회하는 select * from Member 쿼리만 실행하게된다.
JPQL 입장에서는 연관관계의 데이터를 무시하고 해당 Entity기준으로만 쿼리를 조회하기 때문에 연관된 데이터가 필요한 경우, FetchType으로 지정한 시점에 조회를 별도로 호출하게 된다.

해결방안 1 - Fetch join

NOTE
실제 우리가 원하는 쿼리는 join을 통해 한번에 들고오는 로직이다.
select * from Member m left join Team ton m.member_id = t.team_id
Fetch join을 사용해서 우리가 원하는 join쿼리를 JPQL로 작성할 수 있다.
@Query("select o from Member m join fetch o.cats") List<Owner> findAllJoinFetch();
Java
복사
하지만 Fetch join을 사용하게된다면 호출시점에 모든 데이터를 들고오므로 Fetch Lazy로 해놓는것이 무의미하다.
또한, 페이징 쿼리를 사용할 수 없다. (하나의 쿼리문으로 가져오므로 페이징 단위로 들고오지 못함)

해결방안 2 - @EntityGraph

NOTE
@EntityGraph의 attributePaths에 쿼리 수행시 바로 가져올 필드명을 지정하면 Lazy가 아닌 Eager조회로 가져오게 된다.
@EntityGraph(attributePaths = "cats") @Query("select o from Owner o") List<Owner> findAllEntityGraph();
Java
복사
Fetch join과는 다르게 join문이 outer join으로 실행된다. 즉 카테시안 곱이 발생해서 중복데이터가 존재할 수 있다.

해결방안 3 - FetchMode.SUBSELECT

NOTE
FetchMode.SUBSELCT는 쿼리 2번으로 해결하는 방식이다.
@Entity @Getter @Setter @NoArgsConstructor public class Owner { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private String name; @Fetch(FetchMode.SUBSELECT) @OneToMany(mappedBy = "owner", fetch = FetchType.EAGER) private Set<Cat> cats = new LinkedHashSet<>(); }
Java
복사
해당 Entity를 조회하는 쿼리는 그대로 발생하고 연관관계 데이터를 조회할 때 서브 쿼리로 함께 조회한다.
즉시로딩으로 설정하면 조회시점에, 지연로딩으로 설정하면 Entity를 사용하는 시점에 쿼리가 동작한다.

해결방안 4 - QueryBuilder

NOTE
Query를 실행하도록 도와주는 다양한 플러그인 (Mybatis, QueryDSL, JDBC Template)등으로 최적화된 쿼리를 구현할 수 있다.
// QueryDSL로 구현한 예제 return from(owner).leftJoin(owner.cats, cat) .fetchJoin()
Java
복사

결론

NOTE
N+1문제는 JPA를 사용하면서 연관된 Entity를 사용한다면 마주치는 문제이다.
Fetch Type, EntityGraph등 다양한 해결방안이 있지만 가장 확실한 해결책은 직접 최적화된 쿼리를 작성하는 것이다.