본문 바로가기

Spring 정리/Spring JPA

일반 Join vs Fetch Join

728x90
반응형

* 개요

 

Fetch Join를 공부하고 정말 좋은 기능이며 성능에 뛰어나다는 사실을 알았습니다. 그렇게 공부하던 중, 일반 Join과 Fetch Join은 구체적으로 어떤 차이가 있는지 궁금해졌습니다. 따라서 예제를 기반으로 내용을 정리합니다.

 

 

* 조건

 

Author : Book이 1 : N로 연관관계를 가지고 있다고 가정합니다. 연관관계의 주인은 Author입니다. 특히, join은 FROM절에 N인 Book이 오느냐, 1인 Author가 오느냐에 따라서 성능이 차이가 납니다. 각 상황을 비교해봅니다.

 

 

* Author

 

@Entity 
public class Author implements Serializable { 
  private static final long serialVersionUID = 1L;
  
  @Id 
  private Long id; 
  private String name; 
  private String genre; 
  private int age; 
    
  @OneToMany(cascade = CascadeType.ALL, mappedBy = "author", orphanRemoval = true) 
  private List<Book> books = new ArrayList<>(); 
}

 

* Book

 

@Entity 
public class Book implements Serializable { 
  private static final long serialVersionUID = 1L; 

  @Id private Long id; 
  private String title; 
  private String isbn; 

  @ManyToOne(fetch = FetchType.LAZY) 
  @JoinColumn(name = "author_id") 
  private Author author; 
}

 

 

* From절에 1인 Author 사용하기

 

// INNER JOIN 
@Query(value = "SELECT a FROM Author a INNER JOIN a.books b WHERE b.price > ?1") 
List<Author> fetchAuthorsBooksByPriceInnerJoin(int price); 


// JOIN FETCH 
@Query(value = "SELECT a FROM Author a JOIN FETCH a.books b WHERE b.price > ?1") 
List<Author> fetchAuthorsBooksByPriceJoinFetch(int price);

 

2개의 JOIN이 있다고 가정합니다. 

 

public void fetchAuthorsBooksByPriceJoinFetch() { 
  List<Author> authors = authorRepository.fetchAuthorsBooksByPriceJoinFetch(40);
  authors.forEach((e) -> System.out.println("Author name: "  + e.getName() + ", books: " + e.getBooks())); 

}

@Transactional(readOnly = true) 
public void fetchAuthorsBooksByPriceInnerJoin() { 
  List<Author> authors = authorRepository.fetchAuthorsBooksByPriceInnerJoin(40); 
  authors.forEach((e) -> System.out.println("Author name: " 
      + e.getName() + ", books: " + e.getBooks())); 
}

 

 

Fetch Join의 결과는 다음과 같습니다.

 

SELECT 
  author0_.id AS id1_0_0_, 
  books1_.id AS id1_1_1_, 
  author0_.age AS age2_0_0_, 
  author0_.genre AS genre3_0_0_, 
  author0_.name AS name4_0_0_, 
  books1_.author_id AS author_i5_1_1_, 
  books1_.isbn AS isbn2_1_1_, 
  books1_.price AS price3_1_1_, 
  books1_.title AS title4_1_1_, 
  books1_.author_id AS author_i5_1_0__, 
  books1_.id AS id1_1_0__ 
FROM author author0_ 
INNER JOIN book books1_ 
  ON author0_.id = books1_.author_id 
WHERE books1_.price > ?

 

 

Inner Join의 결과는 다음과 같습니다.

 

SELECT 
  author0_.id AS id1_0_, 
  author0_.age AS age2_0_, 
  author0_.genre AS genre3_0_, 
  author0_.name AS name4_0_ 
FROM author author0_ 
INNER JOIN book books1_ 
  ON author0_.id = books1_.author_id 
WHERE books1_.price > ?

 

