코드 저장소.

JPQL에서 QueryDSL을 활용한 동적쿼리 적용 본문

포폴/JPABlog

JPQL에서 QueryDSL을 활용한 동적쿼리 적용

slown 2024. 5. 31. 22:10

목차

1.QueryDSL?
2.JPQL과의 차이점?
3.프로젝트의 적용

 

1.QueryDSL?

QueryDSL은 자바 기반의 동적 쿼리를 작성하는 데 사용되는 라이브러리입니다. 주로 JPA(Java Persistence API)와 함께 사용되며, 데이터베이스에 대한 쿼리를 자바 코드로 작성할 수 있도록 도와주는 라이브러리 입니다.

2.JPQL과의 차이점?

QueryDSL과 JPQL(JPA Query Language)은 둘 다 자바 표준인 JPA(Java Persistence API)를 사용하여 데이터베이스 쿼리를 작성하는 데에 사용되는 라이브러리입니다. 하지만 두 라이브러리 간에는 몇 가지 차이가 있습니다.

 

2-1.문법 및 표현 방법

JPQL: 문자열 기반의 쿼리 언어로, SQL과 유사하지만 엔터티와 필드를 대상으로 쿼리를 작성합니다. JPQL 쿼리는 문자열로 작성되기 때문에 컴파일 타임에 오류를 발견하기 어렵습니다.

QueryDSL: 자바 언어를 사용하여 쿼리를 작성하는 데에 중점을 둡니다. 코드로 작성되므로 컴파일 타임에 오류를 쉽게 찾을 수 있습니다. 또한 IDE 지원을 통해 코드 자동 완성 및 리팩토링 기능을 활용할 수 있습니다.

 

2-2.타입 안전성

JPQL: 문자열로 작성되기 때문에 컴파일러가 쿼리에 대한 타입을 확인할 수 없습니다. 따라서 실행 시점에 오류가 발생할 가능성이 있습니다.

QueryDSL: 자바 코드로 작성되기 때문에 타입 안전성을 보장합니다. IDE에서 코드 자동 완성 및 타입 체크를 지원하여 오류를 줄일 수 있습니다.

 

2-3.가독성

JPQL: 문자열로 작성되기 때문에 복잡한 쿼리의 경우 가독성이 떨어질 수 있습니다. 특히 동적인 쿼리를 작성할 때는 문자열 조합이 필요합니다.

QueryDSL: 자바 코드로 작성되므로 가독성이 향상될 수 있습니다. 코드의 구조를 활용하여 복잡한 쿼리도 보다 명확하게 작성할 수 있습니다.

 

2-4.코드 재사용 및 리팩토링

JPQL: 문자열 기반으로 코드 리팩토링이 어렵습니다. 코드 중복이 발생할 가능성이 있습니다.

QueryDSL: 자바 코드로 작성되어 코드 리팩토링이 용이합니다. 코드를 재사용하고 모듈화할 수 있어 유지보수가 편리합니다.

3.프로젝트의 적용

3-1. query dsl을 적용하기 위해서 build.gradle에 다음과 같이 작성을 합니다.

//querydsl
implementation "com.querydsl:querydsl-jpa"
implementation "com.querydsl:querydsl-core"
implementation "com.querydsl:querydsl-collections"

......

// Querydsl 설정부
def generated = 'src/main/generated'

// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
	options.getGeneratedSourceOutputDirectory().set(file(generated))
}

// java source set 에 querydsl QClass 위치 추가
sourceSets {
	main.java.srcDirs += [ generated ]
}

// gradle clean 시에 QClass 디렉토리 삭제
clean {
	delete file(generated)
}

tasks.named('test') {
	useJUnitPlatform()
}

targetCompatibility = JavaVersion.VERSION_14

 

3-2.builde.gradle 내에서 query dsl에 관한 설정을 적어 준 뒤 build를 합니다. build가 끝나면 옆의 사진과 같이 Q가 붙어있는 클래스가 생성 되는 것을 볼 수가 있습니다.

 

3-3. QueryDsl에 관련된 설정 클래스를 작성해야 합니다.

@Configuration
public class QuerydslConfig {
    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

 

3-4.Repository의 구조를 변경을 합니다.

  • 쿼리 메소드나 @Query 등으로 처리할 수 없는 기능은 별도의 인터페이스로 설계
  • 별도의 인터페이스에 대한 구현 클래스를 작성한다
  • 구현 클래스에 인터페이스의 기능을 Q도메인 클래스와 JPQLQuery를 이용해서 구현한다.

 

3-5.QueryDsl을 적용

 

BoardCustomReposiotry에 원하는 기능의 메소드를 작성을 합니다.

public interface BoardCustomRepository {
  //게시글 검색(카테고리 ,검색 ,정렬)
  Page<BoardDto.BoardResponseDto> findByAllSearch(String searchVal, SearchType searchType , Pageable pageable);
}

 

그 다음은 JPAQueryFactory를 이용해서 본격적으로 QueryDSL코드를 작성해보겠습니다.

@Log4j2
public class BoardCustomRepositoryImpl implements BoardCustomRepository{
    private final JPAQueryFactory jpaQueryFactory;

    QBoard qBoard;
    QMember qMember;
    QLike qLike;
    QCategory qCategory;
    QBoardHashTag qBoardHashTag;
    QHashTag qHashTag;

    //생성자 주입
    public BoardCustomRepositoryImpl(EntityManager em) {
        this.jpaQueryFactory = new JPAQueryFactory(em);
        qBoard = QBoard.board;
        qMember = QMember.member;
        qLike = QLike.like;
        qCategory = QCategory.category;
        qHashTag = QHashTag.hashTag;
        qBoardHashTag = QBoardHashTag.boardHashTag;
    }
.......	


