JPA 객체지향 쿼리 언어(JPQL) - 패치 조인(한계)

2022. 7. 23. 11:58·공부/JPA
반응형

출처 : 자바 ORM 표준 JPA 프로그래밍 인프런 강의

 

JPA 게시글은 대부분 인프런의 김영한님의 강의인 '자바 ORM 표준 JPA 프로그래밍' 기반으로 내용을 정리했습니다.

 

패치 조인(한계)

패치 조인의 특징과 한계

  • 패치 조인 대상에는 별칭을 줄 수 없다.
    • 하이버네이트는 가능하지만 가급적 사용하지 않는 것이 좋다.
    • 패치 조인은 기본적으로 엔티티와 연관 된 데이터를 모두 가져온다. 그 중에 where절을 이용해 몇 개만 가져오고 싶을 때는 그냥 따로 조회하는 것이 좋다. 
      예를들면 1개의 팀과 연관된 회원이 5명이라고 치고, 그 중에 한 명만 불러오는 경우엔 잘못 조작하게 되면 나머지 4명이 누락되어 이상하게 동작할 수 있는 확률이 있다. 그리고 기본적으로 객체 그래프라는 것은 모든 데이터를 조회할 수 있어야하는게 정상이다. 그런데 일정 데이터만 걸러서 조회하는 경우에는 모든 데이터를 가져올 수 없다. 그래서 결론은 쿼리를 따로 분리하는게 맞다. member에 관한 SELECT문을 따로 보내 멤버 다섯 명을 가져오고 나서 한 명을 거르던가 해야한다.
  • 둘 이상의 컬렉션은 패치할 수 없다.
  • 컬렉션을 패치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
    • 일대일, 다대일 같은 단일 값 연관 필드들은 패치 조인해도 페이징이 가능하다.
      • 데이터 뻥튀기가 안 되기 때문!
    • 하이버네이트는 경고 로그를  남기고 메모리에서 페이징(매우 위험)
    • 페이징 예제
String query = "select t From Team t join fetch t.members m";
List<Team> result= em.createQuery(query, Team.class)
        .setFirstResult(0)
        .setMaxResults(1)
        .getResultList();
// 출력 로그 중 일부
WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!

Hibernate: 
    /* select
        t 
    From
        Team t 
    join
        fetch t.members m */ select
            team0_.id as id1_3_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_3_0_,
            members1_.age as age2_0_1_,
            members1_.TEAM_ID as TEAM_ID5_0_1_,
            members1_.type as type3_0_1_,
            members1_.username as username4_0_1_,
            members1_.TEAM_ID as TEAM_ID5_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID

그리고 쿼리를 보면 페이지 쿼리가 없는 걸 볼 수 있다. DB에서 Team에 대한 데이터를 다 끌고왔기 때문이다. 만약 데이터가 100만건이면 100만건을 다 가져와서 메모리에 올린 후 페이징을 하는 것이다. 

이런 경우 해결 방법은 그냥 쿼리를 뒤집어버리는 방법이 있다.

String query = "select m From Member m join fetch m.team t";

이렇게 뒤집으면 연관 관계가 다대일이이 되므로 페이징에 문제가 생기지 않는다.

  • BatchSize

패치 조인을 사용하면 N + 1 문제가 해결되지만 컬렉션 패치 조인에서는 해결이 안 된다. 그럴땐 BatchSize() 애노테이션을  사용하면 된다.

String query = "select t From Team t";
List<Team> result= em.createQuery(query, Team.class)
        .setFirstResult(0)
        .setMaxResults(2)
        .getResultList();

