본문 바로가기
web/springboot

[springboot/jpa] QueryDSL(spring-data-jpa)

by fien 2021. 3. 3.

QueryDSL 목차

- 특징

- QueryDSL 설정

- QueryDSL 사용

- Spring Data JPA + QueryDSL

- QueryDslPredicateExecutor

- QueryDslReposiotrySupport

- 동적쿼리

QueryDSL

특징

Querydsl의 핵심 원칙은 타입 안정성(Type safety)이다. 도메인 타입의 프로퍼티를 반영해서 생성한 쿼리 타입을 이용해서 쿼리를 작성하게 된다. 또한, 완전히 타입에 안전한 방법으로 함수/메서드 호출이 이루어진다.

또 다른 중요한 원칙은 일관성(consistency)이다. 기반 기술에 상관없이 쿼리 경로와 오퍼레이션은 모두 동일하며, Query 인터페이스는 공통의 상위 인터페이스를 갖는다.

모든 쿼리 인스턴스는 여러 차례 재사용 가능하다. 쿼리 실행 이후 페이징 데이터와 프로젝션 정의는 제거된다.

QueryDSL 설정

pom.xml

  1. 의존성 추가
  2. 빌드 플러그인 추가
<dependencies>
	...
	<!-- 의존성 추가 -->
   <dependency>
       <groupId>com.querydsl</groupId>
       <artifactId>querydsl-apt</artifactId>
       <version>${querydsl.version}</version>
       <scope>provided</scope>
   </dependency>

   <dependency>
       <groupId>com.querydsl</groupId>
       <artifactId>querydsl-jpa</artifactId>
       <version>${querydsl.version}</version>
   </dependency>
	...
</dependencies>
...
<build>
     <plugins>
         ...
         <!-- 플러그인 추가 -->
		<plugin>
             <groupId>com.mysema.maven</groupId>
             <artifactId>apt-maven-plugin</artifactId>
             <version>1.1.3</version>
             <executions>
                 <execution>
                     <goals>
                         <goal>process</goal>
                     </goals>
                     <configuration>
                         <outputDirectory>target/generated-sources/java</outputDirectory>
                         <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                     </configuration>
                 </execution>
             </executions>
         </plugin>
				...
     </plugins>
   </build>

메이븐 패키징을 하면 target/generated-source/java 하위 폴더에 Qdomain이 생성된 것을 확인 할 수 있다. Qdomain은 QueryDSL에서 사용하는 쿼리 타입이다.

기본 설정을 했으니 querydsl을 적용 해보자.

 

QueryDSL 사용

@Test
public void querydsl() throws Exception{
    Book book = Book.createBook("제목입니다","작가","123",200);
    bookRepository.save(book);
    //1. JPAQueryFactory에 EntityManager 주입
	// (빈 컨테이너에 등록 해놓고 DI 받아 사용하면 편함)
	JPAQueryFactory queryFactory = new JPAQueryFactory(em);

	//2. JPAQueryFactory 메소드 체이닝을 이용해 쿼리 작성
	// 이 때 Qdomain 인스턴스를 사용하다.
    QBook b = QBook.book;
    Book findBook = queryFactory
            .select(b)
            .from(b)
            .where(b.title.containsIgnoreCase("입")
                .and(b.author.startsWithIgnoreCase("작")))
            .fetchOne();

    assertEquals(findBook.getTitle(),book.getTitle());
}

vs JPQL

@Test
public void jpql() { 
	Book book = Book.createBook("제목입니다","작가","123",200);
  bookRepository.save(book);
	String query = "select b from Book b" +
				   " where b.title like %:title% " +
				   " or b.author like :author% ")
	String title = "입";
	String author ="작"; 
	Book findBook = em.createQuery(query, Book.class)
						.setParameter("title", title)
						.setParameter("author", author)
					    .getSingleResult(); 
	assertEquals(findBook.getTitle(),book.getTitle()); 
}

