JPA 즉시로딩과 지연로딩, N+1 문제, Fetch Join을 통한 해결

ㅎㅎ·2023년 10월 5일
1

지연로딩

  • 연관관계 엔티티를 프록시로 조회하도록 하고, 프록시 객체의 실제 속성을 이용하기 위해 메소드를 호출할 때 쿼리를 날려 실제 값을 조회하여 객체를 생성하는 것
  • 객체 자체만 호출했을 때는 쿼리를 날리지 않고 프록시 객체를 반환함
  • 객체의 속성을 조회하는 메소드를 실행하면 그때 해당 값이 필요하다고 판단하고 DB에 접근하여 쿼리를 조회한 후 값을 가져옴
  • 연관관계 어노테이션에 fetch = FetchType.LAZY를 명시해서 사용

즉시로딩

  • 연관관계 엔티티들을 함께 자주 사용한다면 지연로딩이 의미가 없음. 오히려 쿼리를 여러번 날려서 손해임. 이럴 때 연관관계 엔티티 값도 조인 쿼리를 날려 한번에 조회하도록 하는 방법(대부분의 JPA 구현체에서 이 방법을 사용)
  • fetch = FetchType.EAGER를 명시하여 즉시로딩을 사용

프록시와 즉시로딩 주의

  • 가급적이면 지연로딩을 사용해야 한다. (특히 실무에서)
  • 즉시로딩을 사용하면 전혀 예상치 못한 SQL 문이 나감
  • 연관관계가 1~2개일 때는 상관없어 보이지만, 10개 그 이상일 때 10개 테이블을 모두 조인해서 한꺼번에 가져오게 됨. 엄청난 쿼리가 나가고 속도가 느려짐.
  • @ManyToOne, @OneToOne은 기본이 즉시로딩이므로 LAZY 명시해줘야함.
  • 나머지 어노테이션도 가독성을 위해 Default임에도 불구하고 LAZY를 명시하는 것을 추천

" 즉시로딩은 JPQL에서 N+1문제를 발생시킴 " (중요)

N + 1 문제란?

  • 객체 하나를 조회했는데, 연관관계로 물고 있는 엔티티를 조회하기 위해 엔티티 개수 N만큼 더 DB에 접근하여 쿼리를 실행함
  • 엔티티 매니저를 이용해서 JPA 내장 메소드를 이용할 때는 즉시로딩이 문제가 되지 않음. 즉시로딩을 설정하면 JPA가 이를 해석하고 위에서 말한대로 JOIN을 통해 값을 한 번에 가져옴.

  • 하지만 문제는 JPA를 위한 쿼리언어인 JPQL을 사용할 때임

  • JPA는 JPQL을 SQL문처럼 그대로 해석해서 쿼리를 날리기 때문에 아래와 같은 쿼리에서 우선 멤버 엔티티만 조회함.

    "select m from Member m"

  • 이후에 Member 엔티티에 연관관계가 매핑된 Team 엔티티가 있으면 다시 DB에 접근해서 쿼리를 날림.

  • 즉 엔티티매니저를 이용할 때와 JPQL을 사용할 때 즉시로딩이 동작하는 방식에 차이가 있음.

  • Spring Data JPA도 내부적으로 JPQL을 만들어서 동작하기 때문에, findAll()과 같은 내장 메소드 이용시 위와 동일한 문제가 발생함.


지연로딩에서의 N+1 문제

  • 지연로딩에서는 N+1 문제가 발생하지 않을까?
  • 지연로딩 역시, 프록시 객체로 임시로 조회했던 연관관계 엔티티의 필드를 호출하는 접근을 하게 되면 그때 DB에 접근하므로 역시 N+1 문제를 발생시킴

"select m from Member"

List<Member> members = EntityManager.findAll(Member.class)
  • 위 JPQL 혹은 앤티티매니저를 이용해 모든 멤버를 조회한 상황에서 아래의 코드를 실행 시에, 프록시 객체였던 팀 엔티티를 조회하기 위해 최대 N번 DB에 다시 접근함(만약 이미 한 번 조회한 팀이면 1차 캐시에 있기 때문에 해당 팀 정보를 가져옴)
  
List<Member> members = EntityManager.findAll(Member.class)  
for(int i=0; i<; i++){
  System.out.println(members.get(i).getTeam().getName();     

}                               
                               

  • 즉 어떤 로딩이든 N+1 문제가 발생하게 됨

해결방법 지연로딩으로 설정 후 Fetch Join 전략

  • 즉시로딩은 JPQL이 알아서 동작하기 때문에 커스텀해서 바꿀 수 있는 것이 없음
  • 대신 지연로딩을 사용해서 즉시로딩을 하지 않고, 연관 엔티티들을 한꺼번에 조회하고 싶을 때 fetch join을 사용하면 됨.
  • JPQL에서 "select m from Member m join fetch m.team"로 조인 전략을 설정해주면 JPQL이 JOIN을 통해 한 번의 쿼리에 모든 값을 가져옴.
  • 지연로딩 설정 해도 Fetch Join이 우선함
  • 혹은 @EntitiyGraph 어노테이션을 이용하여 fetch join 전략을 명시하면 JPQL 구문에 join만 넣어도 알아서 fetch join을 실행해줌

참고: JPA 모든 N+1 발생 케이스과 해결책, 자바 ORM 표준 JPA 프로그래밍

profile
Hello World

0개의 댓글