System.out.println("result = " + result.size());


    for (Team team : result) {
        System.out.println("team = " + team.getName() + "| members= "
         + team.getMembers().size());
        for(Member findMember : team.getMembers()){
            System.out.println("-> member = " + findMember);
        }

이렇게 쿼리를 날려보면 N + 1 문제로 쿼리가 총 세 번 나가게 된다.

Hibernate: 
    /* select
        t 
    From
        Team t */ select
            team0_.id as id1_3_,
            team0_.name as name2_3_ 
        from
            Team team0_ limit ?
result = 2
Hibernate: 
    select
        members0_.TEAM_ID as TEAM_ID5_0_0_,
        members0_.id as id1_0_0_,
        members0_.id as id1_0_1_,
        members0_.age as age2_0_1_,
        members0_.TEAM_ID as TEAM_ID5_0_1_,
        members0_.type as type3_0_1_,
        members0_.username as username4_0_1_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID=?
team = 팀A| members= 2
-> member = Member{id=3, username='회원1', age=0}
-> member = Member{id=4, username='회원2', age=0}
Hibernate: 
    select
        members0_.TEAM_ID as TEAM_ID5_0_0_,
        members0_.id as id1_0_0_,
        members0_.id as id1_0_1_,
        members0_.age as age2_0_1_,
        members0_.TEAM_ID as TEAM_ID5_0_1_,
        members0_.type as type3_0_1_,
        members0_.username as username4_0_1_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID=?
team = 팀B| members= 1

이 문제를 간단하게 해결할 수 있는 방법은 Team에서 연관 관계가 매핑되어 있는 필드에 @BatchSize() 애노테이션을 사용하면 된다. BatchSize의 값은 1000이하의 숫자여야한다.

@BatchSize(size = 100)
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();

실행해보자.

Hibernate: 
    /* select
        t 
    From
        Team t */ select
            team0_.id as id1_3_,
            team0_.name as name2_3_ 
        from
            Team team0_ limit ?
result = 2
Hibernate: 
    /* load one-to-many jpql.Team.members */ select
        members0_.TEAM_ID as TEAM_ID5_0_1_,
        members0_.id as id1_0_1_,
        members0_.id as id1_0_0_,
        members0_.age as age2_0_0_,
        members0_.TEAM_ID as TEAM_ID5_0_0_,
        members0_.type as type3_0_0_,
        members0_.username as username4_0_0_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID in (
            ?, ?
        )
team = 팀A| members= 2
-> member = Member{id=3, username='회원1', age=0}
-> member = Member{id=4, username='회원2', age=0}
team = 팀B| members= 1
-> member = Member{id=5, username='회원3', age=0}

N+1  문제가 해결된 걸 볼 수 있다. 참고로 BatchSize()는 각 필드에 직접 써주는 방법도 있지만 글로벌하게 설정할 수도 있다. 먼저 Team 클래스에서 추가했던 BatchSize()를 주석한 후 persistence.xml에 아래 코드를 추가해준다.

<property name="hibernate.default_batch_fetch_size" value="100"/>
  • 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
  • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선
    • @OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩이다.
  • 최적화가 필요한 곳은 패치 조인을 적용한다.

 

 

패치 조인 - 정리

  • 모든 것을 패치 조인으로 해결할 순 없다.
  • 패치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 패치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
반응형
저작자표시 비영리 변경금지 (새창열림)
'공부/JPA' 카테고리의 다른 글
  • JPA 객체지향 쿼리 언어(JPQL) - 엔티티 직접 사용
  • JPA 객체지향 쿼리 언어(JPQL) - 다형성 쿼리
  • JPA 객체지향 쿼리 언어(JPQL) - 패치 조인(기본)
  • JPA 객체지향 쿼리 언어(JPQL) - 경로 표현식
데부한
데부한
어차피 할 거면 긍정적으로 하고 싶은 개발자
    반응형
  • 데부한
    동동이개발바닥
    데부한
  • 전체
    오늘
    어제
    • 분류 전체보기 (307)
      • 방통대 컴퓨터과학과 (27)
        • 잡담 (9)
        • 3학년1학기 (17)
      • 프로젝트 및 컨퍼런스 회고 (1)
        • 프로젝트 (4)
        • 한이음 프로젝트 (0)
        • 회고 (3)
      • 공부 (165)
        • Spring (37)
        • JPA (71)
        • 인프런 워밍업 클럽_BE (10)
        • Java (6)
        • React.js (27)
        • 넥사크로 (11)
        • 기타 (3)
      • 알고리즘 (85)
        • 알고리즘 유형 (10)
        • 알고리즘 풀이 (57)
        • SQL 풀이 (18)
      • 에러 해결 (13)
      • 잡담 (7)
        • 국비교육 (2)
        • 구매후기 (5)
        • 진짜 잡담 (0)
  • 블로그 메뉴

    • Github
    • Linkedin
    • 홈
    • 방명록
    • 글쓰기
    • 관리
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    JPA
    백준
    Spring
    운영체제
    프론트엔드
    코딩테스트
    알고리즘
    방통대
    에러해결
    프로그래머스
    MSA
    기출문제
    넥사크로
    RESTful
    Java
    egov
    인프런
    react
    SpringBoot를 이용한 RESTful Web Service 개발
    springboot
    oracle
    SQL
    전자정부프레임워크
    IT
    자바스크립트
    토이프로젝트
    QueryDSL
    개발자
    토비의스프링부트
    스프링부트
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
데부한
JPA 객체지향 쿼리 언어(JPQL) - 패치 조인(한계)
상단으로

티스토리툴바