Inner Join은 From 절에 있는 Author 객체만 조회가 가능하며, Book 객체를 조회하지 못합니다.

따라서, Inner Join으로 Book 객체를 조회한다면, 아래와 같이 추가적인 쿼리가 필요합니다.

 

SELECT 
  books0_.author_id AS author_i5_1_0_, 
  books0_.id AS id1_1_0_, 
  books0_.id AS id1_1_1_, 
  books0_.author_id AS author_i5_1_1_, 
  books0_.isbn AS isbn2_1_1_, 
  books0_.price AS price3_1_1_,
  books0_.title AS title4_1_1_ 
FROM book books0_ 
WHERE books0_.author_id = ?

 

 

그렇다면, 연관관계에 있는 것까지 모두 조회할 수 있을까요? 아래와 같이 조건식이 있다면 어떻게 될까요?

 

 

@Query(value = "SELECT a, b FROM Author a INNER JOIN a.books b WHERE b.price > ?1")

 

 

 

 예를들어, Book의 price가 3000 이상인 쿼리를 실행한다면, Author만 조회됩니다. 이 Author가 getBooks()를 하면 price가 40이상이 아닌, 10, 20인 Book들이 조회될 수 있습니다. 아래 쿼리문에서 Book은 조회되지 않기 때문에, Author의 상태에 따라서 언제든지 price가 40 미만인 Book들이 조회가 가능합니다. WHERE절에 N인 Book 엔티티로 조건을 걸으면 오류가 발생할 확률이 높습니다.

 

