본문 바로가기

Dot Programming/JPA

[JPA] 페치 조인(Fetch Join)의 일관성 - 대상에 별칭 사용하면 안되는 이유

    JOIN의 ON과 WHERE절의 차이

    SQL문에서 ON과 WHERE절의 차이점은 JOIN을 할때 필터링하는 시점이 다르다는 것이다. 확연한 예시를 위해 LEFT OUTER JOIN을 사용해서 쿼리를 날려보자. 

    • user테이블과 matches테이블은 N대1 매핑이 되어있다.

    일반 LEFT JOIN

    비교를 위해 그냥 아무런 별칭없이 LEFT JOIN을 하면 다음과 같이 조인이 된다.

    select * from user u 
    left join matches m 
    on (u.matches_id = m.id);

    left join

    ON

    JOIN을 하는 시점에 JOIN 대상을 필터링한다. 

    select * from user u 
    left join matches m 
    on (u.matches_id = m.id) and u.name='Alice';

     

    JOIN을 하는 시점에 필터링이 되기 때문에 'Alice'라는 이름을 가지지 않은 다른 레코드들은 null과 조인이 된 것을 볼 수 있다.

     

    조회 결과

    WHERE

    JOIN을 하고 난 후에 그 목록을 필터링을 한다.

    select * from user u 
    left join matches m 
    on (u.matches_id = m.id) where u.name='Alice';

     

    JOIN이 이뤄지고 난 후에 필터링이 되기 때문에 깔끔하게 'Alice'라는 이름을 가진 데이터만 조회되는 것을 볼 수 있다.

     

    조회 결과

     

    페치 조인(Fetch Join)에서 대상에게 별칭을 걸면 안되는 이유

    여러 조회 쿼리를 만들다보면 당연히 조인에 WHERE이나 ON절과 같은 필터링이 필요해지는 순간이 온다. 원래 JPA에서는 페치 조인은 별칭을 사용할 수 없게 만들었다는데 Hibernate에는 별칭을 허용하였다고 한다. 

    A fetch join does not usually need to assign an alias, because the associated objects should not be used in the where clause (or any other clause). The associated objects are also not returned directly in the query results. Instead, they may be accessed via the parent object. The only reason you might need an alias is if you are recursively join fetching a further collection: - hibernate 문서 중

    페치 조인은 일반적으로 별칭(alias)을 할당할 필요가 없다. 왜냐하면 페치 조인의 대상이 where절에서 사용되어서는 안되기 때문이다. 만약 별칭을 사용하게 되면 그 연관된 객체(페치 조인 대상)들은 곧바로 쿼리 결과로 리턴되지 않고 페치조인 주인(parent) 객체를 통해 접근할 수 있게 된다.
    → 요약하면, 별칭을 사용할 경우 페치 조인 결과는 연관된 모든 엔티티가 있을 것이라 가정하고 사용해야 하는데 이를 어기게 된다. (객체지향과 DB의 불협화음. orm 매핑, 일관성이 깨진다고 보면 된다.)

    별칭이 필요한 유일한 때는 다음과 같이 추가 컬렉션을 재귀적으로 갖고올 때 뿐이다. 그 외는 x 

    from Cat as cat
         inner join fetch cat.mate
         left join fetch cat.kittens child
         left join fetch child.kittens

     

    ON을 사용하면 에러나는 이유

    페치 조인에 on을 사용하면 다음과 같은 에러가 발생한다. 이는 어느 관계(ToMany, ToOne)에 해도 마찬가지이다.

    List<Member> result = em.createQuery("select m from Member m left"
    	+" join m.team t on m.name='member1'", Member.class)
    	.getResultList();

     

    이와 같은 에러가 발생하는 이유는 위에서 다뤘다시피 JOIN절에 ON 필터링을 사용하면 JOIN 시점에 JOIN 대상을 필터링하기 때문에 모든 컬렉션 데이터를 가져올 수 없기 때문이다. 이는 페치 조인 의의인 '연관된 엔티티나 컬렉션을 한 번에 조회해준다'는 내용과 위반되기 때문에 on 조건을 주면 무조건 에러가 발생하도록 설계되었다.

    org.hibernate.hql.internal.ast.QuerySyntaxException: with-clause not allowed on fetched associations; use filters [select m from com.example.springfetchjoin.domain.Member m join fetch m.team t on t.name='teamA']

     

    WHERE절은 페치조인 대상에게는 사용하면 안된다

    WHERE절은 JOIN이 이뤄지고 난 후에 필터링이 되는 것이기 때문에 ON절과 달리 무리없이 실행이 된다. 그러나 조심해야 할 부분이 있는데 바로 다음과 페치 조인 대상에게 WHERE절 별칭을 다는 경우이다. 애플리케이션에서 페치 조인 결과는 연관된 모든 엔티티가 있을 것이라 가정하고 사용해야 한다. 만약 페치 조인에 별칭을 잘못 명시해서 컬렉션 결과를 필터링하게 된다면 객체의 상태와 DB의 상태가 일치하지 않아 일관성이 깨지게 되는 것을 주의해야 한다.

     

    OneToMany 주인에게 별칭 - OK

    다음과 같이 페치 조인의 주인(Team)에게 필터링을 거는 것은 상관이 없다. 원하는 Team을  필터링한 후 그에 속한 모든 Member 엔티티도 함께 조회가 되기 때문에 아무 무리가 없다.

    List<Team> result = em.createQuery("select t from Team t " 
    	+ "join fetch t.members m where t.name='teamA'", Team.class)
    	.getResultList();

     

    OneToMany 대상에게 별칭 - 일관성 깨짐

    하지만 반대로 페치 조인의 대상(Member)에게 사용되는 방식은 에러가 발생하지는 않지만 지양되고 있다. 

    List<Team> result = em.createQuery("select t from Team t join fetch t.members m where m.name='member1'", Team.class)
    			.getResultList();

     

    페치 조인 대상을 필터링 해버리면 다음과 같이 Member 컬렉션의 일부만 조회가 되버린다. 이렇게 되면 페치 조인의 의의와 어긋나 버리게 된다. 페치 조인의 결과는 연관된 모든 엔티티가 존재할 것이라고 가정하고 사용하는데 이와 같이 잘못된 별칭으로 컬렉션 결과를 필터링 해버리면 객체의 상태와 DB의 일관성이 깨지게 된다.

     

     

    원래대로 라면 다음과 같은 3개의 Member 데이터와 관계가 있는 TeamB에서 'member1'이라는 이름을 가진 데이터를 고르는 것이 맞는 방식이다. 

     

     

     

    ManyToOne 대상에게 별칭 - OK

    그런데 ManyToOne의 경우에는 대상에게 WHERE절 별칭을 걸어도 괜찮다. 왜냐하면 조회된 회원은 DB와 동일한 일관성을 유지한 Team 데이터를 갖고 있기 때문에 Member와 Team의 일관성을 해치지 않기 때문이다. 

    List<Member> result = em.createQuery("select m from Member m "
    			+ "join fetch m.team t where t.name='teamA'", Member.class)
    			.getResultList();

     

    해당 쿼리는 연관된 Team 엔티티에 대해 모두 조회가 된다.

     

     

    ManyToOne left join으로 대상에게 별칭 - 일관성 깨짐

    그러나 이제 또 여기서 만약 left outer join으로 바꾸면 일관성이 깨지게 되는 것이다. 해당 글 맨 위에서 보여준 left join은 원래 왼쪽(주인)의 컬럼을 모두 가져와야 한다. 그래서 left join을 사용했으면 무조건 주인이 되는 member 컬렉션에 대한 정보를 모두 불러와야 한다.

    List<Member> result = em.createQuery("select m from Member m "
    			+ "left join fetch m.team t where t.name='teamA'", Member.class)
    			.getResultList();

     

    그러나 위의 inner join 결과와 마찬가지로 2개의 member 데이터만 조회가 된다.

     

     

    일관성이 깨지지 않으려면 다음과 같이 모든 member가 조회된 상태에서 'teamA'를 가진 2개의 데이터만 고르는 것이 맞다.

     


    참고

    자바 ORM 표준 JPA 프로그래밍

    https://www.inflearn.com/questions/15876

    https://stackoverflow.com/questions/17431312/what-is-the-difference-between-join-and-join-fetch-when-using-jpa-and-hibernate

    https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/queryhql.html#queryhql-joins