우당탕탕 팀 프로젝트 QueryDSL 적용기

김희망·2023년 4월 21일
3

개발일지

목록 보기
11/17

🤔 개요

광주소프트웨어마이스터고등학교의 MSG팀에서 진행한 GAuth라는 프로젝트에서 검색 api를 만들면서 querydsl를 적용한 사례를 오늘 작성해보려고 한다. 작성해보고 나서의 장점에 대해서도 얘기해보겠다.

✍️ 요구사항

요구사항은 이랬다. 학년, 반, 키워드를 통해 유저를 검색한다. 만약 학년, 반에 0이 들어온다면 전체 검색의 기준을 달고 키워드는 "" 빈 문자열이 들어오면 전체 검색 기준을 단다. 예를 들어 학년 반 키워드에 2, 0, "김" 이 들어오면 2학년 김씨 모두를 검색한다.

🧐 첫 구현

처음에는 findAll을 통해서 모든 유저리스트를 가져오고 filter를 통해서 유저를 걸러내었다.

class GetAcceptedUsersService(
    private val userRepository: UserRepository,
) {
    fun execute(grade: Int, classNum: Int, keyword: String, page: Int, size: Int): Page<SingleAcceptedUserResDto> =
        userRepository.findAllByState(UserState.CREATED, PageRequest.of(page, size))
            .let { filterUser(it, grade, classNum, keyword) }
            .map { SingleAcceptedUserResDto(it) }

    private fun filterUser(users: Page<User>, grade: Int, classNum: Int, keyword: String): Page<User> =
        users.filter { grade == 0 || it.grade == grade }
            .filter { classNum == 0 || it.classNum == classNum }
            .filter { keyword.isEmpty() || it.name!!.contains(keyword) }
            .toList()
            .let { PageImpl(it, users.pageable, it.size.toLong()) }

}

이렇게 되면 문제가 생긴다. 내부 연산이 많이 일어나게 된다. 그리고 키워드를 거를때 contains를 사용하는데 가입된 유저의 수가 많을 수록 더 많은 연산을 수행하기때문에 프로그램의 성능이 저하된다.

🥵 두번째 구현

두번째 구현때는 좀 멍청한 하드코딩을 했는데 if문으로 인자 여부를 확인하고 그에 맞는 JPA find를 날렸다.

package com.msg.gauth.domain.user.services

import com.msg.gauth.domain.user.User
import com.msg.gauth.domain.user.enums.UserState
import com.msg.gauth.domain.user.presentation.dto.response.SingleAcceptedUserResDto
import com.msg.gauth.domain.user.repository.UserRepository
import com.msg.gauth.global.annotation.service.ReadOnlyService
import org.springframework.data.domain.Pageable

