1.  Querydsl을 이용한 동적 쿼리 처리

데이터베이스를 이용해야 할 때 JPA나 JPQL을 이용하면 SQL을 작성하거나 쿼리를 처리하는 소스 부분이 줄어들기 때문에 무척 편리하지
어노테이션을 이용해서 지정하기 때문에 고정된 형태라는 단점이 있음
  ✓  예를 들어 Board의 경우 '검색' 기능이 필요한데 '제목 / 내용 / 작성자'와 같은 단일 조건이 생성되는 경우도 있지만 '제목과 내용, 제목과 작성자'와 같이 복합적인 검색 조건이 생길 수 있음. 만일 여러 종류의 검색 조건이 생기면 모든 경우의 수를 별도의 메소드로 작성을 해야함.

이러한 문제의 근본 원인은 JPQL이 정적으로 고정되기 때문. JPQL은 코드를 통해서 생성해야 하는데 이 문제를 해결하기 위해 다양한 방법이 존재하지만 국내에서 가장 많이 사용되는 방식은 Querydsl

 

📍 Querydsl 이란?

    - JPQL을 코드로 작성할 수 있도록 도와주는 빌더 API

    - 장점 

        1. 고정된 sql문이 아닌 조건에 맞게 동적으로 쿼리 생성

        2. 비슷한 쿼리를 재사용 가능. 제약 조건 조립 및 가독성 향상

        3. 문자열이 아닌 자바 소스코드로 작성하기 때문에 컴파일 시점에 오류를 발견할 수 있음

        4. IDE의 도움을 받아서 자동 완성 기능을 이용할 수 있기 때문에 생산성을 향상시킬 수 있음

💡  엄밀하게 말하면 'Querydsl'은 JPA의 구현체인 Hibernate 프레임워크가 사용하는 HQL Hibernate Query Language 을 동적으로 생성할 수 있는 프레임워크지만 JPA를 지원
    ➡️  Q도메인이라는 존재가 필요한데 Q도메인은 Querydsl의 설정을 통해서 기존의 엔티티 클래스를 Querydsl에서 사용하기 위해서 별도의 코드로 생성하는 클래스

 

 


1) Querydsl을 사용하기 위한 프로젝트 설정 변경

Querydsl을 이용하기 위해서는 build.gradle 설정을 변경
dependencies에 Querydsl 관련 라이브러리들을 추가

// querydsl 추가

implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
// === QueryDsl 빌드 옵션 (선택) ===
// 마지막 부분에 추가
def querydslDir = "$buildDir/generated/querydsl"

sourceSets {
	main.java.srcDirs += [ querydslDir ]
}

tasks.withType(JavaCompile) {
	options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}

clean.doLast {
	file(querydslDir).deleteDir()
}

 

Querydsl의 설정 확인


Querydsl의 설정이 올바르게 되었는지 확인하는 방법은 Q도메인 클래스가 정상적으로 만들어지는지 확인.
프로젝트 내에 Gradle 메뉴를 열어서 'other'부분을 살펴보면 compileJava task가 존재하는데 이를 실행.

complileJava가 실행되면 build 폴더에 QBorad 클래스가 생성되는 것을 볼 수 있음.

 

 


2)  기존의 Repository와 Querydsl 연동하기

📌  Querydsl을 기존 코드에 연동하기 위해서는 다음과 같은 과정으로 작성

  • Querydsl을 이용할 인터페이스 선언
  • '인터페이스 + Impl'이라는 이름으로 클래스를 선언 - 이때 QuerydslRepositorySupport라는 부모 클래스를 지정하고 인터페이스를 구현
  • 기존의 Repository에는 부모 인터페이스로 Querydsl을 위한 인터페이스 지정
repository 패키지에 search 하위 패키지를 추가하고 BoardSearch라는 인터페이스를 선언
public interface BoardSearch {
    Page<Board> search1(Pageable pageable);
}

 

실제 구현 클래스는 반드시 '인터페이스 이름 + Impl'로 작성
public class BoardSearchImpl extends QuerydslRepositorySupport implements BoardSearch {
    public BoardSearchImpl() {
        super(Board.class);
    }

    @Override
    public Page<Board> search1(Pageable pageable) {
        return null;
    }
}

 

