Per ardua ad astra !
I'm On My Way
Per ardua ad astra !
전체 방문자
오늘
어제
  • 분류 전체보기 (126)
    • Algorithm (50)
      • 백준 (30)
      • SWEA (3)
      • JUNGOL (3)
      • Programmers (5)
      • LeetCode (2)
    • 안드로이드 개발 (6)
      • Java로 개발 (0)
      • Kotlin으로 개발 (3)
    • Spring (41)
      • Spring기본 (17)
      • JPA기본 (15)
      • JPA활용 SpringBoot 기본 (9)
      • API 개발 기본 (0)
    • 네트워크 (3)
    • 운영체제 (0)
    • Life (3)
      • 책 (0)
      • 자기계발 (1)
      • 일상 (2)
    • 노마드코더 (3)
      • python으로 웹 스크래퍼 만들기 (3)
    • 프로그래밍 언어 (17)
      • Java 기본 (2)
      • 코틀린 기본 (15)

블로그 메뉴

  • 홈
  • 방명록

인기 글

hELLO · Designed By 정상우.
Per ardua ad astra !

I'm On My Way

Spring/JPA기본

JPQL의 페치 조인 (Fetch Join)

2021. 8. 4. 13:11

페치 조인(Fetch join)


특징

• SQL의 조인 종류가 아니다. 
• JPQL에서 성능 최적화를 위해 제공하는 기능
  => 관련 객체 그래프를 SQL 한번에 조회하는 개념이다.
• 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
  => 페지 조인을 사용할 때 연관된 엔티티도 함께 즉시 로딩해서 조회
• join fetch 명령어 사용
  => 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로

 

 

지연로딩

참고: https://ggp03016.tistory.com/68

저번 시간에 지연로딩에 대해 공부했었다. 지연로딩은 하나의 엔티티를 조회할 때 연관된 객체 그래프의 엔티티까지 즉시 로딩하지 않고, 프록시 객체로 두어 그 연관된 객체의 속성값을 호출하기 전까지 로딩시키지 않는 것을 의미한다. 원치 않는 객체까지 접근하여 일일히 조회 쿼리를 날리는 불편함을 없애기위해 가능하면 지연로딩을 설정하는 것이 좋다고 했었다.

예시로 Member에서의 Team 속성은 @ManyToOne(fetch = FetchType.LAZY) 로 설정하여 team객체의 속성값을 호출하지 않는이상 team의 데이터에 접근하지 않도록 설정했다. 이 상태에서 각 맴버의 팀 객체에 접근하도록 해보자.

 

 

예시코드

 

데이터 삽입

            Team team1 = new Team();
            team1.setName("팀1");

            Team team2 = new Team();
            team2.setName("팀2");

            em.persist(team1);
            em.persist(team2);

            Member member1 = new Member();
            member1.setAge(10);
            member1.setUsername("회원1");
            member1.setTeam(team1);
            em.persist(member1);

            Member member2 = new Member();
            member2.setAge(10);
            member2.setUsername("회원2");
            member2.setTeam(team1);
            em.persist(member2);

            Member member3 = new Member();
            member3.setAge(10);
            member3.setUsername("회원3");
            member3.setTeam(team2);
            em.persist(member3);

            em.flush();
            em.clear();

팀1: 회원1, 회원2

팀2: 회원3

 

멤버의 팀 엔티티 조회

            List<Member> resultList = em.createQuery("select m from Member m", Member.class)
                                        .getResultList();
            for (Member member : resultList) {
                System.out.println("username = " + member.getUsername() + " team.name = " + member.getTeam().getName());
            }

member.getTeam().getName() -> team 객체의 속성 name에 접근할 때, 팀이 2팀이므로 쿼리는 2개가 나간다. 아래 쿼리 예시가 있다. 

 

처음 멤버 조회시 한 번, 팀1조회, 팀2 조회시 각각 1번씩 쿼리가 나간모습

회원들을 조회할 때 1번 쿼리가 나간다. 

회원1의 team의 name을 조회할 때 쿼리가 1번 나가고, 이후 회원2의 team을 조회할 때는 이미 해당 팀이 1차 캐시에 있기 때문에 영속성 컨텍스트의 1차 캐시에서 값을 가져와서 출력한다. 회원3의 team은 또 처음 조회되기 때문에 쿼리를 1번 날려서 팀 데이터를 가져온다. 
그래서 총 3번의 질의가 날라가는 것을 확인할 수 있다. 이를 통해 우리는 fetch join을 통해 한꺼번에 조회할 수 있는 편의성을 느낄 수 있다. (팀 수만큼 날아가는 질의가 더 많을 것이기 때문에)

 

 


페치 조인의 사용

 

엔티티 페치 조인


• 회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에) 
• SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT
• [JPQL]
  select m from Member m join fetch m.team 
• [SQL]
  SELECT M.*, T.* FROM MEMBER M
  INNER JOIN TEAM T ON M.TEAM_ID=T.ID

 

 

다대일 관계 (Member의 Team조회)

 

코드

            List<Member> resultList = em.createQuery("select m from Member m fetch join m.team", Member.class)
                    .getResultList();
            for (Member member : resultList) {
                System.out.println("username = " + member.getUsername() + " team.name = " + member.getTeam().getName());
            }

select m from Member m join fetch m.team 

 

출력 결과

쿼리가 한번만 나간다. 

