fetch join, join, entityGraph 1

연어는결국강으로·2023년 5월 20일
0

JPA를 사용해서 토이프로젝트를 하는 중에 아래의 함수를 작성하면서 배운것이다. 아래 함수는 어떤 목표가 로그인한 사람의 목표인지 확인하는 메서드이다.

    /**
     * @param goalId    계획 글의 id
     * @param loginUser 로그인한 유저
     * @throws AccessDeniedException   계획의 소유자가 아닌 경우
     * @throws EntityNotFoundException 엔티티가 존재하지 않는 경우
     * @apiNote 계획의 소유자인지 판정하는 메서드
     */
    public void isGoalOwner(Long goalId, User loginUser) throws AccessDeniedException {
        Goal goal = goalRepository.getGoalToJudgeOwner(goalId).orElseThrow(() -> {
            throw new EntityNotFoundException("해당 계획이 DB에 없습니다.");
        });
        User ownerUser = goal.getPlan().getMember().getUser();

        if (!ownerUser.getUsername().equals(loginUser.getUsername())) {
            throw new AccessDeniedException("당신의 계획이 아닙니다.");
        }
    }

메서드에서 보면 알겠지만 단지 username만 있으면 된다. 그런데 이것을 처음에는 goal -> plan -> teamMember -> user -> username 으로 찾았다. 작성하고나니 엄청난 N+1 문제가 생길게 눈에 보였다. 이것을 처음 개선한 형태는 아래처럼 @EntityGraph로 개선하려고 했다.

@EntityGraph(attributePaths = {"plan.member.user.username"})
Optional<Goal> getGoalToJudgeOwner(Long goalId);

그러나 아래와 같은 이유로 fetch join을 사용하는게 더 낫다고한다.
1. 성능 측면 : fetch join은 데이터베이스에서 한 번의 쿼리로 필요한 정보를 함께 로드한다. 따라서 데이터베이스 호출 수를 줄이고 성능을 향상시킬 수 있다. 반면, EntityGraph는 여전히 추가적인 쿼리 호출을 발생시킬 수 있기 때문에 fetch join이 더 효율적일 수 있다.
2. 코드 가독성 : fetch join은 쿼리 내에서 명시적으로 연관 엔티티를 함께 로드하는 방식이므로 코드의 의도가 명확하게 드러난다. 반면 EntityGraph는 애플리케이션 레벨에서 로딩되기 때문에 명시적인 조인 표현이 없어 코드의 가독성이 다소 저하될 수 있다.
3. 유지 보수 측면 : fetch join은 쿼리를 직접 작성하여 필요한 정보를 선택적으로 가져올 수 있기 때문에, 복잡한 쿼리 작성이 요구되는 상황에서 유연성이 높을 수 있다. EntityGraph는 애노테이션 기반으로 동작하므로, 쿼리의 유지 보수에는 제약이 있을 수 있다.

다른건 뭐 그렇구나 하겠는데, 굵은 글씨로 해놓은 부분은 잘 이해가 가지 않았다.

@EntityGraph는 어째서 여전히 추가적인 쿼리 호출을 발생시킬 수 있는걸까? JPA에서는 기본적으로 연관 엔티티를 Lazy Loading으로 설정한다. 따라서 @EntityGraph를 사용하여 연관 엔티티를 명시적으로 로드해도, 해당 연관 엔티티에 대한 실제 데이터베이스 쿼리 호출은 필요한 시점에 발생한다. 이로 인해 추가적인 쿼리 호출이 발생할 수 있다.

또한, @EntityGraph는 기본적으로 연관 엔티티를 로드할 때 LEFT Join을 사용한다. 이는 Lazy Loading과 함께 사용될 때 N+1 문제를 완전히 해결하지는 못할 수 있다. 예를 들면, 한 번의 쿼리로 목표를 가져온 다음, 각 목표에 대한 계획 정보를 @EntityGraph로 로드하면 목표 개수만큼 추가적인 쿼리 호출이 발생할 수 있는 것이다.

그렇다. 이렇기 때문에 추가적인 쿼리 호출을 발생시킬 수 있는 것이다. 내 Entity들도 대부분 Lazy Loading으로 설정해놨기 때문에, 추가적인 쿼리 호출이 발생할지도 모른다. 아 물론 저 경우에는 안하지 않을까 싶다. 왜냐하면 딱한번 조회하는 시점에 username까지 다 조회하고 거기에 있는 것들을 다시 꺼내쓰지는 않으니까 말이다.

그러고나서 검토해보니 그렇지는 않다. 나의 경우 습관적으로 User까지 호출하고, 그다음에 username을 또 꺼내서 쓴다. 아 그럼 이게 쿼리 한번 더 호출하는 결과를 낳겠구나 싶다. 이 부분을 고쳐썻다.

이제 내 상황에서는 그게 그거라는 소리이다! 그럼에도 fetch join으로 바꿔볼꺼다. 왜냐하면 공부해야되니까 ㅎ 그리고 가독성이 더 좋다잖아~~ 아래는 fetch join 쿼리로 바꾼 것이다.

@Query("SELECT g FROM Goal g JOIN FETCH g.plan p JOIN FETCH p.member m JOIN FETCH m.user WHERE g.id = :goalId")
Optional<Goal> findGoalWithOwner(@Param("goalId") Long goalId);

끝!!

0개의 댓글