[3] JPA 프로그래밍 (14) - JPQL(객체지향 쿼리 언어) 2 (경로표현식 / fetch join)

김정욱·2021년 3월 11일
0

[3] JPA 프로그래밍

목록 보기
14/15
post-thumbnail

경로 표현식

[ 정의 ]

  • .(점) 을 찍어 객체 그래프탐색하는 것
    ex)
select m.username -> 상태 필드
from Member m
  join m.team t -> 단일 값 연관 필드
  join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A'

[ 용어 정리 ]

  • 상태 필드(state field)
    : 단순히 값을 저장하기 위한 필드
    ex) m.username
  • 연관 필드(association field)
    • 단일 값 연관 필드
      : 대상이 단수의 엔티티(Entity)
        ex) @ManyToOne / @OneToOne
    • 컬렉션 값 연관 필드
      : 대상이 컬렉션(Collection)
        ex) @OneToMany / @ManyToMany

[ 묵시적 조인 / 명시적 조인 ]

  • 묵시적 조인
    : 경로 표현식에 의해 묵시적으로 SQL 조인이 발생되는 것
    (내부조인만 가능 / 외부조인은 불가능)
  • 명시적 조인
    : join 키워드를 직접 사용하는 것

[ 특징 ]

  • 상태필드(state field)
    : 경로 탐색의 끝, 탐색 X
    m.username에서 더이상 탐색할 수 없음 (더 들어갈 게 없음)
  • 단일 값 연관 경로
    : 묵시적 내부 조인 발생 O / 탐색 O
/* 묵시적 내부 조인 X */
select m from Member m
 --> Member내부에 Team이라는 객체 필드가 있지만, 기본적으로 지연로딩(LAZY)이기 때문

/* 묵시적 내부 조인 O */
select m.team from Member m
--> 연관 필드를 직접 프로젝션에 올렸기 때문에 실제 Join된 커리가 나간다(묵시적 내부 조인)
--> 그리고, Member에 있는 또 다른 연관 필드에 접근 가능(탐색 O)
  • 컬렉션 값 연관 경로
    : 묵시적 내부 조인 발생 O / 탐색 X
/* 묵시적 내부 조인 O */
select t.members from Team t
--> 컬렉션 값 연관 필드를 가져올 경우 당연히 묵시적 내부 조인은 생긴다
--> 하지만, 더이상 탐색은 할 수 없음
select t.members.username from Team t --> (X)
--> 원한다면 명시적 조인을 해야함
select m.username from Team t join Member m

[ 정리 ]

  • 묵시적 조인경로 표현식에 의해 생기는데 이는 실무에서 조심해야 함
  • 조인은 명시적으로 일어나야 나중에 sql 튜닝이나 유지보수 하기에 좋음
  • 실무에서는 묵시적 조인이 최대한 일어나지 않게 하자(미사용 권장)
  • 컬렉션 값 연관필드에 대한 조인은 더이상 탐색불가능 하다

fetch join

[ 설명 ]

  • 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회하는 기능
  • SQL 조인 종류 X
  • JPQL에서 성능 최적화를 위해 제공하는 기능
  • join fetch 명령어 사용

[ 엔티티 fetch join ]

[ 설명 ]

  • fetch join 대상이 엔티티(단수)
  • 즉시 로딩 처럼 연관된 객체를 함께 조회해서 영속성 컨텍스트 1차 캐시에 저장됨
  • 하나의 SQL로 가져오는 것이 핵심!
    --> N+1문제해결방안으로 많이 사용
/* JPQL -> 회원을 조회하면서 연관된 팀도 함께 조회 (하나의 SQL로!) */
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
// M.*과 T.*은 모든 컬럼을 생략으로 표시한 것 실제로는 이렇게 나오지 X

[ 예시 ]

  • 현재상황
    : 팀에 소속된 회원들의 정보를 가져오면서, 해당 팀의 정보를 함께 조회하고 싶음
      (member + team)
  • fetch join 미사용 일 경우
String jpql = "select m from Member m join m.team";
List<Member> members = em.createQuery(jpql, Member.class)
                          .getResultList();

for (Member member : members) {
  // 일반 조인을 사용 --> 지연로딩 설정, 즉 이 시점에 반복문이 돌면서 쿼리가 나옴!
  System.out.println("username = " + member.getUsername() + ", " +
  "teamName = " + member.getTeam().name());
}

: 만약 데이터가 3개가 있고 모두 다른 팀이라면, 반복문에서 3번의 쿼리가 추가적으로 더 발생한다 --> N+1 문제 발생

  • fetch join 사용일 경우
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
                          .getResultList();

for (Member member : members) {
  // 페치 조인 사용 --> 추가적인 쿼리가 나가지 않음!
  System.out.println("username = " + member.getUsername() + ", " +
  "teamName = " + member.getTeam().name());
}

: 이미 fetch join으로 한방 쿼리로 조회 했으니 영속성 컨텍스트에 정보들이 존재
--> 추가적인 쿼리가 발생하지 않는다
      (fetch 전략LAZY여도 fetch join이 우선권을 가진다!)

[ 컬렉션 fetch join ]

[ 설명 ]

  • fetch join 대상이 컬렉션(복수)
  • 일대다 관계 or 다대다 관계에서 발생
/* JPQL -> 팀을 조회하면서 소속된 멤버들도 함께 조회 (하나의 SQL로!) */
select t from Team t join fetch t.members

/* 실제 수행되는 SQL */
select M.*,T.* from Team t
  inner join Member m on t.id=m.team_id