⇒ JPQL은 쿼리문을 문자열로 작성하기 때문에 실행 시점에 오류를 발견 할 수 있는 반면 QueryDSL은 메서드 호출 방식으로 컴파일 시점에 발견 할 수 있다.

Spring Data JPA + QueryDSL

Spring Data JPA은 두 가지 방식으로 querydsl을 지원한다

위의 방식으로 별도로 사용하는 것이아니라 아래의 방식을 이용하면 SpringDataJPA가 제공하는 paging 등을 같이 사용 할 수 있음

  1. QueryDslPredicateExecutor

    JPA Repository와 Querydsl의 Predicate를 결합해 사용, join, fetch를 사용할 수 없다

  2. QueryDslRepositorySupport

    service layer에 Querydsl을 침투시키지 않고 사용 가능

QueryDslPredicateExecutor

1. Repository에 QuerydslPredicateExecutor<{domain}>를 상속받는다

@Repository
public interface BookRepository extends JpaRepository<Book,Long>,
        QuerydslPredicateExecutor<Book> {
	
	//Spring Data JPA로 쿼리 정의하는 방식 (QueryDSL 사용 X)
	// 1. 메서드 네이밍을 통해 쿼리를 작성하는 방식
	List<Book> findAllByTitle(String title);

	// 2. @Query 어노테이션을 이용해 직접 JPQL을 작성하는 방식
	("select b from Book b where b.title like %?1% " +
            " or b.author like %?1% or b.isbn like %?1% ")
    Page<Book> findAllBySearch(String search, Pageable pageable);
	
}

QuerydslPredicateExecutor 를 보면 Predicate를 매개변수로 사용한다.

기존의 controller - service - repository 구조를 유지한 채로 Predicate를 이용해 쿼리만 쉽게 다룰 수 있다.

Predicate는 표현식으로 정의되는 쿼리라고 보면 될듯

public interface QuerydslPredicateExecutor<T> {
	
	Optional<T> findOne(Predicate predicate);

	Iterable<T> findAll(Predicate predicate);

	Iterable<T> findAll(Predicate predicate, Sort sort);

	Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);

	Iterable<T> findAll(OrderSpecifier<?>... orders);

	Page<T> findAll(Predicate predicate, Pageable pageable);

	long count(Predicate predicate);

	boolean exists(Predicate predicate);
}

2. Predicate를 통해 쿼리를 작성한다.

3. repository 메소드 인자로 predicate를 넘겨서 조회한다.

(Pageable를 통해 Page 객체를 반환 받을 수 도 있음)

@Test
public void querydsl() throws Exception{
    //given
    Book book = Book.createBook("제목입니다","작가","123",200);
    bookRepository.save(book);
    //when
    Predicate predicate =  QBook.book.title.containsIgnoreCase("입")
            .and(QBook.book.author.startsWithIgnoreCase("작"));
    Optional<Book> findBook = bookRepository.findOne(predicate);
    //then
    assertEquals(findBook.orElseThrow(()->new EntityNotFoundException()).getId(),book.getId());
}

QueyrDSL을 이용하면 Repository 인터페이스에서 메서드명이나 @Query 어노테이션을 이용해 쿼리를 만드는 방식에 비해 가독성이 좋다. 또한, 특정 쿼리마다 Repository 메소드를 작성 하지 않아도 된다.

 

QueryDslRepositorySupport

이 방식은 custom repository를 이용해 구현한다.

  1. Domain에 해당하는 Domain Repository interface(BookRepository)를 생성한다.

  2. Domain Repository interface에서 SpringDataJPA가 제공하는 인터페이스(ex. JpaRepository)를 상속 받는다.

    (여기까지는 SpringDataJPA를 적용하는 단계)

  3. Custom Repository interface(BookRepositoryCustom)를 생성. 구현할 메서드 명세를 작성한다

  4. BookRepository에서 BookRepositoryCustom를 상속 받는다.

  5. Custom Repository interface를 구현한 Custom Repository Implement class(BookRepositoryImpl)를 만든다

  6. Custom Repository Implement classQueyrDslRepositorySupport를 상속 받는다.

  7. BookRepositoryImpl에서 기본 생성자로 도메인 클래스를 넘겨주고 메서드를 구현한다.

