Skip to content

메시지 조회 API 개발 상세 QueryDSL

yeonLog edited this page Nov 12, 2022 · 5 revisions

메시지 조회 API 개발 상세

  • 여러 검색 조건을 동적으로 적용해야 하는 요구사항이 발생했습니다.
  • Spring Data JPA 기술만으로 대응하기 어렵다고 느껴졌습니다.
  • 메시지 조회 API 개발 전까지 도입되지 않았고, 도입한 당사자들도 아주 익숙하지는 않은 기술이어서 정리해봅니다.
  • 관련 커밋은 여기를 참고해주세요



QueryDSL 초기 세팅

build.gradle 세팅

  • plugin 추가
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
  • configurations 추가
querydsl.extendsFrom compileClasspath
  • dependencies 추가
implementation 'com.querydsl:querydsl-jpa:5.0.0'
implementation 'com.querydsl:querydsl-apt:5.0.0'
  • Q클래스 컴파일 설정 추가
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}
sourceSets {
    main.java.srcDir querydslDir
}
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}



JpaQueryFactory 스프링 빈 등록하기

  • 아래와 같이 스프링 빈으로 등록해두면, 필요한 곳에서 JpaQueryFactory를 생성자 주입받아 사용할 수 있습니다.
@Configuration
public class QuerydslConfig {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}



compileQuerydsl

Entity 클래스에 변경이 생겼을 경우 compileQuerydsl 을 실행해야 추후 코딩 시 컴파일 시 변경된 내용이 적용됩니다.



QueryDSL 사용법

기본 사용법

jpaQueryFactory
    .selectFrom(QMessage.message)
    .leftJoin(QMessage.message.member)
    .fetchJoin()
    .where(QMessage.message.channel.id.in(slackMessageRequest.getChannelIds()))
    .where(builder)
    .limit(slackMessageRequest.getMessageCount())
    .orderBy(getTimeCondition(slackMessageRequest.isNeedPastMessage()))
    .fetch();
  • jpaQueryFactory를 이용해 문장을 시작합니다.
  • selectFrom 을 이용해 select m from Message m 과 같은 문장을 간략화할 수 있습니다.
    • QMessage는 compileQuerydsl 을 통해 생성된 엔티티 클래스의 별칭으로 보시면 됩니다.
    • 이를 통해컴파일 시점에 정적으로 타입을 체크해주는 기능을 제공합니다.
  • Message 엔티티 조회 시, 작성자인 Member도 함께 즉시 로딩하기 위해 leftJoin, fetchJoin을 사용했습니다.
  • where 절에 여러 조건을 명시하기 위해 where절 안에 , 구분자로 여러 Predicate 또는 BooleanExpression을 담을 수도 있고, 위 예시처럼 여러 where절을 메서드 체이닝으로 선언할 수도 있습니다.
  • limit과 orderBy는 메서드 체이닝 상 순서와 생성되는 JPQL은 무관합니다.
    • 즉 .limit().orderBy() 와 .orderBy().limit() 이 생성하는 JPQL이 동일합니다.
  • 최종적으로 fetch 를 통해 쿼리를 수행합니다.
    • fetch 를 수행하기 전에 변수로 할당한 뒤 이를 출력하면 생성된 JPQL을 확인할 수 있습니다.



동적 쿼리 사용법

위 사용법 예시에서 where절에 사용된 builder 객체는 아래와 같은 코드로 생성됩니다.

BooleanBuilder builder = createFindMessagesCondition(slackMessageRequest);



createFindMessagesCondition 메서드 내부 중 일부를 살펴보겠습니다.

BooleanBuilder builder = new BooleanBuilder();

String keyword = slackMessageRequest.getKeyword();
if (StringUtils.hasText(keyword)) {
    builder.and(QMessage.message.text.contains(keyword));
}

하나의 BooleanBuilder를 선언한 뒤에, 전달된 값에 따라 분기하여 builder에 조건을 추가해주는 식입니다. like %keyword% 와 같은 문장을 Java의 contains와 같이 직관적으로 사용할 수 있습니다.



동적 쿼리 리팩터링

이하 내용은 아직 코드에 반영되지 않았고 리팩터링 가능성을 제시하는 정도로 이해해주시면 감사하겠습니다.

private BooleanExpression meetAllConditions(final SlackMessageRequest request) {
    return channelIdsIn(request.getChannelIds())
        .and(textContains(request.getKeyword()))
        .and(messageIdOrDateCondition(request.getMessageId(), request.getDate(), request.isNeedPastMessage()));
}

전체 검색 조건을 위와 같이 추출해보았습니다. 모든 검색 조건을 만족한다는 메서드명으로 BooleanExpression을 반환하고, 이를 where절에 담기만 하면 됩니다.


총 세 가지를 검증합니다.

  1. 채널이 속하는지. - 이 조건은 항상 적용되어야 합니다.
  2. 검색어가 포함되는지. - 이 조건은 검색어가 전달됐을 때만 적용되어야 합니다.
  3. 메시지 ID 또는 날짜 기준을 만족하는지 - 이 조건도 전달되었을 때아만 적용되어야 합니다.

다만 3번의 경우 다소 복잡하여 아래에 추가로 정리합니다.

3-1. 메시지 아이디만 전달됐을 때 - 메시지 아이디 기준 적용
3-2. 날짜만 전달됐을 때 - 날짜 기준 적용
3-3. 둘 다 전달됐을 때 - 메시지 아이디 기준 적용


중요한 것은 2,3 조건은 해당 값이 전달되었을 때에만 적용되어야한다는 점입니다. 이 내용들이 어떻게 적용되는지 살펴보겠습니다.



동적 쿼리 상세

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;
}

  • 채널 ID 조건은 항상 검증되어야 하므로 즉시 검증하는 BooleanExpression을 return합니다.
  • 반면 textContains조건은 keyword가 hasText 할 때에만 적용되어야 합니다.
    • StringUtils.hasText는 스프링에서 제공하는 유틸 메서드입니다.
    • null도 아니어야 하고, 공백이 아닌 유의미한 문자가 하나라도 있어야 참을 반환합니다.
  • keyword값이 유효한지 확인하고, 유효할때는 contains로 표현되는 BooleanExpression을 반환합니다.
  • keyword값이 유효하지 않을 경우, 즉 전달되지 않았을 경우엔 null을 반환합니다.
  • jpaQueryFactory를 이용해 QueryDSL을 다룰 때의 동적인 쿼리의 장점이 여기에서 부각됩니다.
  • BooleanExpression에 null이 반환될 경우, JPQL생성 시 이 조건은 애초에 적용된 적 없는 것처럼 무시됩니다.



Clone this wiki locally