위 코드를 실행시켜서 로그를 분석해보자.
for 문 때문에 select 쿼리가 세 개가 나간 것을 볼 수 있다. Member 클래스에서 team 과의 연관관계를 LAZY 즉, 즉시로딩으로 설정하여 쿼리가 세 개 나간것이다. 이유는 일단 JPQL 에서 지정한 쿼리 때문에 Member 에 대한 쿼리를 날린다. 그런데 for 문에서 team 데이터를 가져오기 때문에 그때 다시 Team 에 대한 SELECT 쿼리를 날린다. 회원2가 바로 출력된 이유는 이미 1차 캐시(영속성 컨텍스트)에 팀A에 대한 데이터가 존재하므로 select 쿼리는 날라가지 않고, 마지막 쿼리인 팀 B에 대한 데이터는 1차 캐시에 없기 때문에 select 쿼리를 날려 데이터를 가져온다.
만약 위 회원 데이터가 세 개가 아닌 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 은 실제 엔티티가 담긴 것이다.
select t
from Team t join fetch t.members
where t.name = '팀A'
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 에서 중복 제거에 성공한 것은 아니다. SQL 의 DISTINCT 의 경우엔 모든 컬럼의 데이터가 같아야 중복으로 치고 제거해준다. 이 예제에서는 팀A는 같지만 member 의 값이 다르므로 SQL 은 중복이 아니라고 친다.
그러면 어떻게 result 는 2가 출력됐을까?
일반 Join으로 코드를 수정하고 실행해보자.
놀랍게도 세 번의 쿼리가 발생한다. 그리고 첫 번째 SELECT 를 잘 보면 Member 와 Join 은 하지만 컬럼에 team 에 대한 값들만 가져오는 걸 볼 수 있다. 그리고 컬렉션은 컬렉션은 아니지만 컬렉션의 데이터가 로딩 시점에 로딩이 다 되지 않아 SELECT 쿼리가 세 번이 나간 것이다.