본문 바로가기
프로그래밍/스프링 자바

[JPA] N+1 문제 개념, 원인, 해결방안

by 커피는아아 2021. 7. 13.
반응형

[JPA] N+1 문제 ,개념, 원인 해결(@EntityGraph, Fetch)

  • 관계를 갖고 있는 엔티티안에서 조회시에 발생할 수 있는 문제
  • 1개의 쿼리를 원했지만 추가적으로 N번의 쿼리가 더 발생한다.

@EntityGraph, Fetch

  • 연관된 엔티티들을 SQL 한번에 조회하는 방법
  • Member N : 1 Team
    • Member.class
      • @ManyToOne Team team
        • 실무에서는 지연로딩(FetchType.LAZY)을 사용하자
    • Team.class
      • @OneToMay List members
        • OneToMany는 Default가 Lazy전략이다

지연로딩 (LazyLoading)

Member 조회시 Team은 각자객체로 조회해놓고 team을 사용하는 시점에 SQL이 별도로 날라간다.

설명

구현코드

//Todo Lazy로딩 작용 순수 Jpa  
@Test  
public void findMemberLazy() throws Exception {  
   //given  
   //member1 -> teamA  
   //member2 -> teamB  
   Team teamA \= new Team("teamA");  
   Team teamB \= new Team("teamB");  
   teamRepository.save(teamA);  
   teamRepository.save(teamB);  
   memberRepository.save(new Member("member1", 10, teamA));  
   memberRepository.save(new Member("member2", 20, teamB));  
   em.flush(); // DB에 반영  
   em.clear(); // 영속성컨텍스트를 날림  
   //when  
   // select 해서 멤버만 가져온다.  
   List<Member\> members \= memberRepository.findAll();  
   //then  
   for (Member member : members) {  
       //System.out.println("member.getUsername() = " + member.getUsername());  
​  
       System.out.println("member.getTeam() = " + member.getTeam().getClass());  
       //System.out.println("member.getTeam().getName() = " + member.getTeam().getName()); // team까지는 프록시라는 가짜객체 getName시 db에서 진짜객체를 가져온다.  
​  
  }  
}
//Jpql 페치 조인  
@Query("select m from Member m left join fetch m.team")  
List<Member\> findMemberFetchJoin();  
​  
//공통 메서드 오버라이드  
@Override  
@EntityGraph(attributePaths \= {"team"})  
List<Member\> findAll();  
​  
// JPQL + 엔티티 그래프  
@EntityGraph(attributePaths \= {"team"})  
@Query("select m from Member m")  
List<Member\> findMemberEntityGraph();  
​  
//메서드 이름으로 쿼리에서 특히 편리하다.  
@EntityGraph(attributePaths \= {"team"})  
List<Member\> findByUsername(String username);

raw 중복 문제

  • OneToOne OneToMany는 테이블이 1:1 매핑되기 때문에 중복의 문제가 발생하지 않는다.
  • 그러나 1: N 관계의 테이블에서 join을 사용하면 당연히 로우수는 증가하기에 데이터 중복의 문제가 발생한다.
    • JoinFetch는 Inner Join, Entity Graph는 Outer Join이라는 차이가 있으나
    • 공통적으로 카테시안 곱(Cartesian Product)이 발생하여 Member의 수만큼 Team이 중복 발생하게 됩니다.

해결방안

  1. 1:N 필드타입을 Set으로 선언한다.(중복을 허용하지 않기 때문)
    • 순서는 보장하려면 LinkedHashSet을 사용한다.
  2. distinct를 사용한다.

MultipleBagFetchException

  • 2개 이상의 OneToMany 테이블에 Fetch 조인 선언 했을 때

OneToOne,ManyToOne`과 같이 단일 관계의 자식 테이블에는 Fetch Join을 써도 됩니다

해결 방법

  • 자식 테이블 하나에만 Fetch Join을 걸고 나머진 Lazy Loading
    • 컬렉션 페치조인은 페이징 사용이 되지않는다.
    • 컬렉션 페치 조인은 1개만 사용할 수 있다.
  • 모든 자식 테이블을 다 Lazy Loading으로 하게 한다.

결론

  • Fetch Join이 없는 자식 엔티티에 관해서는 위에서 선언한 hibernate.default_batch_fetch_size 적용으로 100~1000개의 in 쿼리로 성능을 보장한다.
  • @OneToOne, @ManyToOne과 같이 1 관계의 자식 엔티티에 대해서는 모두 Fetch Join을 적용하여 한방 쿼리를 수행한다.

참조