[스프링 DB 2편] - QueryDSL

Chooooo·2023년 3월 18일
0

스프링 DB 2편

목록 보기
8/8

QueryDSL 소개 - 기존 방식의 문제점

기존 query의 문제점

  • query는 문자이므로 Type-check 불가
  • 실행하기 전까지 작동 여부 확인 불가(런타임 에러)
String sql = "select * from member" +
"where name like ?" +
"and age between ? and ?" // 공백이 없어 오류
// = select * from memberwhere name like ?and age between ? and ?"

⚽ 만약 SQL이 클래스처럼 타입이 있고 자바 코드로 작성할 수 있다면 type-safe이다.

QueryDSL

Query(쿼리) + Domain(도메인) + Specific(특화) + Language(언어)

QueryDSL은 쿼리를 JAVA로 type-safe하게 개발할 수 있게 지원하는 프레임워크이다. 주로 JPA쿼리(JPQL)에 사용한다.

  • 쿼리에 특화된 프로그래밍 언어
  • 단순, 간결, 유창
  • 다양한 저장소 쿼리 기능 통합
  • 런타임 에러x 컴파일 에러o

QueryDSL 설정

build.gradle
QueryDSL로 추가된 부분

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

"com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" 은 한줄로 붙어있는코드이다.

//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
delete file('src/main/generated')
}

검증 - Q타입 생성 확인 방법

Preferences → Build, Execution, Deployment → Build Tools → Gradle
여기에 가면 크게 2가지 옵션을 선택할 수 있다. 참고로 옵션은 둘다 같게 맞추어 두자.
1. Gradle: Gradle을 통해서 빌드한다.
2. IntelliJ IDEA: IntelliJ가 직접 자바를 실행해서 빌드한다.

옵션 선택1 - Gradle-Q타입 생성 확인 방법

Gradle IntelliJ 사용법

  • Gradle -> Tasks -> build -> clean
  • Gradle -> Tasks -> other -> compileJava

Q 타입 생성 확인
build -> generated -> sources -> annotationProcessor -> java/main 하위에 hello.itemservice.domain.QItem 이 생성되어 있어야 한다.

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

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

옵션 선택2 - IntelliJ IDEA - Q타입 생성 확인 방법

Build -> Build Project 또는
Build -> Rebuild 또는
main() 또는 테스트를 실행하면 된다.
src/main/generated 하위에
hello.itemservice.domain.QItem 이 생성되어 있어야 한다



QueryDSL 적용

JpaItemRepositoryV3

import static hello.itemservice.domain.QItem.*;
@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 = 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;
    }
}

⚽ 공통

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

save(), update(), findById()

기본 기능들은 JPA가 제공하는 기본 기능을 사용한다.

⚽ findAllOld

  • QueryDsl을 사용해서 동적 쿼리 문제를 해결한다.
    BooleanBuilder 를 사용해서 원하는 where 조건들을 넣어주면 된다.
    이 모든 것을 자바 코드로 작성하기 때문에 동적 쿼리를 매우 편리하게 작성할 수 있다

⚽ 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() 를 다른 쿼리를 작성할 때 재사용 할 수 있다는 점이다. 쉽게 이야기해서 쿼리 조건을 부분적으로 모듈화 할 수 있다. 자바 코드로 개발하기 때문에 얻을 수 있는 큰 장점이다.

설정 후 실행

⚽ QuerydslConfig

import javax.persistence.EntityManager;
@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덕분에 동적 쿼리를 매우 깔끔하게 사용할 수 있다.

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

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

profile
back-end, 지속 성장 가능한 개발자를 향하여

0개의 댓글