Mybatis 버전의 방명록 애플리케이션( 링크 )을 JPA 버전으로 만드는 주제입니다.





QueryDSL

QueryDSL은 정적 타입을 이용해서 SQL과 같은 쿼리를 생성할 수 있도록 하는 프레임워크입니다.

JPQL처럼 문자열로 작성하거나 Mybatis처럼 XML 파일에 쿼리를 작성하는 대신,

Querydsl이 제공하는 Fluent API를 이용해서 쿼리를 생성할 수 있습니다.

즉 JPA에서 사용했던 SQL과 비슷하게 생긴 JQPL조차 사용하지 않습니다.

그렇기 때문에 오류 발생률도 적죠.


단순 문자열( JDBC, Mybatis, JPQL )과 비교해서 Fluent API( QueryDSL )를 사용할 때의 장점은 다음과 같습니다.

1) IDE의 코드 자동 완성 기능 사용

2) 문법적으로 잘못된 쿼리를 허용하지 않음

3) 도메인 타입과 property를 안전하게 참조할 수 있음

4) 도메인 타입의 리팩토링을 더 잘 할 수 있음


QueryDSL은 타입에 안전한 방식으로 쿼리를 실행하기 위한 목적으로 만들어졌습니다.

즉, QueryDSL의 핵심 원칙은 타입 안정성( Type safety )입니다.

그것이 가능한 이유는 문자열이 아닌 메서드 호출로 쿼리가 수행되기 때문입니다.


ex)   query.from().where()

이와 같이 메서드를 호출하여 쿼리를 수행합니다.

from() 메서드는 SQL에서 from 절이고, where() 메서드는 where절입니다.


QueryDSL에 대한 더 많은 정보를 이곳을 참고해주세요 ( 링크 )




이번 글에서는 JPQL을 사용해서 만들었던 방명록 애플리케이션을 QueryDSL 버전으로 수정하려고 합니다.

프로젝트 폴더를 만드는 것부터 진행하는 것은 무의미하다고 생각되어, 환경 설정 부분은 이 글로 대체 하고 QueryDSL을 사용하기 위해 필요한 부분만 언급하도록 하겠습니다.

그리고 JPQL 버전과 QueryDSL 버전을 비교하여 눈으로 비교해보고, 추가적인 QueryDSL 사용법을 알아보겠습니다.





환경 설정

환경 설정은 전반적으로 위에서 링크를 걸었던 글과 전반적으로 일치합니다.

추가적인 다음은 다음과 같습니다.


1. 프로젝트 생성 할 때 소스폴더 추가

dynamic 프로젝트 폴더를 생성할 때 소스폴더를 추가하는 화면에서 아래 사진과 같이 tartget/generated-sources/java 소스폴더를 생성합니다.





2. 라이브러리 추가

pom.xml

<!-- queryDSL -->

<dependency>

        <groupId>com.mysema.querydsl</groupId>

        <artifactId>querydsl-jpa</artifactId>

        <version>3.6.2</version>

</dependency>

<dependency>

        <groupId>com.mysema.querydsl</groupId>

        <artifactId>querydsl-apt</artifactId>

        <version>3.6.2</version>

        <scope>provided</scope>

</dependency>

의존성에는 위의 두 라이브러리를 추가하고,

<build> 태그의 <plugins> 아래에 플러그인을 추가합니다.

<plugin>

         <groupId>com.mysema.maven</groupId>

         <artifactId>apt-maven-plugin</artifactId>

         <version>1.0.9</version>

         <executions>

                  <execution>

                           <goals>

                                   <goal>process</goal>

                           </goals>

                           <configuration>

                                   <outputDirectory>target/generated-sources/java</outputDirectory>

                                   <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>

                           </configuration>

                  </execution>

         </executions>

</plugin>



나머지 applicationContext.xml , spring-serlvet.xml , web.xml , views 폴더 , src/main/java 소스 폴더에 있는 패키지들을 그대로 복사하면 target/generated-sources/java 소스폴더에 패키지와 클래스가 생성됩니다.



QGuestbook가 갑자기 생긴 이유는 src/main/java 소스폴더의 com.victolee.guestbook.domain 패키지 때문입니다.

QGuestbook 클래스는 QueryDSL query type 입니다.

