우선 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
를 조회하는 플로우는 존재하지 않으므로 Status
는 ACTIVE
고정값으로 전달된다.
이제부터 조건이 추가될 수록 메서드 길이도 길어지며 전달되는 인자 개수 등 수도 없이 복잡해질 수 있다.
필요한 데이터만 불러오려면 검색 조건이 포함된 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
생성이며, 동적 쿼리 최적화에 알맞다.
서비스 요구 사항 중 기간, 닉네임, 납부 여부 등의 필터링을 통해 원하는 데이터만 조회할 수 있는 기능이 존재한다.
@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나 비즈니스 로직에서 활용할 수 있는 라이브러리라는 점에서 많은 배움을 얻었다.