-
Notifications
You must be signed in to change notification settings - Fork 6
메시지 조회 API 개발 상세 QueryDSL
yeonLog edited this page Nov 12, 2022
·
5 revisions
- 여러 검색 조건을 동적으로 적용해야 하는 요구사항이 발생했습니다.
- Spring Data JPA 기술만으로 대응하기 어렵다고 느껴졌습니다.
- 메시지 조회 API 개발 전까지 도입되지 않았고, 도입한 당사자들도 아주 익숙하지는 않은 기술이어서 정리해봅니다.
- 관련 커밋은 여기를 참고해주세요
- 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를 생성자 주입받아 사용할 수 있습니다.
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
Entity 클래스에 변경이 생겼을 경우 compileQuerydsl 을 실행해야 추후 코딩 시 컴파일 시 변경된 내용이 적용됩니다.
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절에 담기만 하면 됩니다.
총 세 가지를 검증합니다.
- 채널이 속하는지. - 이 조건은 항상 적용되어야 합니다.
- 검색어가 포함되는지. - 이 조건은 검색어가 전달됐을 때만 적용되어야 합니다.
- 메시지 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생성 시 이 조건은 애초에 적용된 적 없는 것처럼 무시됩니다.