JPA 지연 로딩과 JOIN FETCH

지찬우·2023년 2월 14일
0

Knowing

목록 보기
8/10
post-thumbnail

오늘도 프로젝트를 진행 중, 오류를 마주쳤다. (JPA를 잘 알지도 못하면서 덤벼든 나의 잘못이다..)

LazyInitializationException: failed to lazily initialize a collection of role: ...

저번에도 마주쳤던 그 녀석이다. 오류가 발생한 배경은 이렇다.

오류 발생 배경

도서의 태그가 붙는데 도서와 태그는 다대다 관계이다. 그래서 다대일, 일대다 관계로 풀어내기 위해 BookTag 엔티티를 추가했다. (BookBookTag와 일대다 관계) @OneToMany는 기본 fetch 전략이 Lazy인 것은 안다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Book {

    ...

    @OneToMany(mappedBy = "book")
    private List<BookTag> bookTags = new ArrayList<>();

    ...
}

BookbookTags를 받아 BookTag 객체에서 Tag를 꺼내 이름을 다시 리스트에 담는 로직이다. 여기서 BookTag 객체를 리스트에서 꺼낼 때 오류가 발생한 것이다.

...

@Override
public List<String> listConvertBookTagToString(List<BookTag> bookTags) {
    List<String> tags = new ArrayList<>();
    bookTags.forEach(bookTag -> { // 이 부분
        String tagName = bookTag.getTag().getTagName();
        tags.add(tagName);
    });
    return tags;
}

...

패치 전략을 Eager로 바꿔주면 잘 조회가 됐지만 분명히 이 방법은 좋은 방법이 아닐 것이다. (느낌이 말해준다.)

그나마 최근에 JPA 강의를 들으며 공부 중이었는데 이 부분에 대해 강사님께서 패치 조인이나 엔티티 그래프 기능(?) 등을 통해 해결이 가능하다고 하셨던 기억이 났다. 그래서 fetch join을 열심히 검색해 보고 적용해 봤다.

패치 조인은 연관된 엔티티를 한 번의 쿼리로 함께 조회하는 방식으로, JPQL에서 성능 최적화를 위해 제공하는 기능이라고 한다.


열심히 해결해 보자!

Repository@Query 애노테이션을 사용해 fetch join 메서드를 만들어 봤다.

SELECT b FROM Book b JOIN FETCH b.bookTags WHERE b.id = :bookId

...

public interface BookRepository extends JpaRepository<Book, Long> {

	...

	@Query("SELECT b FROM Book b JOIN FETCH b.bookTags WHERE b.id = :bookId")
	Optional<Book> findByIdFetchJoin(@Param("bookId") Long id);

	...

}

기대를 품고 요청을 보내 보았다..! (두근두근..) 하지만 돌아오는 것은 LazyInitializationException

하지만 이번에는 오류 내용이 조금 달랐다.

could not initialize proxy [knu.networksecuritylab.appserver.entity.book.Tag#2]

@xToOne 연관관계 매핑의 경우 기본 패치 전략이 Eager라서 Lazy로 설정해 두었기 때문에 BookTagTag도 함께 패치 조인을 해야 하는 것이다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BookTag {

    ...

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "tag_id")
    private Tag tag;

    ...
}

이렇게 JOIN FETCH를 두 번 써서 해결이 가능했다 ㅎㅎ

...

public interface BookRepository extends JpaRepository<Book, Long> {

	...

	@Query("SELECT b FROM Book b JOIN FETCH b.bookTags bt JOIN FETCH bt.tag WHERE b.id = :bookId")
	Optional<Book> findByIdFetchJoin(@Param("bookId") Long id);

	...

}

아니다. 해결이 안 됐다. 만약 태그가 없는 책을 조회하면 ‘해당 도서가 존재하지 않습니다.’라는 예외가 발생했다. (null인 경우에 해당 예외가 던져지도록 예외 처리를 해두었었다.)


난 아까 그 오류보다 이 오류가 더 골치 아팠다 ㅋㅋㅋㅋ

이유를 찾았다. 쿼리문을 보니 inner join으로 날아간다. inner join이면 양쪽 테이블에 값이 있어야 조회되는 것은 대학교 데이터베이스 수업 때 배웠었다. 왼쪽 테이블이 기준이 되어야 하니까 left join으로 날아가야 제대로 조회될 것이다. 근데 left 조인으로 어떻게 하지..

https://www.logicbig.com/tutorials/java-ee-tutorial/jpa/fetch-join.html

그냥 LEFT JOIN FETCH라고 작성하면 된단다. 멍청하게 JOIN FETCH가 하나의 명령어라는 고정관념에 LEFT JOIN이라고 작성할 생각을 못 했다.


진짜 최종 코드! (점점 길어지는 쿼리..)

...

public interface BookRepository extends JpaRepository<Book, Long> {

	...

	@Query("SELECT b FROM Book b LEFT JOIN FETCH b.bookTags bt LEFT JOIN FETCH bt.tag WHERE b.id = :bookId")
	Optional<Book> findByIdFetchJoin(@Param("bookId") Long id);

	...

}

이젠 책이 잘 조회된다 ㅎㅎ😆


아직 의문이 하나 남았다. 내 생각에는 BookTagTag 패치 조인 시에는 left join을 사용하지 않아도 될 것 같아서 그냥 JOIN FETCH를 사용해 봤는데 태그가 없는 책은 조회가 되지 않았다. 이 부분은 열심히 고민해 보고 해결하면 추가로 작성해야겠다.

2023년 02월 16일 00시 12분

매우 간단한 이유였다. 난 당연히 뒤에 있는 조인이 수행되고 이후에 앞에 있는 조인이 수행될 것이라고 생각했는데 아니었다. 앞의 조인이 먼저 수행되고 뒤의 조인이 수행되기 때문에 뒤의 조인도 LEFT JOIN이 되어야 했던 것이다.😅

profile
좋은 개발자가 되자.

0개의 댓글