본문 바로가기
Programming/Study

Querydsl 사용해보기 - 검색 기능 만들기

by JKROH 2023. 11. 30.
반응형

 이번에 TDD 프로젝트의 검색 기능을 구현하면서 Querydsl을 처음으로 사용해보았다. 동적 쿼리 사용에 도움이 많이 된다는 말을 얼핏 들었고, 검색 기능을 구현함에 있어 동적 쿼리를 사용하는 것은 반드시 필요했기 때문이다.

 

 직접 사용해 본 후기로, Querydsl의 가장 큰 장점은 일단 상당히 가독성이 좋다는 것이다. JPQL로 일일이 SELECT, WHERE 절을 작성하는 것보다 함수 형태로 표현되기 때문에 훨씬 보기 좋았다. 또한, 다양한 기능을 어려움 없이 사용할 수 있다는 점도 좋았다. Specification을 사용한 검색 조건 지정 등은 SQL문법에 맞춰 LIKE 에 들어갈 조건을 작성하는 등 귀찮은 과정이 있었다. 그러나 Querydsl은 그런 과정이 없어 편리했다.

 

 각설하고, 이번에 Querydsl을 적용하는 과정에는 정말 많은 분들의 도움을 받았다. 구글은 신이고, 선배님들은 무적이다.

 

 Querydsl을 사용하기 위해서는 build.gradle에 의존성을 추가함은 물론 여러 설정도 거쳐야 한다. 특히 Querydsl의 버전이 스프링부트의 버전 및 자바 버전과 밀접한 연관이 있다고 하여 이 부분은 링크를 참조해 설정했다. 글 말미에 참조한 글들의 링크를 전부 걸어둘 예정이다. 위의 설정을 적용하고 프로젝트를 재빌드하면 아래와 같은 디렉토리에 이름 앞에 Q가 붙은 Q클래스들이 생긴다.

 

 예시를 위해 이번에 사용한 FreeBoard의 Q클래스 코드를 가져와보자.

/**
 * QFreeBoard is a Querydsl query type for FreeBoard
 */
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QFreeBoard extends EntityPathBase<FreeBoard> {

    private static final long serialVersionUID = -526829779L;

    private static final PathInits INITS = PathInits.DIRECT2;

    public static final QFreeBoard freeBoard = new QFreeBoard("freeBoard");

    public final tdd.groomingzone.domain.board.QBoard _super;

    //inherited
    public final ListPath<tdd.groomingzone.domain.comment.Comment, tdd.groomingzone.domain.comment.QComment> comments;

    //inherited
    public final StringPath content;

    //inherited
    public final DateTimePath<java.time.LocalDateTime> createdAt;

    //inherited
    public final NumberPath<Long> id;

    //inherited
    public final DateTimePath<java.time.LocalDateTime> modifiedAt;

    //inherited
    public final StringPath title;

    // inherited
    public final tdd.groomingzone.domain.member.QMember writer;

    public QFreeBoard(String variable) {
        this(FreeBoard.class, forVariable(variable), INITS);
    }

    public QFreeBoard(Path<? extends FreeBoard> path) {
        this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
    }

    public QFreeBoard(PathMetadata metadata) {
        this(metadata, PathInits.getFor(metadata, INITS));
    }

    public QFreeBoard(PathMetadata metadata, PathInits inits) {
        this(FreeBoard.class, metadata, inits);
    }

    public QFreeBoard(Class<? extends FreeBoard> type, PathMetadata metadata, PathInits inits) {
        super(type, metadata, inits);
        this._super = new tdd.groomingzone.domain.board.QBoard(type, metadata, inits);
        this.comments = _super.comments;
        this.content = _super.content;
        this.createdAt = _super.createdAt;
        this.id = _super.id;
        this.modifiedAt = _super.modifiedAt;
        this.title = _super.title;
        this.writer = _super.writer;
    }

}

 

 다양한 상수들이 선언되어있는데, 이후 구현 과정에서 사용할 것이다.

 

 다음으로 Querydsl을 사용하기 위한 Configuration 등의 설정은 해당 글을 참조하였다. 먼저 QueryDslConfig클래스를 생성해 @Configuration으로 등록하고 JPAQueryFactory @Bean을 등록해준다.

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

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

 

 다음으로, Querydsl을 사용해 데이터베이스를 검색할 CustomRepository Interface를 만들고, 이를 구현한 Impl클래스를 만든다. 구현에는 아마 수많은 학생들의 멘토가 반강제로 되고 계시는 동욱님의 글을 참조했다.

public interface FreeBoardCustomRepository {

    Page<FreeBoard> findFilteredFreeBoards(String title, String content, String writer, Pageable pageable);
}

/////////////////////////

@Repository
public class FreeBoardCustomRepositoryImpl implements FreeBoardCustomRepository {

    private final JPAQueryFactory jpaQueryFactory;

    public FreeBoardCustomRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
        this.jpaQueryFactory = jpaQueryFactory;
    }

    @Override
    public Page<FreeBoard> findFilteredFreeBoards(String title, String content, String writer, Pageable pageable) {
        List<FreeBoard> foundEntities = jpaQueryFactory.selectFrom(freeBoard)
                .where(containTitle(title),
                        containContent(content),
                        containWriter(writer))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        Long count = jpaQueryFactory.select(freeBoard.count())
                .from(freeBoard)
                .fetchOne();

        return new PageImpl<>(foundEntities, pageable, count);
    }

    private BooleanExpression containTitle(String title) {
        if (!StringUtils.hasText(title)) {
            return null;
        }
        return freeBoard.title.contains(title);
    }

    private BooleanExpression containContent(String content) {
        if (!StringUtils.hasText(content)) {
            return null;
        }
        return freeBoard.content.contains(content);
    }

    private BooleanExpression containWriter(String writer) {
        if (!StringUtils.hasText(writer)) {
            return null;
        }
        return freeBoard.writer.name.contains(writer);
    }
}

 

 코드만 읽어도 대충 어떤 값들이 들어오고 나올지 알 수 있지 않은가? 동욱님의 글과 다른 점은 StringUtils.isEmpty()가 Deprecate 되어서 hasText로 대체해서 사용했다는 점과, 검색 조건과 완전히 일치하는 것이 아닌 검색 조건을 포함하는 엔티티들을 검색하기 위해 eq()대신 contains()를 사용했다는 점이다. Pageable 적용에는 에디님의 글을 참고했다.

 

 테스트 코드도 작성했다. 역시 동욱님의 글을 참고했다.