쉽게 QueryDSL을 사용하기 위해서는 QGuestbook 클래스를 사용해야 한다고 생각하면 됩니다.




**     pom.xml 파일의 <execution>에서 에러 발생     **

만약 pom.xml 파일 <build>의 <execution>에서 오류가 발생한다면,

( You need to run build with JDK or have tools.jar on the classpath.If this occures during eclipse build make sure you run eclipse under JDK as well  )


이클립스가 설치된 경로( 저 같은 경우는 C:\Users\samsung\eclipse )로 가서 elipse.ini 파일을 다음과 같이 수정합니다.



-vm에 JDK가 설치된 경로로 작성하면 됩니다. ( JDK 경로는 다를 것입니다. )


이클립스를 껏다 켜도 오류가 발생한다면 프로젝트 폴더를 우클릭 하여,

Java Build Path , Java Compiler , Project Facets 탭에서 JDK 버전을 맞춰줍니다.





JPQL  ->  QueryDSL

JPQL 버전을 QueryDSL 버전으로 바꾸기 위해서는 GeustbookRepository 클래스만 수정하면 됩니다.

아래는 JPQL 버전의 GuestbookRepository 클래스입니다.

@Repository
public class GuestbookRepository {
	
	@PersistenceContext 	// EntityManagerFactory가 DI 할 수 있도록 어노테이션 설정
	private EntityManager em;
	
	public List<Guestbook> findAll(){
		String jpql = "SELECT gb FROM Guestbook gb ORDER BY gb.regDate DESC";
		TypedQuery<Guestbook> query = em.createQuery(jpql, Guestbook.class);
		return query.getResultList();
	}
	
	public void save(Guestbook guestbook) {
		em.persist(guestbook);
	}

	public boolean remove(Guestbook guestbook) {
		String jpql = "SELECT gb from Guestbook gb WHERE gb.no = :no AND gb.pwd = :pwd";
		TypedQuery<Guestbook> query = em.createQuery(jpql, Guestbook.class);
		query.setParameter("no", guestbook.getNo());
		query.setParameter("pwd", guestbook.getPwd());
		
		List<Guestbook> guestbookList = query.getResultList();
		if( guestbookList.size() != 1 ) {
			return false;
		}
		
		em.remove(guestbookList.get(0));
		return true;
	}
}



1. findAll() 메서드

[ JPQL ]

public List<Guestbook> findAll(){
	String jpql = "SELECT gb FROM Guestbook gb ORDER BY gb.regDate DESC";
	TypedQuery<Guestbook> query = em.createQuery(jpql, Guestbook.class);
	return query.getResultList();
}


[ QueryDSL ]

public List<Guestbook> findAll(){
	JPAQuery query = new JPAQuery(em);
	QGuestbook qguestbook = new QGuestbook("guestbook");
	
	List<Guestbook> guestbookList
		= query.from(qguestbook)
					.orderBy(qguestbook.regDate.desc())
					.list(qguestbook);
	return guestbookList;
}

JPQL의 FROM 절은 QueryDSL의 form() 메서드로, ORDER BY 절은 orderBy() 메서드로 대체 되었습니다.

그리고 list() 메서드는 조회 결과를 List로 반환하라는 의미입니다.


QGuestbook 클래스는 target/generated-sources/java 소스폴더 아래에 있는 QGuestbook 클래스입니다.

QGuestbook 클래스를 열어보시면, static으로 미리 QGuestbook 객체를 guestbook이라는 변수로 만들어 놓았습니다.



따라서 예제와 같이 new라는 키워드로 QGuestbook 객체를 생성하지 않고 QueryDSL이 미리 만들어 놓은 객체를 사용하는 것이 좋습니다.

GuestbookRepository 클래스에서 import를 해주면 아래와 같이 수정이 가능합니다.

import static com.victolee.guestbook.domain.QGuestbook.guestbook;


public List<Guestbook> findAll(){
	JPAQuery query = new JPAQuery(em);

	List<Guestbook> guestbookList
				= query.from(guestbook)
							.orderBy(guestbook.regDate.desc())
							.list(guestbook);
	return guestbookList;
}





2. remove() 메서드

[ JPQL ]