// M.*과 T.*은 모든 컬럼을 생략으로 표시한 것 실제로는 이렇게 나오지 X

[ 예시 ]

  • 현재 상황
    : 팀의 정보를 가져오는데 소속된 멤버들의 정보도 함께 원하는 상황
  • 결과
    : 일대다 관계이기 때문에 우리가 받는 teams 결과 리스트에 팀A는 2개가 들어가 있음
       즉, 원하는 정보가 아닌 중복 정보가 나온다!
    (조인을 해서 team에 대한 정보를 가져오는 것이니까 속한 멤버 수만큼 나옴)
/* ===================요청================== */
String jpql = "select t from Team t join fetch t.members where t.name = '팀A'"
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();

for(Team team : teams) {
    System.out.println("teamname = " + team.getName() + ", team = " + team);
    for (Member member : team.getMembers()) {
    //페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
    System.out.println(-> username = " + member.getUsername()+ ", member = " + member);
    }
}

/* ===================결과================== */
  teamname = 팀A, team = Team@0x100
    -> username = 회원1, member = Member@0x200
    -> username = 회원2, member = Member@0x300
  teamname = 팀A, team = Team@0x100
    -> username = 회원1, member = Member@0x200
    -> username = 회원2, member = Member@0x300

: 같은 팀A에 대한 정보가 중복되어 나오는 일이 발생함!
  (조인을 하면 어쩔 수 없음 --> 그래서 중복 제거를 위해 필요한게 DISTINCT)

[ fetch join 과 DISTINCT ]

[ 설명 ]

  • SQLDISTINCT는 중복된 결과를 제거하는 명령
    --> JPQL에서는 객체 중심이라서 중복 엔티티를 제거해줄 수 없음
  • JPQL에서 새롭게 DISTINCT를 제공
    • SQLDISTINCT 기능
    • 애플리케이션에서 엔티티 중복 제거

[ 예시 ]

  • 앞 선 예제에서 엔티티의 중복으로 여러개가 출력되었음
  • DISTINCT 옵션을 넣어서 실행한 결과
/* ===================요청================== */
String jpql = "select DISTINCT t from Team t join fetch t.members where t.name = '팀A'"
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();

for(Team team : teams) {
    System.out.println("teamname = " + team.getName() + ", team = " + team);
    for (Member member : team.getMembers()) {
    //페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
    System.out.println(-> username = " + member.getUsername()+ ", member = " + member);
    }
}

/* ===================결과================== */
  [DISTINCT 추가시 결과]
  teamname = 팀A, team = Team@0x100
    -> username = 회원1, member = Member@0x200
    -> username = 회원2, member = Member@0x300

[ fetch join 과 일반 join ]

  • 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않는다(지연로딩)
[JPQL]
select t
from Team t join t.members m
where t.name = ‘팀A'

[SQL]
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'

: Team 엔티티만 조회하고 연관 객체인 member에 대해 조회하지 X


  • 페치 조인연관된 엔티티를 함께 조회
[JPQL]
select t
from Team t join fetch t.members
where t.name = ‘팀A'

[SQL]
SELECT T.*, M.* // 추가됨
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'

[ fetch join 특징 & 한계 ]

  • 페치 조인 대상에는 별칭을 줄 수 없다
    : 표준으로는 지원 X, Hibernate에서는 가능하긴 한데 권장 X
/* 페치 조인 대상에 별칭을 부여한 경우 */
select t from Team t join fetch t.members m where m.age>10
--> 만약 member의 원래 와야할 data가 5개인데 걸러져서 가져오면 정합성 이슈 때문에 문제 발생 가능성이 생김
--> 매우매우 위험한 것임 
--> 페치조인 대상에는 별칭을 주어 사용하지 말자;
  • 둘 이상의 컬렉션페치 조인 할 수 X
만약 Team에 있는 members에 대한 orders를 조회하는 경우!
team -> member -> order 이렇게 2개의 컬렉션을 타게 된다
1:N + 1:N  관계가 되는데 이는 매우 위험하다!
1:N만 되어도 데이터가 뻥튀기 되는데 '일대다대다' 관계는 가늠하기 힘들고 위험성이 크다
  • 컬렉션을 페치 조인 하면 페이징 API를 사용할 수 없다
    --> 일대일 / 다대일 같은 단일 값 연관 필드들은 패치조인해도 페이징 가능!
일대다 관계에서 조회시 데이터가 뻥튀기 된다고 말했다.
만약 이 상태에서 페이징으로 데이터의 개수 1개를 원한다고 가정하자.
팀A의 결과는 2개인데 그중에 1개만 반환되어 뒤에 잘려버린 member에 대한 정보는 다음페이지로 간다
즉, 올바른 페이징 로직이 되지 않아서 위험함

이를 해결하기 위한 방안 3가지
1) 다대일 관계로 쿼리를 짜면 페이징 가능
2) @Batchsize 사용
3) DTO 사용
// 2,3번은 필요하다면 나중에 더찾아보자!

[ 정리 ]

  • 페치 조인객체 그래프유지할 때 사용하면 효과적
  • 일반조인과 달리 하나의 SQL로 연관된 객체의 정보를 조회한다
    --> 대부분의 N+1 문제를 해결할 수 있음
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야하면,
    페치 조인 보다는 일반조인을 사용하고, 필요한 데이터만 조회DTO로 받는게 효과적
    --> 최적화에 관한 이야기
profile
Developer & PhotoGrapher

0개의 댓글