QueryDsl로 까다로운 구조의 데이터 조회 처리하기

개요

사이드프로젝트 진행중 까다로운 데이터 구조를 다루게 되었다.
데이터 구조는 계층형 구조로 A -> B -> C 의 구조로 종속되어있다.

A에서 B를 가지고있고 B에서 C를 가지고 있는 구조이고, jpa repository에서 조회하게 되면 N + 1 문제도 생기게 된다.

고민

그러면 근본적으로 테이블 구조를 바꿔볼 수 없을까라고 생각하여 상속관계 매핑을 고려해보았는데 부모 클래스에 있는 데이터를 아래 자식 클래스에서 사용할일이 없다. 즉 확장의 개념이 아니기 때문에 이 방법은 사용하지 않기로 했다.

그렇다면 기존 데이터 구조는 유지하고 보다 효율적인 쿼리를 작성해보자라는 결론이 나왔다.

JPQL vs QueryDsl

JPQL의 경우 쿼리를 String으로 직접 작성해줘야하기때문에 짜증난다.
그래서 컴파일 시점에서 에러를 잡을 수 없고... 개인적으로 가독성이 굉장히 안좋다고 생각한다.

그리하여 QueryDsl을 사용해서 해결해보기로 했다.

기존 코드

// JpaRepository에서 호출
repository.findByEmailAndSubjectId(email, id);

결과


Hibernate: 
    /* select
        generatedAlias0 
    from
        UserCheckList as generatedAlias0 
    where
        (
            generatedAlias0.email=:param0 
        ) 
        and (
            generatedAlias0.subjectId=:param1 
        ) */ select
            usercheckl0_.id as id1_6_,
            usercheckl0_.email as email2_6_,
            usercheckl0_.subject_id as subject_3_6_ 
        from
            user_check_list usercheckl0_ 
        where
            usercheckl0_.email=? 
            and usercheckl0_.subject_id=?
Hibernate: 
    select
        usercheckl0_.user_check_list_id as user_che3_8_0_,
        usercheckl0_.id as id1_8_0_,
        usercheckl0_.id as id1_8_1_,
        usercheckl0_.section_title as section_2_8_1_,
        usercheckl0_.user_check_list_id as user_che3_8_1_ 
    from
        user_check_list_section usercheckl0_ 
    where
        usercheckl0_.user_check_list_id=?
Hibernate: 
    select
        elements0_.user_check_list_section_id as user_che4_7_0_,
        elements0_.id as id1_7_0_,
        elements0_.id as id1_7_1_,
        elements0_.element_title as element_2_7_1_,
        elements0_.is_checked as is_check3_7_1_,
        elements0_.user_check_list_section_id as user_che4_7_1_ 
    from
        user_check_list_element elements0_ 
    where
        elements0_.user_check_list_section_id=?

연관관계의 fetchType이 Lazy로 되어있기 때문에 필요한 시점에 n번의 쿼리가 더 날아가는것을 확인할 수 있었다.
아직 프로덕션에 올라가지않은 서비스이지만 충분히 문제가 될만한 부분이었다.

이 부분을 QueryDsl로 우아하게 처리해보자.

QueryDsl

fun findByEmailAndSubjectId(email: String, subjectId: Long): UserCheckList? {
        val userChecklist = QUserCheckList.userCheckList
        val section = QUserCheckListSection.userCheckListSection

        val userCheckLists = query.selectFrom(userChecklist)
            .leftJoin(userChecklist.userCheckListSections, section)
            .fetchJoin()
            .leftJoin(section.elements)
            .fetchJoin()
            .where(
                QUserCheckList.userCheckList.email.eq(email)
                    .and(QUserCheckList.userCheckList.subjectId.eq(subjectId))
            )
            .fetch()
            // join을 하게되면 테이블이 합쳐지게 되는데 이 과정에서 중복 데이터가 발생한다.
            // 따라서 직접 중복 제거를 한다.
            .stream()
            .distinct()
            .collect(Collectors.toList())

        return if(userCheckLists.isEmpty()) null else userCheckLists[0]
}

결과

Hibernate: 
*/ select
            usercheckl0_.id as id1_6_0_,
            usercheckl1_.id as id1_8_1_,
            elements2_.id as id1_7_2_,
            usercheckl0_.email as email2_6_0_,
            usercheckl0_.subject_id as subject_3_6_0_,
            usercheckl1_.section_title as section_2_8_1_,
            usercheckl1_.user_check_list_id as user_che3_8_1_,
            usercheckl1_.user_check_list_id as user_che3_8_0__,
            usercheckl1_.id as id1_8_0__,
            elements2_.element_title as element_2_7_2_,
            elements2_.is_checked as is_check3_7_2_,
            elements2_.user_check_list_section_id as user_che4_7_2_,
            elements2_.user_check_list_section_id as user_che4_7_1__,
            elements2_.id as id1_7_1__ 
        from
            user_check_list usercheckl0_ 
        left outer join
            user_check_list_section usercheckl1_ 
                on usercheckl0_.id=usercheckl1_.user_check_list_id 
        left outer join
            user_check_list_element elements2_ 
                on usercheckl1_.id=elements2_.user_check_list_section_id 
        where
            usercheckl0_.email=? 
            and usercheckl0_.subject_id=?

쿼리가 한번만 나가는것을 볼 수 있다.
추가적으로 이 데이터는 한번에 다 가져와야하는 데이터라 컬럼을 따로 지정해서 가져오진 않았다.

후기

JPQL대신 QueryDsl로 처리를 해보았는데 기존에 JPQL의 경우

"select * from data " +
"where id = blah"

위와같이 작성해주어야 해서 굉장히 까다로웠다.
하지만 QueryDsl을 도입함으로써 편리함과 가독성을 가져갈 수 있었다.
QueryDsl을 다루는데에는 아직 미숙하지만 공부하면서 더 잘 쓰게 된다면 굉장히 좋은 라이브러리일것같다.

추신

잘못된 부분은 지적 해주시면 감사하겠습니다.

좋은 웹페이지 즐겨찾기