@ReadOnlyService
class GetAcceptedUsersService(
    private val userRepository: UserRepository,
) {
    fun execute(grade: Int, classNum: Int, name: String, pageable: Pageable): List<SingleAcceptedUserResDto> {
        val userList: List<User>
        = when {
            grade == 0 && classNum == 0 && name == "" -> userRepository.findAllByStateOrderByGrade(UserState.CREATED, pageable)
            grade == 0 && classNum == 0 -> userRepository.findAllByStateAndNameContainingOrderByGrade(UserState.CREATED, name, pageable)
            classNum == 0 && name == "" -> userRepository.findAllByStateAndGradeOrderByGrade(UserState.CREATED, classNum, pageable)
            grade == 0 && name == ""-> userRepository.findAllByStateAndClassNumOrderByGrade(UserState.CREATED, grade, pageable)
            grade == 0 -> userRepository.findAllByStateAndClassNumAndNameContainingOrderByGrade(UserState.CREATED, classNum, name, pageable)
            classNum == 0 -> userRepository.findAllByStateAndGradeAndNameContainingOrderByGrade(UserState.CREATED, grade, name, pageable)
            name == "" -> userRepository.findAllByStateAndGradeAndClassNumOrderByGrade(UserState.CREATED, grade, classNum, pageable)
            else -> userRepository.findAllByStateAndGradeAndClassNumAndNameContainingOrderByGrade(UserState.CREATED, grade, classNum, name, pageable)
        }
        return userList.map {
            SingleAcceptedUserResDto(it)
        }

이건 다시봐도 좀 에바다 ㅋㅋㅋ

✅ 결국 이 문제를 해결하기 위해서는?

동적 쿼리를 작성하기 위해 JPQL, Criteria나 QueryDSL을 사용해야한다.

그래서 결국 나는 QueryDSL를 채택하기로 했다.

😃 QueryDSL!!

그렇게 쿼리 dsl로 작성한 코드다.

package com.msg.gauth.domain.user.repository

import com.fasterxml.jackson.databind.util.ArrayBuilders
import com.msg.gauth.domain.user.QUser.user
import com.msg.gauth.domain.user.User
import com.querydsl.core.BooleanBuilder
import com.querydsl.core.types.dsl.BooleanExpression
import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport
import org.springframework.stereotype.Repository
import javax.persistence.EntityManager
import javax.persistence.PersistenceContext

@Repository
class CustomUserRepositoryImpl(
    private val jpaQueryFactory: JPAQueryFactory
) : CustomUserRepository {

    override fun searchUser(grade: Int, classNum: Int, keyword: String): List<User> {
        val booleanBuilder = BooleanBuilder()

        if (grade != 0) {
            booleanBuilder.and(user.grade.eq(grade))
        }

        if (classNum != 0) {
            booleanBuilder.and(user.classNum.eq(classNum))
        }

        if (keyword.isNotEmpty()) {
            booleanBuilder.and(user.name.like("%$keyword%"))
        }

        return jpaQueryFactory.selectFrom(user)
            .where(booleanBuilder)
            .fetch()
    }

}
import com.msg.gauth.domain.user.presentation.dto.response.SingleAcceptedUserResDto
import com.msg.gauth.domain.user.repository.UserRepository
import com.msg.gauth.global.annotation.service.ReadOnlyService
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageImpl
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable

@ReadOnlyService
class GetAcceptedUsersService(
    private val userRepository: UserRepository,
) {
    fun execute(grade: Int, classNum: Int, keyword: String): List<SingleAcceptedUserResDto> =
        userRepository.searchUser(grade, classNum, keyword)
            .map { SingleAcceptedUserResDto(it) }
}

✨ BooleanExpression

그렇게 성공적으로 리팩터링을 끝나치나 싶었던 찰나에 졸업생 선배분의 피드백이 있었다. 그것은 바로 BooleanBuilder가 아닌 BooleanExpression을 사용하란 것이였다.

그렇게 최종적으로 리팩터링한 코드다.

@Repository
class CustomUserRepositoryImpl(
    private val jpaQueryFactory: JPAQueryFactory
) : CustomUserRepository {

    override fun search(grade: Int, classNum: Int, keyword: String): List<User> {

        val expression = (if (grade != 0) user.grade.eq(grade) else null)
            ?.and(if (classNum != 0) user.classNum.eq(classNum) else null)
            ?.and(if (keyword.isNotEmpty()) user.name.like("%$keyword%") else null)

        return if(expression == null){
            jpaQueryFactory.selectFrom(user)
                .fetch()
        } else {
            jpaQueryFactory.selectFrom(user)
                .where(expression)
                .fetch()
        }
    }

}

이렇게 querydsl로 연산도 줄이고 더러운 하드 코딩도 줄일 수 있게 되었다.

앞으로 동적쿼리를 짤때 계속 공부하고 응용을 해보아야겠다. 

이렇게 나의 첫 querydsl 적용기는 이렇게 끝을 내렸다.

profile
소프트웨어 엔지니어, 김희망입니다.

0개의 댓글