본문 바로가기
JAVA/JPA

[JPA] JPQL 페치조인(fetch join)

by 개미가되고싶은사람 2024. 11. 28.

목차

    페치조인(fetch join)

    Fetch Join은 JPQL에서 성능 최적화를 위해 제공되는 조인 방식으로, 연관된 엔티티나 컬렉션을 SQL 한 번의 쿼리로 조회할 수 있는 기능입니다. 이를 통해 N+1 문제를 방지하고 필요한 데이터를 효율적으로 가져올 수 있습니다. Fetch Join을 사용하면 쿼리에서 지정한 엔티티와 함께 연관된 엔티티를 즉시 로드할 수 있습니다. Fetch Join을 사용하면 지연 로딩(LAZY)으로 설정된 연관 엔티티를 특정 쿼리에서 즉시 로딩(EAGER)할 수 있어 필요한 경우에만 연관된 엔티티를 한 번의 쿼리로 가져올 수 있습니다. 이는 아래 쿼리문 JOIN FETCH 구문을 통해 구현됩니다. 

     

    select m from Member m INNER/LEFT/RIGHT join fetch m.team

     

     

    페치 조인을 사용하는 이유

    N+1 문제를 해결하기 위해 지연 로딩 전략을 사용해도 N+1 문제가 발생합니다. 아래 사진처럼 데이터를 넣어보겠습니다.

     

    Member 엔티티는 Team 엔티티와 1대N 양방향 매핑으로 연관되어 있으며, Team은 지연 로딩으로 설정되어 있습니다.

    Team team1 = new Team();
    team1.setName("팀A");
    em.persist(team1);
    
    Team team2 = new Team();
    team2.setName("팀B");
    em.persist(team2);
    
    Member member1 = new Member();
    member1.setUsername("회원1");
    member1.setTeam(team1);
    em.persist(member1);
    
    Member member2 = new Member();
    member2.setUsername("회원2");
    member2.setTeam(team1);
    em.persist(member2);
    
    Member member3 = new Member();
    member3.setUsername("회원3");
    member3.setTeam(team2);
    em.persist(member3);
    
    em.flush();
    em.clear();
    
    String query = "select m from Member m";
    
    List<Member> members = em.createQuery(query, Member.class)
    	.getResultList();
    
    for (Member member : members) {
    	System.out.println("member = " + member); // Member 엔티티에 Team은 지연로딩으로 설정되어 있어 MEMBER 테이블에 대한 쿼리만 발생
    
    	System.out.println("memberInfo =  " + member + ", teamName = " + member.getTeam().getName());
    }

    System.out.println("member = " + member) 코드가 실행되면 member 객체의 정보만 출력되고 Team에 대한 정보는 필요하지 않기 때문에 오직 Member 테이블에 대한 쿼리만 실행됩니다.

     

    System.out.println("memberInfo =  " + member + ", teamName = " + member.getTeam().getName()) 코드를 실행할 때 Team 객체의 정보도 필요하기 때문에 Team에 접근하게 됩니다. 이 과정에서 각 Member 데이터마다 해당하는 Team 정보를 얻기 위한 쿼리가 전송됩니다.

     

     

    첫 번째 루프

    지연 로딩을 이용한 JPA 입장에서는 일단 Member 정보만 가져옵니다.

    id username team_id
    1 회원1 ?
    2 회원2 ?
    3 회원3 ?

     

     

    이제 JPA에서는 회원1의 team_id 데이터를 가져오기 위해 Team에 대한 쿼리를 전송합니다.

    id username team_id
    1 회원1 1
    2 회원2 ?
    3 회원3 ?

     

     

    두 번째 루프

    이제 회원2에 대한 team_id 데이터를 가져와야 하는데, 영속성 컨텍스트에 있는 1차 캐시에 값이 동일하므로 Team에 대한 쿼리는 발생하지 않고 두 번째 루프를 종료합니다.

    memberInfo =  Member{id=2, username='회원2', age=2}, teamName = 팀A
    id username team_id
    1 회원1 1
    2 회원2 1
    3 회원3 ?

     

     

    세 번째 루프

    마지막으로 회원3에 대한 team_id 데이터를 가져와야 하는데, 1차 캐시에 해당 값이 없으므로 Team에 대한 쿼리가 발생합니다.

    id username team_id
    1 회원1 1
    2 회원2 1
    3 회원3 2

    이와 같이 지연 로딩을 사용한 Member 엔티티에서 Team 엔티티 정보에 접근할 때마다 쿼리가 발생하여 N+1 쿼리 문제가 발생하게 됩니다.

     

    Join, Fetch Join 차이점

    일반 Join

    • 일반 Join은 연관 Entity에 Join을 걸어도 실제 쿼리에서 SELECT하는 Entity는 JPQL에서 조회하는 주체가 되는 Entity만 조회하여 영속화합니다.
    • 즉, 조회의 주체가 되는 Entity만 SELECT하므로, 연관 Entity는 검색 조건에 필요할 때 사용됩니다. 이 경우, 연관 Entity의 데이터는 실제로 필요하지 않을 수 있습니다.

    Fetch Join

    • Fetch Join은 조회의 주체가 되는 Entity와 함께 Fetch Join이 걸린 연관 Entity도 SELECT하여 모두 영속화합니다.
    • 이 방식은 FetchType이 Lazy인 Entity를 참조하더라도 이미 영속성 컨텍스트에 들어있기 때문에 추가 쿼리가 실행되지 않아 N+1 문제를 해결합니다. 즉, 필요한 모든 데이터를 한 번의 쿼리로 가져올 수 있습니다.

    distinct

    페치 조인에 대한 설명에서, 컬렉션 페치 조인을 사용하면 예상과 다른 결과가 나올 수 있다는 점이 중요합니다. 

    예를 들어, member에 대한 team 개수가 2개일 것이라고 예상했지만, 실제 결과는 더 많은 레코드가 반환됩니다.이는 조인된 결과가 연관된 team 데이터와 함께 조회되기 때문에 발생하는 현상입니다. 일대다 조인에서는 결과가 증가할 수 있지만, 일대일이나 다대일 조인에서는 결과가 증가하지 않습니다.

     

    DISTINCT는 SQL에서 중복을 제거하는 명령어로, JPQL은 객체 지향 쿼리 언어로, SQL 쿼리로 변환되어 실행되며, DISTINCT 키워드를 사용하면 쿼리 결과에서 중복된 값이 제거됩니다. 예를 들어, 특정 고객의 이름을 조회할 때 여러 주문이 동일한 고객에게 연결될 수 있는데, 이 경우 DISTINCT를 사용하면 중복된 고객 이름이 하나로 통합되어 반환됩니다. 

     

    하이버네이트 6.x에서는 DISTINCT 키워드를 사용하지 않아도 자동으로 fetch join 쿼리에 적용되도록 변경되었습니다. 이로 인해 HINT_PASS_DISTINCT_THROUGH 옵션이 완전히 제거되었습니다. 즉, 이제는 DISTINCT를 명시적으로 사용하지 않아도 중복된 결과가 자동으로 처리됩니다. 해당 내용의 자료는 아래 공식문서를 참고하시면 있습니다.

     

     

     

     

     

    참고자료

    https://woo-chang.tistory.com/38

     

    [JPA] 페치 조인(fetch join)이란?

    SQL을 사용할 때 연관관계가 있는 다른 테이블의 정보를 이용하려고 한다면 JOIN을 사용해야 합니다. INNER JOIN, OUTER JOIN과 같은 JOIN 형태가 존재합니다. 그렇다면 FETCH JOIN은 무엇을 의미할까요? Fetch

    woo-chang.tistory.com

     

    https://medium.com/sjk5766/fetch-join-%ED%8A%B9%EC%A7%95-%EB%B0%8F-%EB%8B%A8%EC%A0%90-75095d1ede21

     

    fetch join 특징 및 단점

    이전 포스팅인 JPA N+1 문제에서 fetch join의 단점을 언급했지만, 샛길로 빠지는 것 같아 언급만 하고 상세한 내용을 작성하지 않았다. 이번 포스팅에서는 join과 fetch join을 비교해서 특징을 알아보

    medium.com

    https://hstory0208.tistory.com/entry/JPA-JPQL%EC%9D%98-fetch-join%ED%8E%98%EC%B9%98-%EC%A1%B0%EC%9D%B8%EC%9D%B4%EB%9E%80

     

    [JPA] JPQL의 fetch join(패치 조인)이란?

    Fetch Join JPQL에서 성능 최적화를 위해 제공하는 기능으로 연관된 엔티티나 컬렉션들을 한번의 SQL 쿼리로 함께 조회할 수 있다. 연관된 엔티티에 대해 추가적인 쿼리를 실행할 필요 없이 효율적인

    hstory0208.tistory.com

    https://github.com/hibernate/hibernate-orm/blob/6.0/migration-guide.adoc#distinct

     

    hibernate-orm/migration-guide.adoc at 6.0 · hibernate/hibernate-orm

    Hibernate's core Object/Relational Mapping functionality - hibernate/hibernate-orm

    github.com