QueryDsl 소개

강정우·2024년 1월 27일
0

JPA

목록 보기
6/12

QueryDsl

SQL문의 문제점

우리가 백엔드 어플리케이션에서 작성하는 SQL문은 엄연히 문자열이다.
즉, 이는 code assistant가 되지 않는다. 이런걸 type-safe라 한다.

이정도면 감이 왔을 텐데 query를 Java로 type-safe하게 작성할 수 있도록 지원하는 프레임웤이 바로 QueryDsl이라는 것이다.

주로 JPA query(JPQL)에 사용한다.

문제

나이: 20~40
성: 강
정렬: 나이 많은 순
3명 을 출력하라

라고 할 때 JPA에서 data를 query하는 방법은 크게 3가지가 있었다.

1. JPQL (HQL)

@Test
public void jpql(){
	String query = "select m from Member m " +
    				"where m.age between 20 and 40 "+
                    "and m.name like '강%' "+
                    "order by m.age desc";
    List<Member> resultList = entityManager.createQuery(query, Member.class).setMaxResult(3).getResultList();
}

장점: SQL Query와 비슷하여 학습 곡선이 낮다.
단점: type-safe하지 않아 동적 쿼리 생성이 복잡하다.

2. Criteria API

@Test
public void jpaCriteriaQuery(){
	CriteriaBuiolder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<Member> cq = cb.createQuery(Member.class);
    Root<Member> root = cq.from(Member.class);

	Path<Integer> age = root.get("age");
    Predicate between = cb.between(age, 20, 40);
    
    Path<String> path = root.get("name");
    Predicate like = cb.like(path, "강%");
    
    CriteriaQuery<Member> query = cq.where(cb.and(between, like));
    query.orderBy(cb.desc(age));
    
    List<Member> resultList = entityManager.createQuery(query).setMaxResult(3).getResultList();
}

장점: 동적 쿼리 생성이 쉽다. (딱히 아님)
단점: 한눈에 안 들어온다, type-safe아님, 복잡함, 학습곡선이 JPQL에 비해 높다.

3. MetaModel Criteria API(type-safe)

root.get("age") -> root.get(Member_.age)
Criteria API + MetaModel
Criteria API와 거의 동일
type-safe
복잡하긴 마찬가지

싹다 별로라서 탄생한 것이 바로 QueryDSL 되시겠다.

QueryDSL이란?

우선 DSL에 대해 먼저 알아보자면

DSL이란?

도메인 + 특화 + 언어
특정한 도메인에 초점을 맞춘 제한적인 표현력을 가진 컴퓨터 프로그래밍 언어
특징: 단순, 간결, 유창

QeuryDSL

쿼리 + 도메인 + 특화 + 언어
쿼리에 특화된 프로그래밍 언어
단순, 간결, 유창
다양한 저장소 쿼리 기능 통합

진짜 개념이 미친게 query문 자체를 추상화 해보자! 이다.
즉, 모두 다른 DB에 해대서 query문을 추상화해보자는 것이 최초의 개념이다.

-> JPA, MongoDB, SQL 같은 기술들을 위해 type-safe SQL을 만드는 프레임웤이다.

물론 type-safe한 query를 작성하려면 코드가 필요하다.

JPA 경우는 xxxEntity 혹은 Table에서 정보를 뽑아내서 "코드 생성기" 가 돈다.
그래서 "Qxxx.java" 즉, Qeury용 해당 객체를 만들어준다.

그럼 "코드 생성기" 이게 APT: Annotation Processing Tool이라고 JPA 기준 @Entity를 읽어서 Qxxx.java라는 객체를 만들어준다.
그래서 그걸로 JAVa query를 작성할 수 있다.

4. QeuryDSL

JPAQeuryFactory query = new JPAQueryFactory(entityManager);
QMember m = QMemeber.member;

List<Member> list = query
	.select(m)
    .from(m)
    .where(
    	m.age.between(20, 40).and(m.name.like("강%"))
    )
    .orderBy(m.age.desc())
    .limit(3)
    .fetch(m);

그럼 위에서 나온 질문이 이렇게 작성할 수 있는 것이다. 뭔가 sql문과 굉장히 유사하다는 것을 볼 수 있다.

이렇게 queryDSL을 작성하면 JPQL문을 만들어준다. 즉, JPQL builder 역할을 하는 것이다.