public boolean remove(Guestbook guestbook) {
	String jpql = "SELECT gb from Guestbook gb WHERE gb.no = :no AND gb.pwd = :pwd";
	TypedQuery<Guestbook> query = em.createQuery(jpql, Guestbook.class);
	query.setParameter("no", guestbook.getNo());
	query.setParameter("pwd", guestbook.getPwd());
	
	List<Guestbook> guestbookList = query.getResultList();
	if( guestbookList.size() != 1 ) {
		return false;
	}
	
	em.remove(guestbookList.get(0));
	return true;
}


[ QueryDSL ]

public boolean remove(Guestbook paramGuestbook) {
	JPAQuery query = new JPAQuery(em);
	List<Guestbook> guestbookList
					= query.from(guestbook)
								.where(guestbook.no.eq(paramGuestbook.getNo())
								.and(guestbook.pwd.eq(paramGuestbook.getPwd())))
								.list(guestbook);
	if( guestbookList.size() != 1 ) {
		return false;
	}
	
	em.remove(guestbookList.get(0));
	return true;
}

SQL의 WHERE 절은 where() 메서드로 대체됩니다.

where절 내부도 워낙 직관적이기 때문에 따로 설명은 필요하지 않을 것 같습니다.



이것으로 JPQL 버전 방명록을 QueryDSL 버전으로 바꿔보았습니다.

톰캣을 실행 해보시면 정상적으로 작동하며, 콘솔에 출력되는 쿼리도 같을 것입니다.


QueryDSL의 장점은 메서드 호출만으로 쿼리가 실행될 수 있기 때문에 오류의 가능성이 적고, IDE의 도움으로 빠른 작성이 가능합니다.





QueryDSL 더 알아보기

QueryDSL은 SQL을 메서드로 바꿀 수 있기 때문에 많은 메서드들이 존재합니다.

그래서 여러가지 메서드를 추가적으로 더 알아보려고 하며, 이 부분은 방명록과 상관이 없는 부분입니다.


1. SELECT
1) where and
List<Book> bookList
				= query.from(book)
							.where(book.title.eq("토비의 스프링")
							.and(book.price.lt(50000)))
							.list(book);

List<Book> bookList
				= query.from(book)
							.where(book.title.eq("토비의 스프링"), book.price.lt(50000))
							.list(book);

and는 콤마(,)로 대체가 가능합니다.



2) where between

List<Book> bookList
				= query.from(book)
							.where(book.price.between(15000, 18000))
							.list(book);



3) where contains

List<Book> bookList
				= query.from(book)
							.where(book.title.contains("리눅스"))
							.list(book);



4) where startsWith

List<Book> bookList
				= query.from(book)
							.where(book.title.startsWith("토비"))
							.list(book);





2. 결과 반환

1) uniqueResult()

조회 결과가 1개일 때 사용합니다.

조회 결과가 없으면 null 을 반환하고, 조회 결과가 2개 이상이면 com.mysema.query.NonUniqueResultException 예외가 발생합니다.

이 메서드는 결과가 1개인 것이 명확할 때 사용하면 좋습니다.

Integer maxPrice = query.from(book).uniqueResult(book.price.max());



2) singleResult()

uniqueResult() 와 같지만 결과가 하나 이상이면 처음 데이터를 반환합니다.



3) list()

결과가 하나 이상일 때 사용하며, 결과가 없으면 빈 컬렉션을 반환합니다.


하나의 칼럼에 대해서 리스트로 가져오기

List<String> bookList = query.from(book).list(book.title);



여러 개의 칼럼을 리스트로 가져오기

public void queryDSL (EntityManager em) {
	JPAQuery query = new JPAQuery(em);
	List<Tuple> list = query.from(book).list(book.title, book.price);
		
	for(Tuple item : list) {
		System.out.println(item.get(book.title) + " : " + item.get(book.price));
	}
}



DTO 사용하기 - Projections.bean()

// as를 통해 별칭을 줄 수도 있다.
List<BookDTO> list
				= query.from(book)
							.list(Projections.bean(BookDTO.class, book.no.as("no"), book.title));


DTO 사용하기 - Projections.fields()

List<BookDTO> list
				= query.from(book)
							.list(Projections.fields(BookDTO.class, book.no.as("no"), book.title));

Projections.bean() 메서드는 DTO의 setter를 사용해서 값을 세팅하는 반면,

Projections.fields() 메서드는 setter가 없어도 값을 세팅합니다.



DTO 사용하기 - Projections.constructor()

