[Spring] QueryDSL을 적용해보자

HwangDo·2023년 12월 15일
0

SpringBoot

목록 보기
8/14
post-thumbnail
post-custom-banner

몇 달 전, 스프링을 처음 사용 할 때 했던 프로젝트를 열어보았다.

동적 쿼리를 위해 StringBuilder를 쓰다보니, 쿼리를 봤을때 뭐하는 쿼리인지 도저히 알아 볼 수 없었다.
따라서 QueryDSL을 도입해보자 한다.

추가로 QueryDSL 적용시 컴파일 타임 검사가 들어가 Type 체크나, 오타등의 여지도 걸러 줄 수 있다.

설정

내 생각에 QueryDSL에서 가장 어려운건 세팅이다.

buildscript {
	ext {
		queryDslVersion = "5.0.0"
	}
}
plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.13'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
	id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" //추가 된 부분
}

...

	implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
	implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
    
...
tasks.named('test') {
	useJUnitPlatform()
}

def querydslDir = "$buildDir/generated/querydsl"

querydsl { 
	jpa = true
	querydslSourcesDir = querydslDir
}

sourceSets { 
	main.java.srcDir querydslDir
}

configurations { 
	compileOnly {
		extendsFrom annotationProcessor
	}
	querydsl.extendsFrom compileClasspath
}

compileQuerydsl { 
	options.annotationProcessorPath = configurations.querydsl
}

먼저, build.gradle로 가서 implementation을 추가해주자.

이후, Gradle-other 탭에 가서 compileQuerydsl을 실행하자.

그러면 build 디렉토리 안에 이렇게 엔티티마다 Q 클래스가 생성됨을 볼 수 있다.

@Configuration
public class QuerydslConfig {
	
	@PersistenceContext
	private EntityManager entityManager;
	
	@Bean
	public JPAQueryFactory jpaQueryFactory(){
		return new JPAQueryFactory(entityManager);
	}
}

이제 config 파일을 하나 만들어주자. JPAQueryFactory를 빈으로 등록해서, 레포지토리에서 주입받아 쓰자.

@PersistenceContext는, EntityManager를 주입받을 때 사용해야 한다는데...?

의존성 주입은 보통 @Autowired를 사용하지 않는가?

스택 오버플로우를 찾아보니, EntityManager는 thread-safe 하지 않는다고 한다.
즉, 어떤 호출마다 스레드가 생성되고, 그 스레드는 각각의 entitymanager를 가져야 한다. 근데 만약 일반 빈처럼 하나의 EntityManager를 공유하게 된다면.. 다른 사용자들이 하나의 엔티티를 공유하는 대 참사가 발생한다. 따라서 추가된 JPA 표준 Annotation이다.

적용


출처 : https://jojoldu.tistory.com/372

만약 스프링 데이터 JPA를쓰고 있었다면, 위 링크를 참조하길 바란다. 나의 경우엔 모든 Repository를 JPQL만으로 썼다.

SearchRepositoryCustom

public interface SearchRepositoryCustom {
	List<SearchDTO> findResultByLowPrice(SearchForm searchForm);
}

그래도 인터페이스 형태로 미리 구성해둬 나중의 편의를 챙기기로 했다.
먼저, Custom 인터페이스를 만들었다. QueryDSL을 쓰기 위해선 강제로 인터페이스를 적극 활용하게 되니 좋다,,

SearchRepositoryCustomImpl

네이밍은 무조건 Impl로 해야 JPA가 인식한다.