장점: type-safe, 단순, 쉬움, 컴파일 에러
단점: Q코드 생성을 위한 APT를 세팅해야한다.

정리
SpringDataJPA + QueryDsl
SpringData project의 약점은 항상 조회였다.
이를 QueryDsl로 복잡한 조회 기능(동적 쿼리 등)을 보완하였다.
단순한 경우엔 : SpringDataJPA
복잡한 경우엔 : QueryDsl 직접 사용
JPQL로 해결이 안 되는 경우엔 : JdbcTemplate, MyBatis 사용

QueryDsl setting

build.gradle에 의존성 추가

참고로 spring 버전, gradle 버전, query DSL 버전 마다 각각 설정법이 다르게 때문에 그때마다 서칭하여 맞춰넣으면 된다.

dependencies {
	//Querydsl 추가
	implementation 'com.querydsl:querydsl-jpa'
	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
	annotationProcessor "jakarta.annotation:jakarta.annotation-api"
	annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
	delete file('src/main/generated')
}

검증 - Q 타입 생성 확인

  1. setting에 들어가서 gradle을 검색 혹은 아래 경로대로 가기
Preferences -> Build, Execution, Deployment -> Build Tools -> Gradle
  1. Gradle이면 Gradle, IntlliJ IDEA면 IntlliJ IDEA로 동일하게 맞추기

1. Gradle일 때

혹은 명령어로 하고싶다면

./gradlew clean compileJava
  • 참고: Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋다.
    gradle 옵션을 선택하면 Q타입은 gradle build 폴더 아래에 생성되기 때문에 여기를 포함하지 않아야 한다.
    대부분 gradle build 폴더를 git에 포함하지 않기 때문에 이 부분은 자연스럽게 해결된다.

Q타입 삭제

gradle clean 을 수행하면 build 폴더 자체가 삭제된다. 따라서 별도의 설정은 없어도 된다.

2. IntlliJ IDEA일 때

그냥 xxxApplication 즉, 메인 메서드를 그냥 실행해주면 위 사진과 같이 generated 폴더가 생긴다.
혹은 단축키 cmd + f9 로 그냥 빌드를 해버리면 된다.

그리고 이땐 gradle에서 .gitIgnore를 추가해주지 않기 때문에 수동으로 설정해주면 된다.

.gitignore

/src/main/generated/

QueryDsl 사용

@Repository
@Transactional
public class JpaItemRepositoryV3 implements ItemRepository {
    private final EntityManager em;
    private final JPAQueryFactory query;
    public JpaItemRepositoryV3(EntityManager em) {
        this.em = em;
        this.query = new JPAQueryFactory(em);
    }

    @Override
    public Item save(Item item) {
        em.persist(item);
        return item;
    }
    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        Item findItem = findById(itemId).orElseThrow();
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }
    @Override
    public Optional<Item> findById(Long id) {
        Item item = em.find(Item.class, id);
        return Optional.ofNullable(item);
    }
    public List<Item> findAllOld(ItemSearchCond itemSearch) {
        String itemName = itemSearch.getItemName();
        Integer maxPrice = itemSearch.getMaxPrice();
        // QItem item = new QItem("i");
        QItem item = QItem.item;
        BooleanBuilder builder = new BooleanBuilder();
        if (StringUtils.hasText(itemName)) {
            builder.and(item.itemName.like("%" + itemName + "%"));
        }
        if (maxPrice != null) {
            builder.and(item.price.loe(maxPrice));
        }
        List<Item> result = query
                .select(item)
                .from(item)
                .where(builder)
                .fetch();
        return result;
    }
    @Override
    public List<Item> findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();
        List<Item> result = query
                .select(item)
                .from(item)
                .where(likeItemName(itemName), maxPrice(maxPrice))
                .fetch();
        return result;
    }
    private BooleanExpression likeItemName(String itemName) {
        if (StringUtils.hasText(itemName)) {
            return item.itemName.like("%" + itemName + "%");
        }
        return null;
    }
    private BooleanExpression maxPrice(Integer maxPrice) {
        if (maxPrice != null) {
            return item.price.loe(maxPrice);
        }
        return null;
    }
}