@SpringBootTest
class FreeBoardRepositoryTest {

    @Autowired
    private FreeBoardRepository freeBoardRepository;

    @Test
    @DisplayName("동적 쿼리를 이용한 검색 조건에 따른 검색이 적절히 이루어지는지 테스트")
    void findFilteredFreeBoardTest(){
        String targetTitle = "targetTitle";
        String targetContent = "targetContent";
        freeBoardRepository.saveAll(Arrays.asList(
                FreeBoard.builder()
                        .title(targetTitle)
                        .content("content")
                        .build(),
                FreeBoard.builder()
                        .title("what")
                        .content("content")
                        .build(),
                FreeBoard.builder()
                        .title("non-target")
                        .content(targetContent)
                        .build()
        ));

        Pageable pageable = PageRequest.ofSize(20).withPage(0);

        Page<FreeBoard> filteredByTitleFreeBoards = freeBoardRepository.findFilteredFreeBoards(targetTitle,"","", pageable);
        Page<FreeBoard> filteredByContentFreeBoards = freeBoardRepository.findFilteredFreeBoards("", targetContent, "", pageable);

        assertThat(filteredByTitleFreeBoards.getContent().size()).isEqualTo(1);
        assertThat(filteredByTitleFreeBoards.getContent().get(0).getContent()).isEqualTo("content");

        assertThat(filteredByContentFreeBoards.getContent().size()).isEqualTo(1);
        assertThat(filteredByContentFreeBoards.getContent().get(0).getTitle()).isEqualTo("non-target");
    }

 

 이번에는 Querydsl을 사용하기 위해 @SpringBootTest를 사용했는데, 추후 Querydsl Respoitory와 일반 JpaRepository의 테스트를 분리하여 JpaRepository 테스트에는 @DataJpaTest를 사용해야겠다.

 

 라고 생각하는 찰나에 엄청난 글을 발견해버렸다. 구글은 신이고... 선배님들은 무적이다...

 

 Querydsl을 직접 사용해보니, 생각보다 별 것 없었다. SQL문법을 잘 모르면 어렵겠다 싶기한데, 뭐 그건 아니니까. 이미 JPQL로도 몇 번 구현해 본 터라 훨씬 낫다.

 

 다음에는 Querydsl을 통해 join테이블을 사용하거나 다른 다양한 기능들을 사용해보고싶다. 얼마나 쉽게 구현될지 감도 안온다.

참고한 글들

- Querydsl 사용을 위한 환경설정 : https://www.inflearn.com/chats/669477/querydsl-springboot-2-7%EC%9D%98-gradle-%EC%84%A4%EC%A0%95%EC%9D%84-%EA%B3%B5%EC%9C%A0%ED%95%A9%EB%8B%88%EB%8B%A4

 

QueryDsl SpringBoot 2.7의 gradle 설정을 공유합니다. - 인프런 | 고민있어요

plugins { id 'org.springframework.boot' version '2.7.4' id 'io.spring.dependency-management' version '1.0.14.RELEASE' id 'java' } group = 'study' ve...

www.inflearn.com

 

- Querydsl 사용을 위한 Configuration, Bean 설정 : https://tecoble.techcourse.co.kr/post/2021-08-08-basic-querydsl/

 

Spring Boot에 QueryDSL을 사용해보자

1. QueryDSL PostRepository.java Spring Data JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용하더라도, 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL…

tecoble.techcourse.co.kr

- Querydsl 메서드 구현 및 테스트 : https://jojoldu.tistory.com/394

 

[Querydsl] 다이나믹 쿼리 사용하기

안녕하세요! 이번 시간에는 Querydsl에서의 다이나믹 쿼리를 어떻게 작성하면 좋을지에 대해 진행합니다. 처음 Querydsl을 쓰시는 분들이 가장 많이 실수하는 부분이니 그럼 시작합니다! 모든 코드

jojoldu.tistory.com

- Querydsl로 페이징 처리 :https://jessyt.tistory.com/55

 

[JPA] Querydsl에 pageable을 적용하며... 2가지 방법을 소개하겠습니다.

Querydsl을 적용하고 paging 처리를 위해 pageable을 적용한 내용을 정리하겠습니다. Querydsl을 적용방법은 지난 글에 정리해놓았습니다. 지난글에서 정리한 Querydsl 3가지 방법 중 제가 테스트를 해본 2

jessyt.tistory.com

- @DataJpaTest 사용하여 Querydsl 테스트하기 : https://rachel0115.tistory.com/entry/QueryDsl-DataJpaTest-%EC%97%90%EC%84%9C-Repository-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0

 

[QueryDsl] @DataJpaTest 에서 @Repository 테스트하기

개요 프로젝트 진행중에 QueryDsl을 사용하는 CustomRepository를 만들었다. 여러 엔티티를 JOIN하여 데이터를 조회할 예정이였기 때문에 JpaRepository에 상속하지 않고 @Repository 어노테이션을 붙여 스프링

rachel0115.tistory.com

 

반응형

댓글