@Repository
public interface BookRepository 
	extends JpaRepository<Book,Long>,BookRepositoryCustom {
}
public interface BookRepositoryCustom {
    Book findAllBySearch(String search);
}
public class BookRepositoryImpl
    extends QuerydslRepositorySupport implements BookRepositoryCustom {
    //기본 생성자를 만들어줘야함
    public BookRepositoryImpl() {
        super(Book.class);
    }

    
    public Book findAllBySearch(String search) {
        QBook book = QBook.book;
        return from(book)
                .where(book.title.containsIgnoreCase(search))
                .select(book)
                .fetchOne();
    }
}

Custom Repository Implement class의 이름이 중요한데 Custom Repository interface 이름 + Impl 이나 Custom Repository interface 상속받은 Domain Repository interface +Impl 로 지어야 한다.

⇒ 어플리케이션을 실행하면 Spring Data JPA가 Domain Repository interface에 해당하는 프록시 객체를 만들고 기본적인 CRUD 작업은 JpaRepository의 구현체인 SimpleJpaRepository에 위임해서 처리하고 Querydsl 작업은 Custom Repository Implement class에 위임해서 처리한다.

@Test
public void querydsl() throws Exception{
    //given
    Book book = Book.createBook("제목입니다","작가","123",200);
    bookRepository.save(book);
    //when
    Book findBook = bookRepository.findAllBySearch("목");
		//구현체인 BookRepositoryImpl이 위임받아 처리한다.
    //then
    assertEquals(findBook.getTitle(),book.getTitle());
}

디버그를 돌려보면 실제로 확인할 수 있다. 정확한 내부 플로우는 이해 못하지만 대강 그렇구나 하고 생각함.. ㅎㅎ

 

동적 쿼리

QueryDsl의 또 다른 장점은 동적 쿼리를 쉽게 생성할 수 있다는 것이다. JPQL을 직접 다루는 경우는 문자열 처리를 해야 하기 때문에 오류를 범하기 쉽지만 QueryDSL은 BooleanBuilder를 통해 이를 쉽게 처리한다.

예를 들면 검색 조건에 고르는 경우이다.

 

제목이 체크 되어 있으면 where 제목 like XXX

저자가 체크 되어 있으면 and 저자 like XXX

를 추가하고 체크가 해제된 항목은 추가하지 않는다.

 

이처럼 동적 쿼리를 작성해야 하는데.. 널체크를 하고 jpql 문자열을 직접 다룬다고 생각하면 굉장히 복잡하고 오류를 범하기 쉽다.

 

1. BooleanBuilder 방식

public Book findAllBySearch(String title,String author) {
    QBook book = QBook.book;
    BooleanBuilder builder = new BooleanBuilder();
    if(!StringUtils.isEmpty(title)) builder.and(book.title.like(title));
    if(!StringUtils.isEmpty(author)) builder.and(book.author.like(author));
    return from(book)
            .where(builder)
            .select(book)
            .fetchOne();
}

 

2. BooleanExpression 방식

BooleanBuilder를 사용하면 jpql보다는 깔끔하지만 여전히 쿼리 형태를 예측하기 어렵다.
조건절에 표현하되 파라미터가 생략되어있으면 조건절에서도 생략된다.
BooleanExpression을 사용하면 쿼리 예측이 훨씬 쉽다

@Override
public Book findAllBySearch(String title,String author) {
	QBook book = QBook.book;
	return from(book)
		.where(titleLike(title),
			authorLike(author))
		.select(book)
		.fetchOne();
}

private BooleanExpression titleLike(String title){
	if(StringUtils.isEmpty(title)) return null;
	return QBook.book.title.containsIgnoreCase(title);
}

private BooleanExpression authorLike(String author){
	if(StringUtils.isEmpty(author)) return null;
	return QBook.book.title.containsIgnoreCase(author);
}

 

참고 https://joont92.github.io/jpa/Spring-Data-JPA/

 

댓글