[#3] QueryDSL 도입기

Park Jae-Min·2024년 3월 25일
0

소심한 총무

목록 보기
5/5

🤔 QueryDSL

우선 DSL이란 Domain Specific Language의 약자로, 특정 영역에 특화된 언어이다. 즉, QueryDSL이란 Query 생성에 특화된 언어이자 프레임워크이다.

위 문장에서 유추할 수 있는 정보는 Query문을 직접 생성할 수 있다는 점이다. 그럼 필요성의 이유를 따져보자.

List<Event> findAllByEventIdListAndUserIdAndGroupId(long userId, long groupId, List<Long> eventIdList);

서버 코드 중 Event 도메인을 eventIdList, userId, groupId를 통해 조회하는 메서드이다.

List<Event> findAllByEventIdListAndUserIdAndGroupIdAndStatus(long userId, long groupId, List<Long> eventIdList, Status status);

조건을 추가해서 삭제되지 않은 Event만 조회하고 싶은 경우 위처럼 변경이 가능하다.


다만 생각해보면 삭제된 Event를 조회하는 플로우는 존재하지 않으므로 StatusACTIVE 고정값으로 전달된다.

이제부터 조건이 추가될 수록 메서드 길이도 길어지며 전달되는 인자 개수 등 수도 없이 복잡해질 수 있다.

필요한 데이터만 불러오려면 검색 조건이 포함된 Query문을 직접 만들어서 조회하는 JPQL의 선택지가 있다.

@Query("SELECT e FROM Event e " +
        "WHERE e.group.id = :groupId " +
        "AND e.user.id = :userId " +
        "AND e.id IN (:eventIdList) " +
        "AND e.status = 'ACTIVE'")
List<Event> findAllByEventIdList(@Param("groupId") long groupId, @Param("eventIdList") List<Long> eventIdList);

JPA에서 지원하는 JPQL 사용 시, 직접 Query문을 만들어서 조건을 통해 조회가 가능해진다.

하지만, 해당 Query문은 문자열에 불과하다. 즉 Query문의 문법 오류를 파악하지 못 하며 직접적인 메서드 호출 시 문법 오류를 파악할 수 있으며,

@Query 어노테이션을 활용해도 프로그램을 시작해야 문법적인 오류를 확인 가능하다는 단점이 존재한다.

조건이 다수일 경우와 해당 조건 각각의 경우의 수가 많을 경우 개발자는 JPA 메서드 명명 규칙을 따라 해당 메서드를 N개를 만들거나 JPQL로 완벽한 Query 문자열을 N개 만들어야만 하는 문제점이 생긴다.

이러한 문제점들로 인해 QueryDSL이란 JPQL을 생성해주는 라이브러리가 등장하게 된다.

QueryDSL의 목적은 JPQL 생성이며, 동적 쿼리 최적화에 알맞다.


🔧 Refactoring

서비스 요구 사항 중 기간, 닉네임, 납부 여부 등의 필터링을 통해 원하는 데이터만 조회할 수 있는 기능이 존재한다.

    @Override
    public ListInfo<EventListInfo> getEventList(long groupId, EventListReq eventListReq) {

        if (eventListReq.getPage() == null) {
            throw new CustomException(CodeType.INPUT_PAGE_DATA);
        }

        Group group = groupRepository.findById(groupId).orElseThrow(() -> new CustomException(CodeType.NOT_FOUND_GROUP));

        long totalCount = 0;
        List<Event> eventList;
        List<EventListInfo> eventInfoList = null;
        PageRequest pageRequest = PageRequest.of(eventListReq.getPage(), 16, Sort.by(Direction.ASC, "groundsDate"));
        Page<Event> page = null;

        if (eventListReq.getYear() == null && eventListReq.getNickname() == null
            && eventListReq.getPaymentType() == null && eventListReq.getToday() == null) {}

        if (eventListReq.getYear() != null && eventListReq.getMonth() != null && eventListReq.getWeek() == null) {}

        if (eventListReq.getYear() != null && eventListReq.getMonth() != null && eventListReq.getWeek() != null) {}

        if (eventListReq.getYear() != null && eventListReq.getMonth() != null && eventListReq.getDay() != null) {}

        if (eventListReq.getToday() != null && eventListReq.getToday().equals("true")) {}

        if (eventListReq.getNickname() != null && eventListReq.getYear() == null
            && eventListReq.getToday() == null && eventListReq.getPaymentType() == null) {}

        if (eventListReq.getYear() != null && eventListReq.getMonth() != null && eventListReq.getWeek() == null && eventListReq.getNickname() != null) {}

        if (eventListReq.getYear() != null && eventListReq.getMonth() != null && eventListReq.getWeek() != null && eventListReq.getNickname() != null) {}

        if (eventListReq.getYear() != null && eventListReq.getMonth() != null && eventListReq.getDay() != null && eventListReq.getNickname() != null) {}

        if (eventListReq.getToday() != null && eventListReq.getToday().equals("true") && eventListReq.getNickname() != null) {}

        if (eventListReq.getPaymentType() != null && eventListReq.getYear() == null && eventListReq.getToday() == null
            && eventListReq.getNickname() == null) {}

        if (eventListReq.getYear() != null && eventListReq.getMonth() != null && eventListReq.getWeek() == null && eventListReq.getPaymentType() != null) {}

        if (eventListReq.getYear() != null && eventListReq.getMonth() != null && eventListReq.getWeek() != null && eventListReq.getPaymentType() != null) {}

        if (eventListReq.getYear() != null && eventListReq.getMonth() != null && eventListReq.getDay() != null && eventListReq.getPaymentType() != null) {}

        if (eventListReq.getToday() != null && eventListReq.getToday().equals("true") && eventListReq.getPaymentType() != null) {}

        if (eventListReq.getNickname()!= null && eventListReq.getPaymentType()!= null) {}

        if (eventListReq.getYear() != null && eventListReq.getMonth() != null && eventListReq.getWeek() == null && eventListReq.getNickname()!= null && eventListReq.getPaymentType()!= null) { }

        if (eventListReq.getYear() != null && eventListReq.getMonth() != null && eventListReq.getWeek() != null && eventListReq.getNickname()!= null && eventListReq.getPaymentType()!= null) {}

        if (eventListReq.getYear() != null && eventListReq.getMonth() != null && eventListReq.getDay() != null && eventListReq.getNickname()!= null && eventListReq.getPaymentType()!= null) {}

        if (eventListReq.getToday() != null && eventListReq.getToday().equals("true") && eventListReq.getNickname()!= null && eventListReq.getPaymentType()!= null) {}
        return ListInfo.from(totalCount, eventInfoList);
    }

1차 배포 당시 해당 API 개발자의 코드이다. 메서드 하나가 약 400줄 가량의 라인으로 서비스 코드를 다 지웠음에도 불구하고 라인 수가 어마어마한 형태이다.

위 코드를 리팩토링 하는 순간 QueryDSL을 고려했으며 직관적인 쿼리 작성이 가능하다는 관점으로 도입을 시도했다.

필터링 조건

기간

if (eventListReq.getYear() != null && eventListReq.getMonth() != null && eventListReq.getWeek() == null) {}
if (eventListReq.getToday() != null && eventListReq.getToday().equals("true")) {}

유추할 수 있는 정보는 Year, Month, Week, Today 등의 변수를 활용해서 해당 기간의 데이터를 조회하는 형태인 듯했다.

between을 활요한 형태로 수정했으며 2개의 기간 사이의 해당되는 데이터를 조회하는 플로우로 변경했다.

private BooleanExpression betweenTime(LocalDate startDate, LocalDate endDate) {
    return startDate == null ? null : event.date.between(startDate, endDate);
}

닉네임

Nickname 이란 변수를 활용해 Nickname이 일치하는 데이터만 조회하는 플로우

private BooleanExpression equalsNickname(String nickname) {
    return StringUtils.isBlank(nickname) ? null : event.nickname.eq(nickname);
}

납부 여부

1차에선 PaymentType, 2차에선 Situation으로 수정되었으며 미납, 완납 등의 일치 데이터만 조회하는 플로우

private BooleanExpression equalsSituation(Situation situation) {
    return situation == null ? null : event.situation.eq(situation);
}

최종 필터링 메서드

private JPAQuery<Event> filterEvent(FilterEventRequest filterEventRequest) {
    return jpaQueryFactory
            .selectFrom(event)
            .where(
                    equalsGroup(filterEventRequest.getGroupId()),
                    betweenTime(filterEventRequest.getStartDate(), filterEventRequest.getEndDate()),
                    equalsNickname(filterEventRequest.getNickname()),
                    equalsSituation(filterEventRequest.getSituation()),
                    event.status.ne(Status.DELETED)
            )
            .orderBy(event.date.asc(), event.id.asc());
}

동적 쿼리 최적화의 QueryDSL 도입으로 인해 400줄 가량의 라인에서 전체 50줄 정도로 압축됐고 직관적인 쿼리문 작성이 가능해졌다.

필터링 API에 국한되지 않고 여러 동적 쿼리가 필요한 API나 비즈니스 로직에서 활용할 수 있는 라이브러리라는 점에서 많은 배움을 얻었다.

0개의 댓글