오늘 한 일
오늘은 조건검색기능을 QueryDSL을 이용해서 만들었다.
자바 백엔드 기술은 Spring Boot와 Spring Data JPA를 함께 사용한다. 하지만, 복잡한 쿼리, 동적 쿼리를 구현하는 데 있어 한계가 있다. 이러한 문제점을 해결할 수 있는 것이 QueryDSL이다.
QueryDSL이 등장하기 이전에는 Mybatis, JPQL, Criteria 등 문자열 형태로 쿼리문을 작성하여 컴파일 시에 오류를 발견하는 것이 불가능했다.
하지만, QueryDSL은 자바 코드로 SQL 문을 작성할 수 있어 컴파일 시에 오류를 발생하여 잘못된 쿼리가 실행되는 것을 방지할 수 있다.
난 이 자바코드로 써서 코드에서 에러를 바로 찾아주는 부분이 정말 신세계 였다.
먼저 QueryDSL을 사용하기 위해서는 먼저 gradle을 추가해줘야한다.
// Spring boot 3.x이상에서 QueryDsl 패키지를 정의하는 방법
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
build.gradle
그리고 gradle아래에 이렇게 설정 코드도 작성해준다.
// === ⭐ QueryDsl 빌드 옵션 (선택) ===
def generated = 'src/main/generated'
// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(generated))
}
// java source set에 querydsl QClass 위치 추가
sourceSets {
main.java.srcDirs += [generated]
}
// gradle clean 시에 QClass 디렉토리 삭제
clean {
delete file(generated)
}
그리고 그 뒤 QueryConfig코드를 작성해준다.
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
이제 QueryDSL을 쓸 준비를 마쳤다 이제 코드로 사용해보자.
Repository쪽을 바꿔줘야하는데,
GroupBuying이라는 공동구매 프로젝트 제작시 실습했으므로 그 코드를 가져오겠다.
원래 사용하던 JpaRepository 뒤에 RepositoryQuery인터페이스를 만들어서 붙여준다.
쿼리 인터페이스 코드는 다음과 같이 작성해주고, 해당 내부 코드들은 이제 QueryDSL을 이용해 Impl클래스를 따로 만들어서 사용한다.
당연히 RepositoryQuery를 implements받아서 사용한다. 이제 아래에 함수들을 작성해줘야하는데
@Repository
@RequiredArgsConstructor
public class GroupBuyingRepositoryImpl implements GroupBuyingRepositoryQuery{
private final JPAQueryFactory jpaQueryFactory;
QGroupBuying qGroupBuying = QGroupBuying.groupBuying;
먼저 DB에 접근한 JPAQueryFactory를 선언해주고 Q객체를 받아온다.
그리고 바로 간단한 카테고리 검색 코드를 보자
public List<GroupBuying> findCategory(GroupBuyingCategoryEnum categoryEnum, Pageable pageable) {
BooleanExpression categoryPredicate = qGroupBuying.enumCategory.eq(categoryEnum);//Query로 검사한 Boolean값을 가져오는 듯 하다.
return jpaQueryFactory
.selectFrom(qGroupBuying)
.where(categoryPredicate) //여기서 조건검색
.fetch();
}
위에 보면 .eq가 보일텐데 해당 함수가 내부적으로 지원해주는 같은것들을 찾아주는 함수다 이것같은 경우는 카테고리가 같은 것을 검색해다.
@Override
public Page<GroupBuying> searchItemList(Pageable pageable,String keyword, GroupBuyingCategoryEnum category,
GroupBuyingShareEnum shareEnum, GroupBuyingStatusEnum statusEnum, String beobJeongDong, String sort) {
QueryResults<GroupBuying> results = jpaQueryFactory
.selectFrom(qGroupBuying)
.where(containsKeyword(keyword),categoryEq(category), statusEq(statusEnum),shareEq(shareEnum),addressEq(beobJeongDong)).orderBy(sort.equals("desc")? qGroupBuying.createdAt.desc():qGroupBuying.createdAt.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetchResults();
return new PageImpl<>(results.getResults(), pageable, results.getTotal());
}
이번코드는 여러 조건을 한번에 검사하는 코드인데 where에 ,를 찍으면 and연산으로 모두 맞는 자료만 가져오게 된다.
or을 쓰고 싶다면 중간에 or을 넣어주면 or연산을 수행할 수 있다. where안에 보이는 함수들은 아래쪽에 내부적eq함수들로 비교하도록 내가 만들어둔 함수들을 가져와 비교했다.
그중 특이한게 있는데,
@Override
public BooleanExpression containsKeyword(String keyword) {
if (keyword == null || keyword.trim().isEmpty()) {
return null;
}
return qGroupBuying.title.containsIgnoreCase(keyword);
}
이 containsIgnoreCase는 키워드를 검색하는 친구이다. 문자열을 검색해서 해당 값에서 문자열이 "포함"되어있다면 모두 가져온다.
우리가 흔히 생각하는 키워드 검색을 정말 편하게 구현할 수 있게 해주는 친구라고 할 수 있다.
아래는 완성 코드이다.
@Repository
@RequiredArgsConstructor
public class GroupBuyingRepositoryImpl implements GroupBuyingRepositoryQuery{
private final JPAQueryFactory jpaQueryFactory;
QGroupBuying qGroupBuying = QGroupBuying.groupBuying;
public List<GroupBuying> findCategory(GroupBuyingCategoryEnum categoryEnum, Pageable pageable) {
BooleanExpression categoryPredicate = qGroupBuying.enumCategory.eq(categoryEnum);//Query로 검사한 Boolean값을 가져오는 듯 하다.
return jpaQueryFactory
.selectFrom(qGroupBuying)
.where(categoryPredicate) //여기서 조건검색
.fetch();
}
@Override
public Page<GroupBuying> searchItemList(Pageable pageable,String keyword, GroupBuyingCategoryEnum category,
GroupBuyingShareEnum shareEnum, GroupBuyingStatusEnum statusEnum, String beobJeongDong, String sort) {
QueryResults<GroupBuying> results = jpaQueryFactory
.selectFrom(qGroupBuying)
.where(containsKeyword(keyword),categoryEq(category), statusEq(statusEnum),shareEq(shareEnum),addressEq(beobJeongDong)).orderBy(sort.equals("desc")? qGroupBuying.createdAt.desc():qGroupBuying.createdAt.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetchResults();
return new PageImpl<>(results.getResults(), pageable, results.getTotal());
}
@Override
public BooleanExpression containsKeyword(String keyword) {
if (keyword == null || keyword.trim().isEmpty()) {
return null;
}
return qGroupBuying.title.containsIgnoreCase(keyword);
}
private BooleanExpression addressEq(String beobJeongDong) {
if (beobJeongDong == null) {
return null;
}
return QGroupBuying.groupBuying.beobJeongDong.eq(beobJeongDong);
}
private BooleanExpression categoryEq(GroupBuyingCategoryEnum category) {
if (category == null) {
return null;
}
return QGroupBuying.groupBuying.enumCategory.eq(category);
}
private BooleanExpression statusEq(GroupBuyingStatusEnum category) {
if (category == null) {
return null;
}
return QGroupBuying.groupBuying.enumStatus.eq(category);
}
private BooleanExpression shareEq(GroupBuyingShareEnum category) {
if (category == null) {
return null;
}
return QGroupBuying.groupBuying.enumShare.eq(category);
}
}
내일 할일
이제 정말 마무리 단계다 다 온것 같다.
슬슬 코드를 리팩토링 해보겠다.