	@Override
    public Page<BoardDto.BoardResponseDto> findByAllSearch(String searchVal, SearchType searchType, Pageable pageable) {
        JPQLQuery<BoardDto.BoardResponseDto>list = jpaQueryFactory
                .select(Projections.constructor(BoardDto.BoardResponseDto.class,qBoard))
                .from(qBoard)
                .leftJoin(qBoard.writer,qMember).fetchJoin()
                .leftJoin(qBoard.category,qCategory)
                .leftJoin(qBoard.likes,qLike)
                .leftJoin(qBoard.hashtags,qBoardHashTag)
                .distinct();

        JPQLQuery<BoardDto.BoardResponseDto>middleQuery = switch (searchType){
            //제목
            case TITLE -> list.where(titleCt(searchVal));
            //작성자
            case AUTHOR -> list.where(authorCt(searchVal));
            //내용
            case CONTENTS -> list.where(contentCt(searchVal));
            //ALL
            default ->list.where(titleCt(searchVal).or(authorCt(searchVal)).or(contentCt(searchVal)));
        };

        return PageableExecutionUtils
                .getPage(middleQuery
                        .orderBy(getAllOrderSpecifiers(pageable.getSort()).toArray(OrderSpecifier[]::new))
                        .offset(pageable.getOffset())
                        .limit(pageable.getPageSize())
                        .fetch(),
                        pageable,
                        middleQuery::fetchCount);
    }

}

 

  • JPQLQuery를 사용해서 게시글을 조회하는 쿼리를 작성을 합니다.
  • 중간 쿼리로 게시글을 검색을 할 때 검색어에 따른 쿼리를 switch문을 사용해서 작성을 합니다.
  • 마지막으로는 PageableExecutionUtils을 사용해서 페이징 처리와 정렬을 합니다.

3-6. 테스트 코드

마지막으로 검색기능이 잘 작동이 되는지를 확인하기 위해서 테스트 코드를 작성해 보았습니다.

    @Test
    @DisplayName("게시글 검색")
    public void boardSearchTest(){
        Pageable pageable = Pageable.ofSize(10);
        String keyword = "well4149";
        Page<BoardDto.BoardResponseDto>list = boardRepository.findByAllSearch(keyword, SearchType.toSearch("boardAuthor"),pageable);
        System.out.println(list.toList());
        assertThat(list);
        System.out.println("size::"+list.stream().count());
    }

 

테스트를 이용해보면 위와 같은 쿼리를 볼 수 있고 테스트가 통과 되는것을 볼 수 있습니다.

Hibernate: select 
                distinct 
                board0_.board_id as board_id1_0_0_, 
                member1_.id as id1_6_1_, 
                board0_.created_at as created_2_0_0_, 
                board0_.board_author as board_au3_0_0_, 
                board0_.board_contents as board_co4_0_0_, 
                board0_.board_title as board_ti5_0_0_, 
                board0_.category_id as category9_0_0_, 
                board0_.liked as liked6_0_0_, 
                board0_.board_pw as board_pw7_0_0_, 
                board0_.read_count as read_cou8_0_0_, 
                board0_.useridx as useridx10_0_0_, 
                member1_.created_at as created_2_6_1_, 
                member1_.membername as memberna3_6_1_, 
                member1_.password as password4_6_1_, 
                member1_.role as role5_6_1_, 
                member1_.useremail as useremai6_6_1_, 
                member1_.userid as userid7_6_1_ 
            from 
                board board0_ 
            left outer join 
                    member member1_ 
                on board0_.useridx=member1_.id 
            left outer join 
                    category category2_ 
                on board0_.category_id=category2_.category_id 
            left outer join 
                    likes likes3_ 
                on board0_.board_id=likes3_.board_id 
            left outer join 
                    boardhashtag hashtags4_ 
                on board0_.board_id=hashtags4_.board_id 
            where 
                lower(board0_.board_author) like ? escape '!' limit ?

Hibernate: select 
                hashtags0_.board_id as board_id1_1_2_, 
                hashtags0_.hashtag_id as hashtag_2_1_2_, 
                hashtags0_.board_id as board_id1_1_1_, 
                hashtags0_.hashtag_id as hashtag_2_1_1_, 
                hashtag1_.hashtag_id as hashtag_1_4_0_, 
                hashtag1_.created_at as created_2_4_0_, 
                hashtag1_.hashtag_name as hashtag_3_4_0_ 
            from 
                    boardhashtag hashtags0_ 
            inner join 
                    hashtag hashtag1_ 
            on 
                    hashtags0_.hashtag_id=hashtag1_.hashtag_id 
            where 
                    hashtags0_.board_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

Hibernate: select 
                category0_.category_id as category1_2_0_, 
                category0_.name as name2_2_0_, 
                category0_.parent_id as parent_i3_2_0_ 
            from 
                category category0_ 
            where 
                category0_.category_id=?
                
Hibernate: select 
                count(distinct board0_.board_id) as col_0_0_ 
            from 
                board board0_ 
         	left outer join 
                member member1_ 
         	on 
                board0_.useridx=member1_.id 
         	left outer join 
                category category2_ 
         	on 
              board0_.category_id=category2_.category_id 
         	left outer join 
                likes likes3_ 
         	on 
                board0_.board_id=likes3_.board_id 
         	left outer join 
                boardhashtag hashtags4_ 
         	on 
                board0_.board_id=hashtags4_.board_id 
         	where 
                lower(board0_.board_author) like ? escape '!'