여러 가지 경우의 수에 대응해야 하는 메시지 조회 API에 대한 이야기입니다
동적 쿼리를 작성하는 부분을 리팩터링한 내용을 기록해봅니다
시나리오
- 채널에 최초 접속 시, 채널 아이디를 전달하면 해당 채널의 가장 최신 메시지 20개를 시간 내림차순 정렬해서 반환
- 아래로 스크롤 내리면 채널 아이디, 메시지 아이디를 전달받아
해당 채널의 해당 메시지 아이디의 작성시간 보다 과거에 작성된 메시지 20개를 시간 내림차순 정렬해서 반환 - 날짜로 이동하면 채널 아이디, 날짜를 전달받아
해당 채널의 해당 날짜의 23:59:59 보다 과거에 작성된 메시지 20개를 시간 내림차순 정렬해서 반환 - 날짜로 이동 후 위로 스크롤을 옮길 시 (이동된 날짜 이후에 작성된 메시지를 보려 하면)
채널 아이디, 메시지 아이디, needPastMessage=false를 전달 받아
해당 채널의 해당 메시지 보다 미래에 작성된 메시지를 시간 오름차순 정렬하여 20개만 자른 뒤, 시간 내림차순 정렬해서 반환 - 검색어로 검색할 경우, 채널 아이디, 검색어를 전달 받아
해당 채널에 검색어가 포함된 메시지 20개를 시간 내림차순 정렬해서 반환 - 검색어로 검색한 후 스크롤을 아래로 내리면 채널 아이디, 검색어, 메시지 아이디를 전달 받아
해당 채널에 검색어가 포함된 메시지 중 해당 메시지 보다 과거에 작성된 메시지 20개를 시간 내림차순 정렬해서 반환
적용한 내용
- BooleanBuilder를 이용해 하나의 메서드 내에서 모든 로직을 수행하던 것에서,
BooleanExpression을 이용한 메서드 단위 개별 검증으로 분리했습니다. - 개별 조건으로 분리해낸 결과, 이들을 조합하여 재사용하기에도 용이하고,
필요시 where절에 컴마로 연이어 선언하여 명시적으로 사용하기에도 좋습니다. - BooleanExpression으로 null을 반환하게 되면
해당 조건은 where절에서 아예 전달되지 않은 것처럼 JPQL이 생성됩니다. - 검색어는 contains, 날짜는 eq, before, after 등으로 아주 손쉽게 비교할 수 있습니다.
- QueryDSL의 강력한 컴파일 시점의 검증 기능을 이용해,
타입 안정성을 확보할 수 있었습니다.
Before
private BooleanBuilder createFindMessagesCondition(final SlackMessageRequest slackMessageRequest) {
BooleanBuilder builder = new BooleanBuilder();
String keyword = slackMessageRequest.getKeyword();
if (StringUtils.hasText(keyword)) {
builder.and(QMessage.message.text.contains(keyword));
}
Long messageId = slackMessageRequest.getMessageId();
boolean needPastMessage = slackMessageRequest.isNeedPastMessage();
if (Objects.nonNull(messageId)) {
Message message = messageRepository.findById(messageId)
.orElseThrow(() -> new MessageNotFoundException(messageId));
LocalDateTime messageDate = message.getPostedDate();
if (needPastMessage) {
builder.and(QMessage.message.postedDate.before(messageDate));
} else {
builder.and(QMessage.message.postedDate.after(messageDate));
}
return builder;
}
LocalDateTime date = slackMessageRequest.getDate();
if (Objects.nonNull(date)) {
if (needPastMessage) {
builder.and(
QMessage.message.postedDate.eq(date)
.or(QMessage.message.postedDate.before(date))
);
builder.and(QMessage.message.postedDate.before(date));
} else {
builder.and(
QMessage.message.postedDate.eq(date)
.or(QMessage.message.postedDate.after(date))
);
}
}
return builder;
}
After
private BooleanExpression meetAllConditions(final SlackMessageRequest request) {
return channelIdsIn(request.getChannelIds())
.and(textContains(request.getKeyword()))
.and(messageIdOrDateCondition(request.getMessageId(), request.getDate(), request.isNeedPastMessage()));
}
private BooleanExpression channelIdsIn(final List<Long> channelIds) {
return QMessage.message.channel.id.in(channelIds);
}
private BooleanExpression textContains(final String keyword) {
if (StringUtils.hasText(keyword)) {
return QMessage.message.text.contains(keyword);
}
return null;
}
private Predicate messageIdOrDateCondition(final Long messageId,
final LocalDateTime date,
final boolean needPastMessage) {
if (Objects.nonNull(messageId)) {
return messageIdCondition(messageId, needPastMessage);
}
return dateCondition(date, needPastMessage);
}
private Predicate messageIdCondition(final Long messageId, final boolean needPastMessage) {
Message message = messageRepository.findById(messageId)
.orElseThrow(() -> new MessageNotFoundException(messageId));
LocalDateTime messageDate = message.getPostedDate();
if (needPastMessage) {
return QMessage.message.postedDate.before(messageDate);
}
return QMessage.message.postedDate.after(messageDate);
}
private Predicate dateCondition(final LocalDateTime date, final boolean needPastMessage) {
if (Objects.isNull(date)) {
return null;
}
if (needPastMessage) {
return QMessage.message.postedDate.eq(date)
.or(QMessage.message.postedDate.before(date));
}
return QMessage.message.postedDate.eq(date)
.or(QMessage.message.postedDate.after(date));
}
줍줍의 메시지 API가 더 궁금하다면?
학습 출처
김영한님, 인프런 - 실전! Querydsl
[우아콘2020] 수십억건에서 QUERYDSL 사용하기
'JPA & QueryDSL' 카테고리의 다른 글
JpaRepository vs Repository (4) | 2022.07.06 |
---|