스프링 공부/JPA

[JPA] 27. Re.Zero : JPQL 그냥 다 끍어오기.(fetch join)

장아장 2023. 1. 19. 19:38

페치 조인

돼지꼬리 그리고 별 세개 박고 밑줄 쫙 긋고 하이라이트까지 칠해라. 겁나 중요하다. 

 

SQL 조인 종류는 아니다. 

JPQL에서 성능 최적화를 위해 따로 만든 기능으로, 연관된 엔티티나 모든 컬렉션을 한번에 조회하는 기능이다. Join fetch 명령어를 이용한다. 

2번으로 나뉘는 쿼리들을 한방에 나가게 할 수 있다. 

 

Select m from Member m join fetch m.team;

이러한 페치 조인은, 회원을 조회하며 연곤 팀도 같이 구해올 수 있다. 

즉, 멤버의 정보와 멤버가 속한 팀의 정보를 모두 구해오게 해준다. 

(즉시로딩과 똑같이 동작한다. 하지만 쿼리를 통한 명시적 방식이다)

 

이렇게 만들어진 상황에서, 페치 조인이 아닌 그냥 멤버 조회를 했을 때, 멤버에서 팀을 가져온다고 해보자. 

멤버1 조회 : db에서 회원1을 찾아오고, 회원1의 팀A를 찾아온다. 1차캐시에 저장해둔다. 

멤버2 조회 : db에서 회원2를 찾아오고, 1차 캐시에서 팀 A를 가져온다. 

멤버3 조회 : 1차캐시에 팀과 멤버 둘 다 없으므로 쿼리가 2번 나간다. 

멤버4 조회 : 1차캐시에 멤버가 없고 팀은 null이므로 쿼리가 한번 나간다. 

 

이랬을 때, 만약 멤버 3같은 경우가 100번이면? 쿼리 200번이 나가야한다. 

이러한 N+1문제(그 유명한!!!)이 나온다. 

 

N+1의 문제는,

예를 들면 Team을 1개 조회했을 때, 거기에 엮인 멤버를 N번 조회해야 한다는 문제가 있다. 

즉, 1로 얻은 결과 만큼 N번 쿼리를 더 보내는 문제이다. 

(로딩이 즉시던, 지연이던 다 그런다)

 

Fetch join을 사용하면, 한번에 모든 것을 찾아오는 쿼리문이기 때문에, 파생되어 쿼리를 더 보낼 일이 없어진다. 

 

그냥 멤버를 가지고 온 후 Team의 이름까지 조회를 요청할 때, 팀에 대한 쿼리를 보낸 후 결과를 가져온다. 

3번 회원의 경우 팀이 다른데, 영속성 컨텍스트에 없기 때문에 다시 쿼리를 보냈다가 받아야 값을 반환해준다. 

 

이에 비교해서 fetch join을 이용한 쿼리문을 실행해보았다. 

이러한 쿼리문을 통해서 fetch join이 적용된 쿼리문이 나갔고, 

 

이후에 추가적인 쿼리문 없이 한번에 결과를 출력하는 것을 볼 수 있다. 

 

멤버와 팀은 다대일 연관관계를 가지고 있다. 

그러면 팀에서, 팀의 멤버를 가지고 오는(객체 기준으로는 팀도 List<Member>를 가지고 있으니까) 쿼리문에서는 

어떤 결과가 나올까?

여기에서 

이런식으로 리스트를 member.toString으로 가져와 출력해준다. 

쿼리문도 이렇게 한방에 처리되는 것을 볼 수 있다. 

이 때 문제는, 일대다 이기 때문에 팀의 기준에서, 멤버가 많아지면 그 만큼 줄 수가 많아진다

팀과 멤버를 조인하기 때문에, 멤버가 다를 때 마다 row가 증가하는 문제가 있다. 

 

이게 참조가 계속되면서 중복이 발생할 수 있다. 

이 때, distinct를 사용해 중복을 제거할 수 있다. 

JPQL의 distinct는 sql의 distinct에 엔티티에 대한 중복을 제거해주는 기능이 추가되어있다. 

쉽게말하면, 팀과 멤버의 조인 테이블을 반환해오는 과정에서, 

A에 멤버1, 멤버2가 조인되었을 떄 멤버에 대한 데이터가 다르기 때문에, A가 두개 발견되게 된다. 

이를 distinct를 사용하면, A는 한개, 그 A에 대해 멤버1, 멤버2를 가지는 결과를 가지게 할 수 있다.

(그래도 데이터가 조금이라도 줄어야 연산량이 줄어들지 않을까, 그리고 사용하는 메모리가 적어지지 않을까 싶다)

 

페치 조인은 연관된 엔티티도 같이 가져오는, 쉽게 말하면 즉시로딩이 일어난다. 

객체 그래프에서 필요한 것을 모두 묶어서 한번에 가져오는, 편리성이 존재한다. 

 


 

페치조인의 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다(join fetch t.myMember m으로 두면 안된다
    연관된 대상을 모두 가져오기 때문라고 한다.
    내 생각으론, 컬렉션으로 가져오기 때문인 것 같다.
    이 컬렉션에서 특정 객체를 별칭으로 선언할 수 없지 않을까? 싶다.
    객체 그래프라는 것은 데이터를 다 조회하는 것이어서 그렇다고 한다.
    그런 의도로 만들어진 방식에서, 데이터를 필터링한다는 것은 문제가 있는 것 같다.
    이런 문제라면 서브쿼리로 만들어서 쓰는게 더 낫지 않을까 싶다. 
  • 둘 이상의 컬렉션에 페치 조인을 할 수 없다.
    페치 조인시엔 데이터가 곱으로 늘어 나온다.
    그런데, 컬렉션이 두개 이상이면 그만큼 곱연산으로 증가된다. 
  • 컬렉션 페치조인 시에 페이징 API를 사용할 수 없다.
    페이징이라는 것은 DB에 대한 사용량을 인덱스, 개수로 줄이는 방식이다.
    하지만, 데이터를 긁어 모으는 방식의 페치 조인과는 맞지 않다.
    (하지만, ~대일의 방식의 페치조인에서는 괜찮다. 데이터 row가 증가하는 페치조인의 방식이 아니기 때문이다)

 

페치조인의 특징

  • 연관된 엔티티들을 sql 한번에 조회시켜준다. 즉, 한번에 모든 처리가 가능하므로 성능 최적화에 사용할 시 좋다
  • 엔티티에 직접 사용한 FetchType.LAZY보다 우선순위상 위에 있다. (이를 글로벌 로딩 전략이라고 한다)
  • 글로벌은 기본적으로 지연로딩으로, 최적화가 필요한 부분에 페치 조인으로 실행하는 것이 좋다

 

페치조인 정리

  • 모든 것을 페치조인으로 해결할 수 있는 것은 아니다.

 

그러면 ~대다 페치조인의 데이터를 페이징으로 만들려면 어떻게 하는게 좋을까?

한번 공부해서 정리해봐야겠다.