마지막으로 기존의 BoardRepository의 선언부에 BoardSearch 인터페이스를 추가로 지정
public interface BoardRepository extends JpaRepository<Board, Long>, BoardSearch {
    @Query(value="select now()", nativeQuery = true)
    String getTime();
}

 

A.  Q도메인을 이용한 쿼리 작성 및 테스트


  🚀  Querydsl의 목적은 '타입'기반으로 '코드'를 이용해서 JPQL 쿼리를 생성하고 실행하는 것
         ➡️  이때 코드를 만드는 클래스가 Q도메인 클래스

BoardSearchImpl에서 Q도메인을 이용하는 코드를 작성
    @Override
    public Page<Board> search1(Pageable pageable) {

        QBoard board = QBoard.board; // Q도메인 객체
        JPQLQuery<Board> query = from(board); // select .. from board
        query.where(board.title.contains("1")); // where title like
        
        List<Board> list = query.fetch();
        long count = query.fetchCount();
        return null;
        
    }

 

  ✓  search1()은 아직 완성된 코드는 아니지만 Q도메인을 어떻게 사용하고 JPQLQuery라는 타입을 어떻게 사용하는지 보여줌
  ✓  JPQLQuery는 @Query로 작성했던 JPQL을 코드를 통해서 생성할 수 있게 함. 이를 통해서 where나 group by 혹은 조인 처리 등이 가능.
  ✓  JPQLQuery의 실행은 fetch()라는 기능을 이용하고, fetchCount()를 이용하면 count쿼리를 실행할 수 있음

 

아직은 null을 반환하지만 Querydsl을 이용해서 코드를 통한 JPQL의 쿼리를 확 인하기 위해 테스트 코드 작성
    @Test
    public void testSearch1() {
        Pageable pageable = PageRequest.of(1, 10, Sort.by("bno").descending());
        boardRepository.search1(pageable);
    }

 


B.  Querydsl로 Pageable 처리 하기


Querydsl의 실행 시에 Pageable을 처리하는 방법은 BoardSearchImpl이 상속한 QuerydslRepositorySupport라는 클래스의 기능을 이용

search1()의 코드 중간에 getQuerydsl()과 applyPagination()을 적용
@Override
    public Page<Board> search1(Pageable pageable) {

        QBoard board = QBoard.board; // Q도메인 객체
        JPQLQuery<Board> query = from(board); // select .. from board
        query.where(board.title.contains("1")); // where title like
        
        // paging
        this.getQuerydsl().applyPagination(pageable, query);
        
        List<Board> list = query.fetch();
        long count = query.fetchCount();
        return null;
        
    }

 

 

  ✓  applyPagination()이 적용된 후에 실행되는 쿼리의 마지막에는 페이징 처리에 사용하는 limit 가 적용되는 것 확인

 

 

 

 

 

 

 


3)  Querydsl로 검색 조건과 목록 처리

검색의 경우 '제목(t), 내용(c), 작성자(w)'의 조합을 통해서 이루어진다고 가정하고 이를 페이징 처리와 함께 동작하도록 구성

 

A. BooleanBuilder


예를 들어 '제목이나 내용'에 특정한 키워드 keyword가 존재하고 bno가 0보다 큰 테이터를 찾는다면 SQL을 다음과 같이 작성.

select * from board where (title like concat('%', '1', '%') or content like concat('%', '1', '%')) and bno > 0

 

  📍  where 조건에 and와 or이 섞여 있을 때는 연산자의 우선 순위가 다르기 때문에 or 조건은 ()로 묶어서 하나의 단위를 만들어 주는 것이 좋음
  📍  Querydsl을 이용할 때 '()'가 필요한 상황이라면 BooleanBuilder를 이용해서 작성할 수 있음

@Override
public Page<Board> search1(Pageable pageable) {
    QBoard board = QBoard.board; // Q도메인 객체
    JPQLQuery<Board> query = from(board); // select .. from board
    //query.where(board.title.contains("1")); // where title like

    BooleanBuilder booleanBuilder = new BooleanBuilder(); // (
    booleanBuilder.or(board.title.contains("11")); // title like
    booleanBuilder.or(board.content.contains("11")); // content like

    query.where(booleanBuilder);
    query.where(board.bno.gt(0L));

    // paging
    this.getQuerydsl().applyPagination(pageable, query);

    List<Board> boards = query.fetch();
    long count = query.fetchCount();
    return null;
}

 

  ✓  앞의 코드에는 '제목 혹은 or 내용'에 대한 처리를 BooleanBuilder에 or()을 이용해서 추가하고 있고, 'bno가 0보다 큰' 조건은 바로 JPQLQuery 객체에 적용되고 있음.
  ✓  코드를 실행하게 되면 다음과 같은 쿼리가 실행되는데 '()'가 생성되어서 or 조건들을 하나로 묶어서 처리하는 것을 볼 수 있음

 


