QueryDSL, JPA CrossJoin

Rojojun·2023년 2월 26일
0

TIL

목록 보기
4/4
post-thumbnail

Intro

글을 적기 앞서서 몇 글자를 적어보려 한다.
이번에는... 사진도 넣고 정성껏 적어보려고 한다...

일단, 첫번째로 유명하신 개발자 '향로'님의 블로그의 글을 참고하였다.
그리고 두번째로 참고 뿐만 아닌 더 궁금한 사항들에 대해서 파고 들어가 보았다.

이렇게 기록하는 이유는 세가지가 있다.

첫째, 차별성이 필요하다.
둘째, 순전히 내가 더 궁금하다.
셋째, 나처럼 궁금해 하는 이들을 위해 편하게 정보를 제공해주고 싶다.

CrossJoin의 무한열차

QueryDSL을 사용해 연관관계가 맺어진 컬럼을 조회하는데... 결과가 제대로 안나왔다.

사실, 문제는 내가 제대로된 컬럼을 설정을 안한 잘못인데... 콘솔창에 crossjoin이라고 적혀있는 것을 보았다...

'나는 CrossJoin 선언을 안했는데 왜 생긴거지?!'

라는 고민을 하고 있을 때, CrossJoin은 도대체 무엇인가에 대해서 궁금해지기 시작했다.

CrossJoin 과 카테시안 곱

Cartesian production(카테시안 곱)은 행과 열을 전체 곱하는 튜플들의 집합이다.

즉, 두 테이블에서 컬럼과 컬럼의 곱이 된다는 것이다

Join된 테이블 컬럼 = 컬럼 * 컬럼

테이블로서 예를 들어보자.
MySQL을 사용하여 구현한다는 가정하에 진행한다.

A테이블 : student

idnameagehousesubject_id
1Harry Potter20GRYFFINDOR1
2Hermione Granger20GRYFFINDOR1
3Ron Weasley20GRYFFINDOR2
4Severus Snape24SLYTHERIN3
5Luna Lovegood17RAVENCRAW2
6Cedric Diggory21HUFFLEPUFF3

B테이블 : subject

idnameroom
1Care of Magical Creatures101
2History of Magic202
3Charms104

이러한 테이블이 있다고 할 때 통상의 통념으로는 LEFT OUTER JOIN을 많이 사용하게 된다.
테이블의 형태는 다음과 같다.

SELECT *
FROM student st
	LEFT OUTER JOIN subject sub ON st.subject_id = sub.id 
idnameagehousesubject_idsub.idnameroom
1Harry Potter20GRYFFINDOR11Care of Magical Creatures101
2Hermione Granger20GRYFFINDOR11Care of Magical Creatures101
3Ron Weasley20GRYFFINDOR22History of Magic202
4Severus Snape24SLYTHERIN33Charms104
5Luna Lovegood17RAVENCRAW22History of Magic202
6Cedric Diggory21HUFFLEPUFF33Charms104

하지만, CROSS JOIN이 사용되게 될 경우 Student의 컬럼 6과 subject 컬럼 3이 곱해진 값인 18개가 나가게 되는 것이다.

즉, 조회에서 불필요한 부분까지 조회하기 때문에 쓰기, 읽기에서 성능 문제가 당연히 발생한다.

LEFT OUTER JOIN vs CROSS JOIN

  1. 둘 모두 연관관계를 가진 두 테이블에 데이터를 비교해 한 테이블에 정리한다.
  2. 전자의 경우 연관관계의 주인을 왼쪽으로 설정하고 왼쪽에 있는 테이블을 기준으로 해서 값을 가져온다.
  3. CROSS JOIN의 경우 모든 가지수를 더하기 때문에 집합 원소의 갯수가 기준점이 되는 원소보다 훨씬 많다.

이 둘의 관계는 집합에 대해서 알고 있으면 보다 더 자세하다.
또한 벤다이어그램을 보는 것을 추천하는데, 이 글에 첨부하지 않는 이유는 타인이 고생해서 많든 자료를 손쉽게 캡쳐해놓는거 보다 링크 공유를 해드리려고 한다.

JOIN에 대한 벤다이어그램 : https://blog.edit.kr/entry/Left-join-and-Left-outer-join-in-SQL-Server

본론으로

이제 길게 돌아와서 본론으로 갈 시간이다.

QueryDSL, JPA에서는 연관관계가 맺어져 있을 때 즉, @OneToOne, @OneToMany, @ManyToOne (혹은 그에 반대에 해당하는 사항도 포함) 별도의 join을 명시적으로 입력하지 않을 때 알아서 Cross Join 이 발생하게 된다.

select count(boardconte0_.id) as col_0_0_ from BoardContent boardconte0_ cross join
BoardCategory boardcateg1_ where boardconte0_.boardCategoryId = boardcateg1_.id
and boardcateg1_.category = ? and boardconte0_.isReplied = ? and boardconte0_.userId = ?

만약 CrossJoin이 걸리게 된다면 실제 콘솔에서의 쿼리는 이러한 형식으로 나오게 된다.

위의 테이블을 예를 들어서 코드로 이야기를 하게 되면 어떻게 될까?

Entity

Student

@NoArgsConstructor
@Getter
@Entity
public class Student {

	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private Integer age;
    private String house;
    @ManyToOne
    @JoinColumn(name = "subject_id")
   	private Subject subject
}

Subject

@NoArgsConstructor
@Getter
@Entity
public class Subject {

	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private Integer room;
    @OneToMany
    @JoinColumn(mappedBy = "student")
   	private Student student
}

이렇게 연결되어 있는 테이블에서 Subject를 기준으로 각 Subject의 학생 수를 조회 한다고 할 때 innerJoin을 명시하지 않으면 CrossJoin이 일어나게 된다.

즉, DSL에서 CrossJoin을 피하기 위해서는 명시적 Join은 무조건 해줘야한다!

뿐만아니라 JPA에서도 동일한데 그 이유는 바로 Hibernate의 특징 때문이다.

Hibernate와 Cross Join

Hibernate에서는 WHERE절에서 명시적인 Join을 설정하지 않으면, Cross Join을 기본 전략으로 하게 되어 있다.

만약 Hibernate가 Inner Join을 기본전략으로 하게 될 때는 개발자가 의도하지 않은 방향으로 결과를 가져올 가능성이 있다.

즉, 개발자가 의도하지 않은 Inner Join에서는 잘못된 데이터를 생성하여 Join시킬 가능성이 있기 때문에 차라리 성능에 불리한 Cross Join을 통해 그에 대한 가능성을 미리 막는 것이다.

위에서 서술한 Cross Join의 특성 때문에 이러한 경우에는 성능보다는 데이터의 정합성에 맞아야하기에 Hibernate에서는 기본전략으로 선택하지 않았나 싶다.

주의점

때때로 명시적 Join을 했음에도, Cross Join이 같이 나가는 경우가 있다.
이것은 그것의 버그가 아니라 내가 쿼리에 대한 이해가 부족한 경우일 확률이 높다.

Inner Join시 Cross Join까지 같이 일어나게 된다면, 쿼리스크립트를 다시 작성해서 한 번 결과를 보는것이 제일 편하다.

참고 : https://jojoldu.tistory.com/533
참고 서적 : Java Persistence with Hibernate

profile
Not today

0개의 댓글