JPA & QueryDSL

줍줍 메시지 조회 API 동적 쿼리 리팩터링

2022. 7. 25. 03:43
목차
  1.  
  2. 시나리오
  3. 적용한 내용
  4. Before
  5. After
  6. 줍줍의 메시지 API가 더 궁금하다면?
  7. 학습 출처

 

여러 가지 경우의 수에 대응해야 하는 메시지 조회 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가 더 궁금하다면?


메시지 조회 API 시나리오 정리

메시지 조회 API 사용법

메시지 조회 API 개발 상세 QueryDSL

 

 

 

학습 출처


김영한님, 인프런 - 실전! Querydsl

[우아콘2020] 수십억건에서 QUERYDSL 사용하기

 

저작자표시 (새창열림)

'JPA & QueryDSL' 카테고리의 다른 글

JpaRepository vs Repository  (4) 2022.07.06
  1.  
  2. 시나리오
  3. 적용한 내용
  4. Before
  5. After
  6. 줍줍의 메시지 API가 더 궁금하다면?
  7. 학습 출처
'JPA & QueryDSL' 카테고리의 다른 글
  • JpaRepository vs Repository
리차드
리차드
리차드
화음을 좋아하는 리차드🎶
리차드
전체
오늘
어제
  • 전체 게시글 보기 (200)
    • Portfolio (0)
    • Thoughts & Records (17)
    • 우아한테크코스 4기 (43)
    • Java & Spring (36)
    • JPA & QueryDSL (2)
    • Database (18)
    • Server & Infra (21)
    • Network (0)
    • Algorithm (11)
    • IDE (12)
    • HTML & CSS (4)
    • JavaScript (11)
    • Life (13)

블로그 메뉴

  • Github

공지사항

인기 글

태그

  • 화음을 좋아하는
  • Spring
  • oracle
  • java
  • IntelliJ
  • git
  • 우테코
  • 웹 백엔드 4기
  • EC2
  • 오라클
  • javascript
  • aws
  • 자바
  • 알고리즘
  • 스프링부트
  • 자바스크립트
  • 스프링
  • 리차드
  • 우아한테크코스
  • SQL

최근 댓글

최근 글

hELLO · Designed By 정상우.
리차드
줍줍 메시지 조회 API 동적 쿼리 리팩터링
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.