List<BookDTO> list
				= query.from(book)
							.list(Projections.constructor(BookDTO.class, book.no.as("no"), book.title));

DTO의 생성자를 호출 하는 방법입니다.

이 때 DTO 클래스에는 프로젝션의 순서와 일치하는 생성자가 오버로딩 되어 있어야 합니다.





3. 페이징 - offset() , limit() ,  listResults()

public static void paging (EntityManager em) {
	int page = 2;
	JPAQuery query = new JPAQuery(em);
	
	SearchResults<Book> results = query.from(book).offset((page-1)*5).limit(5).listResults(book);
	
	Long totalCount = results.getTotal();
	Long offset = results.getOffset();
	Long limit = results.getLimit();
	System.out.println(totalCount);
	System.out.println(offset);
	System.out.println(limit);
	
	for(Book book : results.getResults()) {
		System.out.println(book.getTitle());
	}
}
5개 씩 조회하는 페이징 중 2페이지를 의미합니다.
즉 6 ~ 10번의 글을 보여줍니다.
listResults() 메서드를 사용하면 전체 개수 및 limit와 offset 등 다양한 정보들을 함께 조회 할 수 있으므로 list() 메서드를 사용하는 것보다 유용합니다.





4. 정렬 - orderBy()

public static void ordering(EntityManager em) {
	JPAQuery query = new JPAQuery(em);
	List<Book> bookList = query.from(book).orderBy(book.price.desc()).list(book);
		
	for(Book book : bookList) {
		System.out.println(book.getTitle());
	}
}

정렬은 orderBy() 메서드를 호출하여 매개변수로 정렬 기준을 작성합니다.





5. 조인

아직 엔티티간의 관계를 맺는 방법에 대해서 알아보지 않았지만,

QueryDSL을 정리할 때 같이 정리를 하려 합니다.


1) join()

List bookList
				= query.from(book)
								.join(book.category, category).list(book);



2) join on

List<Book> bookList
				= query.from(book)
								.join(book.category, category)
								.on(category.name.like("%토비%")).list(book);


3) equi join

List<Book> bookList
				= query.from(book, category)
								.where(book.category.eq(category))
								.list(book);




이상으로 JPQL 버전의 방명록 애플리케이션을 QueryDSL 버전으로 수정해보았고,

추가적으로 기본적인 QueryDSL을 알아보았습니다.

서두에서 언급했던 링크를 참고하시면 더 많은 정보가 있으므로 참고하시길 바랍니다.


이후의 글에서는 JPA를 깊이 있게 이해하기 위한 내용과 고급 주제( persistence context , 연관관계 매핑 , 상속 , proxy , 지연로딩 , Spring Data JPA )들을 다룰 것입니다.

방명록 애플리케이션은 Spring Data JPA 버전으로 수정할 때 다시 사용될 것입니다.

실무에서는 Spring Data JPA를 사용하므로 꼭 알아야 하는 기술입니다.

댓글 펼치기 👇
  1. Favicon of https://shinsunyoung.tistory.com 해어린 2020.11.03 12:29 신고

    안녕하세요! 블로그 늘 잘 보고 있습니다. 다름이 아니라 질문이 있어 댓글을 남기게 되었습니다.
    현재 저는 위 게시글에 나와있는대로 Projections bean 또는 field를 사용하여 모든 요청마다 필요한 컬럼만 select 하는 식으로 구현을 해두었는데
    이런식으로 구현을 하나 보니까 관리포인트도 많아지고 mybatis처럼 사용하는 것 같다는 느낌이 들었습니다.
    혹시 현업에서도 위와 같이 Projections bean을 많이 사용하나요?

    • Favicon of https://victorydntmd.tistory.com victolee 우르르응 2020.11.14 14:06 신고

      java 업무를 하고 있지 않아서 확실한 답은 드릴수 없지만..
      객체는 bean으로 다루는 것이 일반적이므로 projections bean을 사용해도 무방해보입니다.
      사용성은.. 사실 query dsl을 얼마나 사용할까 싶네요. 대부분 JpaRepository 인터페이스를 상속하여 처리가 되니까요.
      복잡한 쿼리는 JPQL로 직접 작성하는듯합니다!

      정확한 답변을 못드려서 죄송하네요 ㅠㅠ 확실하진 않으니 참고만 해주시면 좋을것 같아요!