@Override
    public List<SearchDTO> findResultByLowPrice(SearchForm searchForm) {
        return queryFactory.select(
                Projections.constructor(
                    SearchDTO.class,
                    p.placeName,
                    p.placeAddress,
                    p.placeRating,
                    p.placeLink,
                    p.placeDistance,
                    p.school,
                    m.id,
                    m.menuName,
                    m.menuPrice,
                    m.menuImg,
                    new CaseBuilder().when(b.menu.isNull()).then(false).otherwise(true)
                )
           )
            .from(m)
            .join(m.place, p)
            .leftJoin(b).on(b.menu.eq(m).
                and(b.userId.eq(searchForm.getUserId())))
            .where(p.school.eq(searchForm.getSchool())
                .and(m.menuPrice.between(searchForm.getMinimumPrice(), searchForm.getMaximumPrice())),
                searchKeyword(searchForm))
            .orderBy(orderMethod(searchForm))
            .offset((searchForm.getPage() - 1) * elementCountInPage)
            .limit(elementCountInPage)
            .fetch();
    }

위 함수가 QueryDSL을 적용한 버전이다.
Projections.constructor로 DTO로 바로 매핑해줬다.
그리고 가장 큰 발전은 다음 부분이라고 생각한다.

     StringBuilder jpqlBuilder = new StringBuilder();
       for (int i = 0; i < searchForm.getSearchKeywordList().size(); i++) {
           if (i > 0) {
               jpqlBuilder.append(" OR ");
           }
           jpqlBuilder.append("m.menuName LIKE :searchString").append(i);
       }

기존에는 사용자가 입력한 키워드에 대해 검색을 하려면, 위와 같이 string을 이어 붙였어야 한다. 굉장히 직관적이지 않다. 이런 코드는 조금만 길어지더라도 고치는 것보다 새로 작성하는게 빠를 것 같다. 그리고 이렇게 더하는 방법을 쓰기 위해, 검색어가 있을 경우의 쿼리를 따로 만들어야 해서 관리도 힘들었다.

 private BooleanBuilder searchKeyword(SearchForm searchForm){
        if (searchForm.getSearchKeywordList() == null || searchForm.getSearchKeywordList().size() == 0)
            return null;
        BooleanBuilder builder = new BooleanBuilder();
        for(String keyword : searchForm.getSearchKeywordList()){
            builder.or(m.menuName.contains(keyword));
        }
        return builder;
    }

그러나 위 코드를 통해, 최소한 위 StringBuilder보단 유지 관리와 가독성이 향상되었다.

 and m.menuPrice between :minValue and :maxValue order by + searchform().makeSortResult() ... 

또한 기존에는 동적으로 정렬 방법을 정하기 위해, 이를 입력 받는 DTO에 위임시켰었다. 추후 유지 보수를 하려면, 직접 저 클래스까지 찾아가서 함수를 봐야 할 것이다.

private OrderSpecifier<Integer> orderMethod(SearchForm searchForm){
        switch (searchForm.getSortMethod()) {
            case "lowPrice":
                return new OrderSpecifier<>(Order.ASC, m.menuPrice);
            case "highPrice":
                return new OrderSpecifier<>(Order.DESC, m.menuPrice);
            case "distance":
                return new OrderSpecifier<>(Order.ASC, p.placeDistance);
        }
        return null;
    }

저 부분을 OrderSpecifier를 통해 코드의 형태로 바꿨다.
참고로, OrderSpecifier의 타입은 정렬 기준의 타입이다. 내가 정렬할 타입인 Price, Distance는 모두 Integer였다. 문자열이면 String, 날짜 형태면 LocalDateTime등을 넣어주면 된다.
만약 이도 저도 아니라면, 그냥 OrderSpecifier<?> 의 꼴로 와일드 카드를 넣자.


정리

기존 프로젝트에 QueryDSL을 도입해보며, 확실히 가독성을 정말 많이 챙긴 것 같다. String 형태의 JPQL에서, 코드를 기반으로 하도록 바뀌었으니 수정하기도 편할 듯 하다. 복잡했던 동적 쿼리를 더 낫게 고친 것 같아 좋다.
아직 문법이 익숙하지 않지만, 사용하면서 QueryDSL의 장점을 더 살려보자.

profile
제가 배워가는 내용과, 실수한 부분을 정리합니다
post-custom-banner

0개의 댓글