객체지향 쿼리 언어 - 페치조인1(기본)

도윤·2024년 3월 23일
post-thumbnail

페치조인1 - 기본

  • SQL 조인 종류가 아니고 JPQL 의 기능 중 하나이다.
  • JPQL 에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능이다. 두 번 쿼리가 나갈 것을 한번 만 나가도록 하게 하는 기능
  • join fetch 사용
  • 페치 조인:: [LEFT][OUTER][INNER] JOIN FETCH 조인 경로

엔티티 패치 조인

예제

  • 회원 1과 회원2는 팀1에 소속되어 있다.
  • 회원3은 2 에 소속되어 있다.
  • 회원4는 아무 곳에서도 소속되지 않았다.
  • JOIN TEAM 에 없는 경우 회원이나 팀 C 가 null 값이기 때문이다.
  • 페치 조인을 사용하는 이유는 화면에 뿌릴 때 Member 외에 Team 데이터도 같이 뿌리기 위함이다.
  • 연속성 컨텍스트에 다섯개를 보관하고 위 사진과 같은 그림을 만들어서 쿼리 해준다.

@ManyToOne 페치 조인 예제


위 코드를 실행시켜서 로그를 분석해보자.

for 문 때문에 select 쿼리가 세 개가 나간 것을 볼 수 있다. Member 클래스에서 team 과의 연관관계를 LAZY 즉, 즉시로딩으로 설정하여 쿼리가 세 개 나간것이다. 이유는 일단 JPQL 에서 지정한 쿼리 때문에 Member 에 대한 쿼리를 날린다. 그런데 for 문에서 team 데이터를 가져오기 때문에 그때 다시 Team 에 대한 SELECT 쿼리를 날린다. 회원2가 바로 출력된 이유는 이미 1차 캐시(영속성 컨텍스트)에 팀A에 대한 데이터가 존재하므로 select 쿼리는 날라가지 않고, 마지막 쿼리인 팀 B에 대한 데이터는 1차 캐시에 없기 때문에 select 쿼리를 날려 데이터를 가져온다.

  • 회원1, 팀A -> 팀A에 대한 SQL
  • 회원2, 팀A -> 팀A가 이미 1차 캐시에 있으므로 쿼리 X
  • 회원3, 팀B -> 팀B는 1차 캐시에 없으므로 쿼리 O

만약 위 회원 데이터가 세 개가 아닌 100 개고 각자 다른 팀을 갖고 있다면? 그럼 Team 에 대한 쿼리만 100개가 나갈것이다. 이런 경우를 N+1 이라고 한다. 1은 제일 처음에 나간 Member에 대한 쿼리이고, N은 첫 번째 날린 쿼리의 결과의 수다. N+1 문제는 즉시 로딩이던 지연 로딩이던 똑같이 발생하는 문제이다. 해결 방법 중 하나는 바로 지금 배울 페치조인이다.

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

페치 조인을 사용하는 세 번의 쿼리가 한 번으로 줄어든 걸 볼 수 있다. 예제에서 보았던 for 문 안에 member.getTeam() 은 지연로딩에서는 프록시 객체였는데 페치 조인을 사용하면 프록시가 아니라 엔티티 이다. 출력 로그를 보면 SELECT 쿼리에서 이미 Member 와 Team 을 join 해서 데이터를 가져오기 떄문에 예제에서 쿼리를 날려 result 담는 순간에 Team 은 실제 엔티티가 담긴 것이다.

컬렉션 패치 조인

  • 일대다 관계, 컬렉션 패치 조인
  • 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'
  • 예제


실행했던 결과가 기대와는 다르게 나왔다. 팀 A가 중복으로 출력됐는데 이유는 컬렉션의 종특이다.DB관점에서 일대다 조인을 하면 데이터가 원래 저장되어 있던 수보다 더 많이 나올 수 있다.


만약 위의 데이터를 JOIN 했을 시엔 일반적으로 결과물이 아래와 같다.


즉 teamA 는 하나지만 member 가 둘이기 때문에 어쩔 수 없이 두 줄이 출력되는 것이다. 이런 경우에 DB 에서 그냥 두 줄을 주는 것이라 JPA 입장에서는 어떤 후처리를 할 수가 없다. 그래서 그냥 일단 DB 에서 주는 그대로 가져온다.


영속성 컨텍스트에는 팀A의 아이디가 똑같이 1번이기 때문에 하나로 관리가 된다.

페치 조인과 DISTINCT

  • SQL 의 DISTINCT 는 중복된 결과를 제거하는 명령이다
    • SQL 의 DISTINCT 만으로는 모든 중복을 제거할 수는 없다.
  • JPQL 의 DISTINCT 의 두가지 기능
    • SQL 에 DISTINCT 를 추가
    • 애플리케이션에서 엔티티 중복 제거
  • 예제


DISTINCT 를 추가하니 중복이 제거된 걸 볼 수 있다. 이게 SQL 에서 중복 제거에 성공한 것은 아니다. SQL 의 DISTINCT 의 경우엔 모든 컬럼의 데이터가 같아야 중복으로 치고 제거해준다. 이 예제에서는 팀A는 같지만 member 의 값이 다르므로 SQL 은 중복이 아니라고 친다.
그러면 어떻게 result 는 2가 출력됐을까?

  • DISTINCT 추가로 애플리케이션 중복 제거를 시도하다.
  • 같은 식별자가 가진 Team 엔티티를 제거한다.

  • DISTINCT 추가 시 결과
    • teamname = 팀A, team = Team@0x100
      • username = 회원1, member=Member@0x200
      • username = 회원2, member=Member@0x300

페치 조인과 일반 조인의 차이


일반 Join으로 코드를 수정하고 실행해보자.


놀랍게도 세 번의 쿼리가 발생한다. 그리고 첫 번째 SELECT 를 잘 보면 Member 와 Join 은 하지만 컬럼에 team 에 대한 값들만 가져오는 걸 볼 수 있다. 그리고 컬렉션은 컬렉션은 아니지만 컬렉션의 데이터가 로딩 시점에 로딩이 다 되지 않아 SELECT 쿼리가 세 번이 나간 것이다.

  • JPQL은 결과를 반환할 때 연관관계를 고려하지 않는다.
  • 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다.
  • 여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회하지 않는다.
  • 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)
  • 페치 조인은 객체 그래프를 SQL 한 번에 조회하는 개념이다.

참고 : 자바 ORM 표준 JPA 프로그래밍 - 기본편

profile
기록은 기억을 이긴다⭐

0개의 댓글