각각의 point

  1. 위 코드의 생성자 함수에서 this.query = new JPAQueryFactory(em); : 앞서 QueryDsl은 jpql을 만들어내는 빌더 역할을 한다고 하였다. 따라서 그냥 "jpa-query를 만들어주는 공장이다." 라고 생각하면 편하다.

    • Querydsl을 사용하려면 JPAQueryFactory 가 필요하다. JPAQueryFactory 는 JPA 쿼리인 JPQL을 만들기 때문에 EntityManager 가 필요하다.
      설정 방식은 JdbcTemplate 을 설정하는 것과 유사하다.
      참고로 JPAQueryFactory 를 스프링 빈으로 등록해서 사용해도 된다.
  2. QueryDsl 문에 .fetch() 메서드를 쓰면 뒤에 리스트가 반환된다.

Qxxx class 생성자

원래는 QItem item = new QItem("i"); 이렇게 alias까지 해서 가져왔어야 하는데 생성된 Qxxx class를 들어가보면 내부적으로 생성이 되어있기 때문에 그냥 QItem item = QItem.item; 이렇게 short version으로 사용하면 된다.

그리고 편리하게 단축키 opt + enter 로 static import 를 해주면 된다.

동적쿼리

new BooleanBuilder() 바로 얘를 사용해서 동적쿼리를 작성하면 된다.

builder.and : sql문에서 AND 역할을 한다고 생각하면 된다.
Qxxxclass.속성값.loe() : loe는 low or equal 이라고 생각하면 된다. 얘가 어디서 튀어나오나면 위 tab 순서대로 타고타고 가다보면 각종 loe 는 물론이고 우리가 편리하게 사용할 수 있는 수와 관련된 많은 메서드들이 존재한다.

findAllOld VS findAll

앞서 findAllOld 에서 작성한 코드를 깔끔하게 리팩토링 했다. 다음 코드는 누가 봐도 쉽게 이해할 수 있을 것이다.

List<Item> result = query
 .select(item)
 .from(item)
 .where(likeItemName(itemName), maxPrice(maxPrice))
 .fetch(); 

Querydsl에서 where(A,B) 에 다양한 조건들을 직접 넣을 수 있는데, 이렇게 넣으면 AND 조건으로 처리된다.
참고로 where() 에 null 을 입력하면 해당 조건은 무시한다.
이 코드의 또 다른 장점은 likeItemName() , maxPrice() 를 다른 쿼리를 작성할 때 재사용 할 수 있다는 점이다.
쉽게 이야기해서 쿼리 조건을 부분적으로 모듈화 할 수 있다. 자바 코드로 개발하기 때문에 얻을 수 있는 큰 장점이다

Querydsl.config 작성

@Configuration
@RequiredArgsConstructor
public class QuerydslConfig {
    private final EntityManager em;

    @Bean
    public ItemService itemService() {
        return new ItemServiceV1(itemRepository());
    }

    @Bean
    public ItemRepository itemRepository() {
        return new JpaItemRepositoryV3(em);
    }
}
  • 예외 변환
    참고로 Querydsl 은 별도의 스프링 예외 추상화를 지원하지 않는다. 대신에 JPA에서 학습한 것 처럼 @Repository 에서 스프링 예외 추상화를 처리해준다.

Querydsl 장점

Querydsl 덕분에 동적 쿼리를 매우 깔끔하게 사용할 수 있다.
쿼리 문장에 오타가 있어도 컴파일 시점에 오류를 막을 수 있다.
메서드 추출을 통해서 코드를 재사용할 수 있다.
예를 들어서 여기서 만든 likeItemName(itemName), maxPrice(maxPrice) 메서드를 다른 쿼리에서도 함께 사용할 수 있다.
Querydsl을 사용해서 자바 코드로 쿼리를 작성하는 장점을 느껴보았을 것이다. 그리고 동적 쿼리 문제도 깔끔하게 해결해보았다.
Querydsl은 이 외에도 수 많은 편리한 기능을 제공한다. 예를 들어서 최적의 쿼리 결과를 만들기 위해서 DTO로 편리하게 조회하는 기능은 실무에서 자주 사용하는 기능이다.
JPA를 사용한다면 스프링 데이터 JPA와 Querydsl은 실무의 다양한 문제를 편리하게 해결하기 위해 선택하는 기본 기술이라 생각한다.

profile
智(지)! 德(덕)! 體(체)!

0개의 댓글