정말 신기하게도 Team 엔티티들이 모두 한쿼리로 조회가 완료된다. 

 

다른 경우도 비교해보자 !!

 

1. 일반 Join의 쿼리, (fetch = FetchType.LAZY) 설정하고 처음부터 일반 join을 날린경우

 

코드

            List<Member> resultList = em.createQuery("select m from Member m join m.team", Member.class)
                                        .getResultList();
            for (Member member : resultList) {
                System.out.println("username = " + member.getUsername() + " team.name = " + member.getTeam().getName());
            }

출력결과

쿼리에 join이 함께 날아간 모습이고, FetchType=Lazy이기 때문에 팀객체의 속성에 직접 접근할 때마다 쿼리를 한번씩 더날려서 로딩한 모습이다. 

 

2. 일반 Join의 쿼리, 지연 로딩 설정 x 

출력결과 즉시 로딩을 진행하여 쿼리가 모두 날라가고 팀이 모두 로딩이 된 후, 팀의 정보를 빼내올 때 1차캐시에 접근하여 이미 로딩된 팀정보를 가져오기만 하면된다. 순서가 그렇다. 

하지만, 의문점인 것은 fetch join을 하게되면 쿼리 한번에 모든 팀객체 정보까지 접근할 수 있었는데, 현재 즉시로딩으로 했는데도 쿼리가 3개가 나가는가? 이다. 정확한 이유는 모르겠지만 fetch join이 관련 객체 그래프 조회에서 그만큼 효율적인 조회 방법이구나를 느낄 수 있었다.

 


일대다 관계 (Team의 Member조회)

이번엔 거꾸로 팀에서 맴버들의 정보에 접근해보자 !!

일대다 관계는 join으로 조회시 데이터가 뻥튀기 되는 현상을 꼭 조심해야 한다 !!

 

SELECT t FROM Team t JOIN FETCH t.members

 

코드

            List<Team> resultList = em.createQuery("SELECT t FROM Team t JOIN FETCH t.members", Team.class)
                                      .getResultList();

            for (Team team : resultList) {
                System.out.print("name = " + team.getName() + " ");
                List<Member> memberList =  team.getMemberList();
                System.out.print("team.memberList: ");
                for (Member member : memberList) {
                    System.out.print("member = " + member.getUsername() + " ");
                }
                System.out.println();
            }

 

출력결과

**팀1의 데이터가 2번 출력되는 것을 확인할 수 있다. **


이는 DB에서 일대다 관계 조인 조회를 할 때 당연한 결과로, 팀 ID가 같더라도 Member ID가 다르기 때문에
조인결과는 팀의 멤버 수 만큼 더 뻥튀기되서 출력될 수 있는 것이다.

 

Member의 id가 달라서 Member의 개수만큼 팀의 결과값이 생성된다.

******일대다 관계 데이터 조인 조회할 때는 데이터가 뻥튀기 되는것을 꼭꼭꼭 주의하자 !! *********

 

Distinct


SQL은 중복된 데이터를 삭제하기 위해 DISTINCT 라는 키워드를 제공한다. 
방금 조인된 결과에 DISTINCT 예약어를 사용해도 (TEAM ID, MEMBERID) 이 키 묶음이 완벽하게 일치하는 데이터가 없기 때문에 DB 상에서는 join결과가 그대로 나온다. 

하지만!! JPQL의 DISTINCT는 다르다. 2가지 기능을 제공한다. 

 

1. SQL의 DISTINCT 기능을 그대로 사용한다.
2. 추가로 에플리케이션에서 조회할 땐 엔티티 중복 제거가 가능하다.

2번을 더 파헤쳐 보자면 
같은 식별자를 가진(즉 같은 TeamID를 가진) 엔티티를 제거하여 중복된 결과가 나오지 않도록 하는 것이다. 
 
코드

em.createQuery("SELECT DISTINCT t FROM Team t JOIN FETCH t.members", Team.class)

 

출력결과




패치 조인의 한계


• 페치 조인 대상에는 별칭을 줄 수 없다. 
ex) select t from Team t join fetch t.memberList as m;
     => m으로 뭔가를 하는건 불가능하다.

• 하이버네이트는 가능, 가급적 사용X 
• 둘 이상의 컬렉션은 페치 조인 할 수 없다. 
• 컬렉션을 페치 조인하면 페이징 API(setFirstResult, 
setMaxResults)를 사용할 수 없다. 
• 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
• 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)


페치 조인 정리


• 모든 것을 페치 조인으로 해결할 수 는 없음
• 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적
• 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른
결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요
한 데이터들만 조회해서 DTO로 반환하는 것이 효과적

 

 

 

 

이 게시물은 '자바 ORM 표준 JPA 프로그래밍' 강의를 수강하고 정리한 내용임을 밝힙니다. 
출처: https://www.inflearn.com/course/ORM-JPA-Basic#

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 본 강의는 자바 백엔

www.inflearn.com

 

    'Spring/JPA기본' 카테고리의 다른 글
    • JPQL의 다형성 쿼리와 네임드 쿼리
    • JPQL의 Update, delete 사용, 벌크연산
    • JPQL 타입과 기타식, CASE 사용 방법, JPQL의 기본함수와 사용자 설정함수
    • JPQL 조인, 서브쿼리
    Per ardua ad astra !
    Per ardua ad astra !
    개발자 지망생이며 열심히 공부하고 기억하기 위한 블로그입니다.

    티스토리툴바