본문 바로가기

Dot Programming/JPA

[JPA] 페치 조인(Fetch Join) - 대상 ToOne, ToMany 관계를 한 번에 조회하기

    페치 조인(Fetch Join) ToOne, ToMany 관계를 조인하는 경우

    페치 조인은 SQL에서 다루는 조인의 종류는 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 이것은 연관된 엔티티나 컬렉션을 한 번에 조회해주는 기능을 가지고 있다.

    A "fetch" join allows associations or collections of values to be initialized along with their parent objects using a single select.

    페치 조인을 사용하면 1개의 select문을 사용하여 주인(부모) 객체와 함께 연관된 엔티티나 컬렉션의 값을 초기화 시킬 수 있다.


    따라서 애플리케이션에서 페치 조인 결과는 연관된 모든 엔티티가 있을 것이라 가정하고 사용해야 한다. 만약 페치 조인에 별칭을 잘못 명시해서 컬렉션 결과를 필터링하게 된다면 객체의 상태와 DB의 상태가 일치하지 않아 일관성이 깨지게 되는 것을 주의해야 한다.

    xToOne관계 (OneToOne, ManyToOne)

    xToOne관계는 fetch join이 이뤄지는 데 큰 문제가 없다. 다음과 같이 Member와 Team이 다대일 관계인 상황에서 Member 컬렉션을 조회하면서 연관된 Team 엔티티도 같이 조회해보자.

    ManyToOne


    JPQL에서 다음과 같이 쿼리를 날리면 SQL문은 그 아래와 같이 생성이 된다.

    // JPQL
    List<Member> result = em.createQuery("select m from Member m "
    			+ "join fetch m.team t", Member.class)
    			.getResultList();
                
    // SQL
    select
        member0_.member_id as member_i1_0_0_,
        team1_.team_id as team_id1_1_1_,
        member0_.name as name2_0_0_,
        member0_.team_id as team_id3_0_0_,
        team1_.name as name2_1_1_ 
    from
        member member0_ 
    inner join
        team team1_ 
            on member0_.team_id=team1_.team_id


    조회되는 테이블은 다음과 같다. 모든 컬렉션(members)과 엔티티(team)가 조회되었음을 볼 수 있다. 실제로 List<Member>를 출력해도 결과는 일치하므로 일관성이 지켜진다.

    ManyToOne 페치조인 결과


    OneToOne관계를 가지는 두 엔티티의 페치조인 결과는 그냥 그대로 1대1로 매칭이 되는 간단한 조인이므로 생략한다.

    xToMany관계 (OneToMany, ManyToMany)

    그러나 xToMany관계와는 fetch join은 지양되어야 한다. 왜냐하면 조인이 이뤄지면서 DB 데이터가 뻥튀기가 되기 때문이다. 이번에는 입장을 바꿔서 Team과 Member가 일대다 관계인 상황에서 Team을 조회하면서 Member도 같이 조회해보자.

    oneToMany


    JPQL에서 다음과 같이 쿼리를 날리면 SQL문은 그 아래와 같이 생성이 된다. 위의 Member 테이블을 조회했을 때하고 위치만 바뀌었지 별 차이가 없다.

    // JPQL
    List<Team> results = em.createQuery("select t from Team t join fetch t.members m", Team.class)
    			.getResultList();
    
    // SQL
    select
        team0_.team_id as team_id1_1_0_,
        members1_.member_id as member_i1_0_1_,
        team0_.name as name2_1_0_,
        members1_.name as name2_0_1_,
        members1_.team_id as team_id3_0_1_,
        members1_.team_id as team_id3_0_0__,
        members1_.member_id as member_i1_0_0__ 
    from
        team team0_ 
    inner join
        member members1_ 
            on team0_.team_id=members1_.team_id


    그러나 결과를 보면 Team을 조회하면서 연관된 Member 컬렉션을 페치 조인을 하게되면 다음과 같이 엔티티 중복 데이터가 발생하게 된다.


    Team은 원래 컬렉션이 아니라 일의 관계를 가지는 엔티티였기 때문에 중복된 데이터가 발생하면 안된다. 다음과 같이 List<Team>에 담겨진 데이터를 출력해보면 왜 그런지 알 수 있다.

    • List Team을 순차대로 조회 후, 각 Team 속하는 Member들(team.getMembers())을 출력해봤다.

     

    Team 출력 결과

     

    distinct 사용

    물론 이에 대한 해결책은 존재한다. JPQL에 distinct를 사용해서 중복된 데이터를 제거할 수 있다.

    // JPQL
    List<Team> results = em.createQuery("select distinct t from Team t join fetch t.members m", Team.class)
    			.getResultList();
    
    // SQL
    select
        distinct team0_.team_id as team_id1_1_0_,
        members1_.member_id as member_i1_0_1_,
        team0_.name as name2_1_0_,
        members1_.name as name2_0_1_,
        members1_.team_id as team_id3_0_1_,
        members1_.wallet_id as wallet_i4_0_1_,
        members1_.team_id as team_id3_0_0__,
        members1_.member_id as member_i1_0_0__ 
    from
        team team0_ 
    inner join
        member members1_ 
            on team0_.team_id=members1_.team_id


    다시 출력해보면 중복된 데이터들은 삭제된 것을 볼 수 있다. 이대로 끝나면 좋겠지만 이는 사실 그리 올바른 방법이 아니다. 왜냐하면 DB의 데이터와 일관성이 깨지기 때문이다.

    • List Team을 순차대로 조회 후, 각 Team 속하는 Member들(team.getMembers())을 출력해봤다.

     

    distinct 적용


    Distinct를 적용한 DB 조회 결과는 어떻게 달라졌을까? 발생한 쿼리문을 조회해보면 다음과 같이 SQL distinct의 중복 제거는 효과가 없음을 볼 수 있다. 왜냐하면 DB에서는 사실상 중복된 데이터가 없어서 오직 team 데이터만의 중복은 고려하지 않기 때문이다. 이렇게 되면 단점이 데이터의 일관성이 깨져서 페이징 처리가 불가능해진다. List<Team>의 2번째 데이터가 "teamB"이면 좋겠지만 DB에서는 중복된 데이터이기 때문에 "teamA"가 나올 수 밖에 없기 때문이다.

     

    데이터 일관성 깨짐


    따라서 xToMany는 fetch join은 지양되어야 하고 만약 정말 필요하다면 최대 1개만 사용하는 것이 적절하다. 그 이상으로 사용하면 데이터가 얼마나 뻥튀기될지 감당하기 힘들어진다. 페치조인에 관해서 더 공부하다보면 조회 API 성능 최적화 부분과 연결이 되는 데 조회 성능 최적화를 이루기 위해 조회 대상 데이터들을 한번에 가져오거나 DTO로 변환하여 부분부분 가져올 수 있다. (ManyToMany도 컬렉션을 조회한다는 부분은 일치하기 때문에 따로 다루진 않았다.)


    참고
    자바 ORM 표준 JPA 프로그래밍