Author name: Joana Nimar, 
       books: [ 
               Book{id=1, title=A History of Ancient Prague, isbn=001-JN, price=36}, //오류결과! 
               Book{id=2, title=A People's History, isbn=002-JN, price=41}
              ]

 

하지만, Fetch Join은 Author, Book 객체를 모두 조회할 수 있습니다.

추가적인 쿼리가 없더라도 한번에 모두 조회가 가능합니다.

 

 

* From절에 N인 Book 사용하기

 

Inner Join 2개와, Fetch Join 1개를 사용하는 경우를 확인합니다.

 

@Repository 
@Transactional(readOnly = true) 
public interface BookRepository extends JpaRepository<Book, Long> {
  // INNER JOIN BAD 
  @Query(value = "SELECT b FROM Book b INNER JOIN b.author a") 
  List<Book> fetchBooksAuthorsInnerJoinBad(); 

  // INNER JOIN GOOD 
  @Query(value = "SELECT b, a FROM Book b INNER JOIN b.author a") 
  List<Book> fetchBooksAuthorsInnerJoinGood(); 

  // JOIN FETCH 
  @Query(value = "SELECT b FROM Book b JOIN FETCH b.author a") 
  List<Book> fetchBooksAuthorsJoinFetch(); 
}

 

다음과 같이 모두 조회를 하고, Book을 조회합니다.

 

public void fetchBooksAuthorsJoinFetch() { 
  List<Book> books = bookRepository.fetchBooksAuthorsJoinFetch(); 
  books.forEach((e) -> System.out.println("Book title: " + e.getTitle() 
     + ", Isbn:" + e.getIsbn() + ", author: " + e.getAuthor())); 
}

@Transactional(readOnly = true) 
public void fetchBooksAuthorsInnerJoinBad/Good() { 
  List<Book> books = bookRepository.fetchBooksAuthorsInnerJoinBad/Good(); 
  books.forEach((e) -> System.out.println("Book title: " + e.getTitle() 
     + ", Isbn: " + e.getIsbn() + ", author: " + e.getAuthor())); 
}

 

 

Fetch Join의 결과입니다. Book과 Author 객체를 모두 한번에 조회합니다.

 

SELECT 
  book0_.id AS id1_1_0_, 
  author1_.id AS id1_0_1_, 
  book0_.author_id AS author_i5_1_0_, 
  book0_.isbn AS isbn2_1_0_, 
  book0_.price AS price3_1_0_, 
  book0_.title AS title4_1_0_, 
  author1_.age AS age2_0_1_, 
  author1_.genre AS genre3_0_1_, 
  author1_.name AS name4_0_1_ 
FROM book book0_ 
INNER JOIN author author1_ 
  ON book0_.author_id = author1_.id

 

 

Bad Inner Join의 결과입니다. SELECT 절에서 Book만 조회하기 때문에, Author를 조회하기 위해 추가 쿼리가 필요합니다.

 

SELECT 
  book0_.id AS id1_1_,
  book0_.author_id AS author_i5_1_, 
  book0_.isbn AS isbn2_1_, 
  book0_.price AS price3_1_, 
  book0_.title AS title4_1_ 
FROM book book0_ 
INNER JOIN author author1_ 
  ON book0_.author_id = author1_.id

 

Book과 연관관계인 Author 객체를 조회하기 위해 getAuthor()를 하면, 추가쿼리가 발생합니다.

 

SELECT 
  author0_.id AS id1_0_0_, 
  author0_.age AS age2_0_0_, 
  author0_.genre AS genre3_0_0_, 
  author0_.name AS name4_0_0_ 
FROM author author0_ 
WHERE author0_.id = ?

 

Good Inner Join의 결과입니다. Fetch Join과 같은 결과를 냅니다. 왜냐하면 FROM 절에 있는 Book이 N이고, Join 대상이 1인 Author이기 때문에, 조인대상이 1개밖에 없어서 Fetch Join처럼 한번 쿼리로 조회해 올 수 있습니다.

 

SELECT 
  book0_.id AS id1_1_0_, 
  author1_.id AS id1_0_1_, 
  book0_.author_id AS author_i5_1_0_, 
  book0_.isbn AS isbn2_1_0_, 
  book0_.price AS price3_1_0_, 
  book0_.title AS title4_1_0_, 
  author1_.age AS age2_0_1_, 
  author1_.genre AS genre3_0_1_, 
  author1_.name AS name4_0_1_ 
FROM book book0_ 
INNER JOIN author author1_ 
  ON book0_.author_id = author1_.id

 

기존에 Join이 무조건, FROM 절에 있는 엔티티만 조회가 가능하다고 오해했지만, 경우에 따라서는 연관관계도 조회가 가능합니다. 

 

 

* 그렇다면 Join과 Fetch Join은 각각 언제 사용해야 할까요?

 

일반 Join은 데이터의 변경 없이 단순히 조회만 한다면 Join Fetch보다 Join + Dto 구조가 좋습니다.

 

하지만, 데이터를 변경해야 하고, Hibernate가 연관 엔티티들을  SELECT에 포함시켜야 한다면 Join Fetch가 좋습니다. 이 때 일반 Join을 사용하면 N + 1 문제가 발생합니다.

 

 

* 결론

Join, Fetch Join 비교를 통해서 어떻게 쿼리를 짜느냐에 따라 결과가 달라지는지 확인했습니다. 특히, FROM절에 연관관계 주인이 오느냐에 따라서 사용방식과 결과가 완전히 달라집니다. 기존의 엔티티에 변형이 많이 되며, 단순히 데이터 조회목적이라면 Join을, 여러 엔티티를 한번에 조회해서 작업을 해야한다면 Fetch Join을 사용하는게 좋을 것 같습니다.

 

 

* 참고

How to Decide Between JOIN and JOIN FETCH

728x90
반응형

'Spring 정리 > Spring JPA' 카테고리의 다른 글

스프링 트랜잭션 전파 기본개념  (0) 2022.11.17
OSIV  (0) 2022.10.01
JPA를 이용해 History 테이블 자동 생성하기  (1) 2021.12.14
페치 조인 ( fetch join ) - 2  (0) 2021.10.06
페치 조인 ( fetch join ) -1  (0) 2021.10.05