JPA : 일대다 Join (Collection Fetch Join) + Distinct

     


    일대다 Join (Collection Join)

    일대다 연관관계를 가지는 엔티티들이 있다고 가정해보자. 이 때, 'One'을 기준으로 "Many"를 Join하는 경우를 생각해보자. One은 말 그대로 하나다. Many는 말 그대로 여러개다. 따라서 One + Many를 Join할 경우, 각 엔티티의 Row 갯수가 맞지 않는다. DB는 이 때, Many의 Row 수를 기준으로 One의 Row를 맞춘다. 

     

    List<Member> findMember = queryFactory.selectFrom(member)
            .join(member.orderList, order)
            .fetch();

    예를 들어 위처럼 일대다 쿼리를 보낸다고 해보자. 그렇다면 DB 단의 테이블은 어떻게 만들어질까? 

     

    DB 테이블은 member와 order를 OrderTable의 FK 값으로 Join을 한다. 따라서 OrderTable에 member FK를 기준으로 붙기 때문에 오른쪽 테이블이 나오게 된다. 이후, MemberTable의 값을 불러온다. 이 때, MemberTable을 Select하면 Member는 총 2개가 불러와진다. 그런데 select 절에는 Member만 들어가있으니, Member Table의 값만 온전히 불러와진다. 동일한 PK, 동일한 이름을 가진 엔티티가 2개 불러와지게 되는 것이다.

    일대다 조인(Collection Join)은 항상 Row가 뻥튀기 되어 중복된 값이 생길 수 있다는 점을 고려해야한다. 

     


    Collection Join → Fetch Join  / Join의 차이는? 

    쉽게, OrderTable에 MemberTable을 붙여서 MemberTable을 가져온다고 생각하자.&nbsp;

    일반 Join과 Fetch Join은 모두 위의 상황에서 Member를 2개 부른다. 중복된 값을 부르는 것은 똑같고, 불러온 배열의 크기를 찍어보면 일반 Join, Fetch Join 모두 2가 나온다. 

    차이점은 불러오는 엔티티 테이블의 차이다. Fetch Join은 Member와 Order를 Fetch Join한다. 따라서 OrderTable까지 함께 불러와져서 각 Member 객체에 Order가 들어가있게 된다. 따라서 Fetch Join으로 불러와진 Order를 조회하면, Order는 바로 로딩이 된다.

    반면 일반 Join은 Member Table만 불러온다. 따라서 Order를 조회하게 되면, Member Id가 같은 Order를 모두 불러와서 OrderList로 만들어주는 Select 쿼리가 한번 더 나가게 된다. 

    결론은 일대다 Join(Collection Join)에서 Fetch Join / 일반 Join은 DB 단에서는 동일하게 '다'를 기준으로 Row를 맞춘 Table이 맞춰진다. 대신 Fetch Join은 연관 엔티티가 모두 불러와지지만, 일반 Join은 Select 절에 있는 엔티티만 불러와진다. 

     


    Distinct

    • DB : Join한 테이블에서 모든 값이 같은 Row는 중복 제거
    • Application : 불러온 엔티티들 중 PK값이 같은 엔티티는 중복 제거 

    JPA에서 사용하는 Distinct는 DB단, 어플리케이션 단에서 각각 다음과 같이 작용한다.

    위의 상황에서 살펴보자

    1. DB : Join한 Table에서 완전 똑같은 Row가 없음 → 중복 제거 X
    2. Apllication : member_id는 1로 동일함. → 중복 제거 O

    각 쿼리를 작성해서 정말로 distinct하면 PK가 같은 값이 제거되는지를 확인해본다. 

    List<Member> findMember = queryFactory.selectFrom(member).distinct()
            .join(member.orderList, order)
            .fetch();
    
    List<Member> findMember2 = queryFactory.selectFrom(member).distinct()
            .join(member.orderList, order).fetchJoin()
            .fetch();
    
    
    log.info("findMember Size = {}", findMember.size());
    log.info("findMember2 Size = {}", findMember2.size());

    위 코드를 실행해보자.

    위 코드를 실행하면 다음과 같이 PK값을 기준으로 중복을 날리고, 하나만 찾아지는 것을 확인할 수 있다. 이 때 일반 Join, Fetch Join 모두 상관없이 Distinct가 없어지는 것을 알 수 있다. 

     


    결론 

    1. 일대다 조인(Collection Join)을 하게 되면, '다'의 Row에 맞춰서 '일'의 엔티티가 뻥튀기 된다. 
    2. 1번 때문에 'One' 엔티티의 중복값이 발생할 수 있다.
    3. Distinct를 이용한 엔티티의 중복값을 제거할 수 있다. Distinct는 DB 단에서는 중복 엔티티를 삭제하고, Application에서는 동일 PK를 가지는 엔티티의 중복제거를 해준다.

     

     

    테스트 코드

    https://github.com/chickenchickenlove/JpaSelfStudy/blob/15e8751ea48a5162073c21e6e5410a9cb920d3b1/studyJpa/test/java/selfjpa/studyjpa/repository/CollectionJoinTest.java

    댓글

    Designed by JB FACTORY