B.  검색을 위한 메서드 선언과 테스트


  🚀  검색을 위해서는 적어도 검색 조건들과 키워드가 필요하므로 이를 types와 keyword로 칭하고 types은 여러 조건의 조합이 가능하도록 처리하는 메서드를 BoardSearch에 추가

public interface BoardSearch {
    Page<Board> search1(Pageable pageable);
    Page<Board> searchAll(String[] types, String keyword, Pageable pageable);
}

 

  🚀  BoardSearchImpl에서 searchAll의 반복문과 제어문을 이용한 처리가 가능. 검색 조건을 의미하는 types는 '제목(t), 내용(c), 작성자(w)'로 구성된다로 가정하고 이를 반영해서 코드 작성

@Override
public Page<Board> searchAll(String[] types, String keyword, Pageable pageable) {
    QBoard board = QBoard.board;
    JPQLQuery<Board> query = from(board);

    if ((types != null && types.length > 0) && keyword != null) { // 검색 조건과 키워드가 있다면
        BooleanBuilder booleanBuilder = new BooleanBuilder(); // (
        for (String type : types) {
            switch (type) {
                 case "t":
                     booleanBuilder.or(board.title.contains(keyword));
                     break;
                 case "c":
                     booleanBuilder.or(board.content.contains(keyword));
                     break;
                 case "w":
                     booleanBuilder.or(board.writer.contains(keyword));
                     break;
             }
         } // end for
         query.where(booleanBuilder);
    }// end if

    // bno > 0
    query.where(board.bno.gt(0L));

    // paging
    this.getQuerydsl().applyPagination(pageable, query);
    List<Board> list = query.fetch();
    long count = query.fetchCount();

    return null;
}

 

  🚀  아직 리턴 값은 null로 지정되어 있으므로 결과는 반환되지 않지만 테스트 코드를 작성해서 원하는 쿼리문이 실행되는지 확인

@Test
public void searchAll() {
    String[] types = {"t", "c", "w"};
    String keyword = "1";
    Pageable pageable = PageRequest.of(1, 10, Sort.by("bno").descending());
    Page<Board> result = boardRepository.searchAll(types, keyword, pageable);
}


C.  PageImpl을 이용한 Page<T> 반환


페이징 처리의 최종 결과는 Page<T> 타입을 반환하는 것이므로 Querydsl에서는 이를 직접 처리해야 하는 불편함이 있음
Spring Data JPA에서는 이를 처리하기 위해서 PageImpl이라는 클래스를 제공해서 3개의 파라미터로 Page<T>을 생성할 수 있음

  • List<T> : 실제 목록 데이터
  • Pageable : 페이지 관련 정보를 가진 객체
  • long : 전체 개수
BoardSearchImpl 클래스 내 searchAll()의 마지막 return null 부분에 반영해서 수정
// paging
this.getQuerydsl().applyPagination(pageable, query);
List<Board> list = query.fetch();
long count = query.fetchCount();

return new PageImpl<>(list, pageable, count);

 

BoardRepositoryTests 테스트 코드에서는 페이지 관련된 정보를 추출
@Test
public void searchAll() {
    String[] types = {"t", "c", "w"};
    String keyword = "1";
    Pageable pageable = PageRequest.of(1, 10, Sort.by("bno").descending());
    Page<Board> result = boardRepository.searchAll(types, keyword, pageable);
    
    // total pages
    log.info(result.getTotalPages());

    // page size
    log.info(result.getSize());

    // pageNumber
    log.info(result.getNumber());

    // prev next
    log.info(result.hasPrevious() + ": " + result.hasNext());

    result.getContent().forEach(board -> log.info(board));
}

 

 

  ✓  테스트 결과를 보면 키워드 '1'이라는 글자가 있는 총 페이지 수는 20이고, 이전페이지는 없지만 다음 페이지는 존재하는 것을 확인

 

 

 

 

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]

+ Recent posts