[Spring] JPA 지연 로딩과 즉시 로딩

JPA

Java Persistence API

JPA에서 테이블 간 연관 관계는 객체의 참조를 통해 이루어집니다. 서비스가 커질 수록 연관 관계는 복잡해집니다. 그리고 각각의 객체가 가진 정보도 많아집니다.

객체 정보를 조회할 때 참조하는 객체들의 정보까지 모두 한 번에 가져오면 부담이 커집니다. 따라서 JPA는 참조하는 객체들의 데이터를 가져오는 시점을 정할 수 있는데, 이것이 FetchType입니다.

  • FetchType.EAGER : 데이터를 미리 읽어옵니다.
  • FetchType.LAZY : 실제 요청하는 순간 가져옵니다.
    • getter로 접근할 때 참조 객체들의 데이터를 가져옵니다.
    • N+1 문제가 발생할 수 있습니다.

N+1 문제가 발생하는 경우

Academy : Subject = 1 : N라고 해보겠습니다.

아래와 같은 코드에서 ‘academyRepository.findAll()’에서 전체를 조회하는 쿼리 1번을 수행합니다. 그리고 stream에서 각각의 subject를 조회하는 쿼리를 N번 수행합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Academy {
  // ...
  @OneToMany(cascade = CascadeType.ALL)
  @JoinColumn(name="academy_id")
  private List<Subject> subjects = new ArrayList<>();
}

public class Subject {
  // ...
  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "academy_id", foreignKey = @ForeignKey(name = "FK_SUBJECT_ACADEMY"))
  private Academy academy;
}

public class AcademyService {
  @Autowired
  private AcademyRepository academyRepository;

  @Transactional(readOnly = true)
  public List<String> findAllSubjectNames(){
      return extractSubjectNames(academyRepository.findAll()); // 쿼리 1 번 수행
  }

  /**
    * Lazy Load를 수행하기 위해 메소드를 별도로 생성
    */
  private List<String> extractSubjectNames(List<Academy> academies){
      log.info(">>>>>>>>[모든 과목을 추출한다]<<<<<<<<<");
      log.info("Academy Size : {}", academies.size());

      return academies.stream()
              .map(a -> a.getSubjects().get(0).getName()) // 쿼리 N번 수행
              .collect(Collectors.toList());
  }
}

해결방법 1. Join Fetch(Inner Join을 사용하는 방법)

repository에서 @Query를 사용하면 fetchType.LAZY여도 연관 객체까지 한 번에 조회를 합니다.

1
2
3
4
5
6
7
8
@Repository
public interface AcademyRepository extends JpaRepository<Academy, Long> {
  /**
  * 1. join fetch를 통한 조회
  */
  @Query("select a from Academy a join fetch a.subjects")
  List<Academy> findAllJoinFetch();
}

subject의 하위 entity까지 한 번에 조회하는 경우에는 두 번의 fetch join을 사용하면 됩니다.

1
2
3
4
5
6
7
8
@Repository
public interface AcademyRepository extends JpaRepository<Academy, Long> {
  /**
  * 5. Academy+Subject+Teacher를 join fetch로 조회
  */
  @Query("select a from Academy a join fetch a.subjects s join fetch s.teacher")
  List<Academy> findAllWithTeacher();
}

하지만 이 방법은 쿼리문이 추가됩니다. inner join을 사용합니다.

해결방법 2. @Entity graph (Outer Join을 사용하는 방법)

쿼리를 조금 더 간단하게 하고, @EntityGraph를 사용하면 LAZY가 아닌 EAGER로 조회를 합니다. 이 방법은 Outer Join을 사용합니다.

1
2
3
4
5
6
7
8
9
@Repository
public interface AcademyRepository extends JpaRepository<Academy, Long> {
  /**
  * 2. @EntityGraph
  */
  @EntityGraph(attributePaths = "subjects")
  @Query("select a from Academy a")
  List<Academy> findAllEntityGraph();
}
1
2
3
4
5
6
7
8
9
@Repository
public interface AcademyRepository extends JpaRepository<Academy, Long> {
  /**
  * 6. Academy+Subject+Teacher를 @EntityGraph 로 조회
  */
  @EntityGraph(attributePaths = {"subjects", "subjects.teacher"})
  @Query("select a from Academy a")
  List<Academy> findAllEntityGraphWithTeacher();
}

WIP

inner join과 outer join을 정리한 뒤 포스팅을 계속합니다.

Reference

Leave a comment