프로젝트/Ticketing

[Ticketing] JPQL에서 QueryDSL으로 전환

이덩우 2024. 5. 18. 23:14

현재 프로젝트에서 직접적인 쿼리가 필요한 부분은 전부 JPQL을 통해 해결했었다.

대체로 크게 복잡하지 않은 쿼리이기에 대수롭지 않게 넘겼었는데, 최근에 생각에 변화가 생겼다.

내가 짠 코드이기에 당장은 이해하기 쉬울 것이다. 하지만 6개월 뒤에 본다면 쉽게 이해할 수 있을까? 
소스 작성자도 시간이 지나면 이해하기 어려울 수 있는데, 다른 팀원이 봤을 때 가독성이 좋다고 할 수 있을까?

 

게다가 동적쿼리를 활용해 조회하는 쿼리도 존재하니, 지난날의 생각이 매우 잘못되었음을 느꼈다.

코드의 가독성, 그리고 동적 쿼리 문제를 해결하기 위해 QueryDSL을 도입하기로 결정했다.

 

비교적 간단한 쿼리로 구성된 단건 조회와 동적 쿼리를 구현해야하는 메서드를 비교해보며, QueryDSL을 도입함으로써 실제로 가독성이 좋아졌는지 확인해보겠다.

 


1. 간단한 쿼리 (단건 조회)

- JPQL

@Repository
@RequiredArgsConstructor
public class JpaMemberRepository implements MemberRepository{
    private final EntityManager em;

    @Override
    public Optional<Member> findByUsername(String email) {
        String jpql = "select m from Member m where m.email = :email";
        Member member = em.createQuery(jpql, Member.class)
                .setParameter("email", email)
                .getSingleResult();
        return Optional.ofNullable(member);
    }
}

 

- QueryDSL

@Repository
@RequiredArgsConstructor
public class JpaMemberRepository implements MemberRepository{
    private final EntityManager em;
    private final JPAQueryFactory queryFactory;

    @Override
    public Optional<Member> findByUsername(String email) {
        return Optional.ofNullable(queryFactory
                .select(member)
                .from(member)
                .where(equalEmail(email))
                .fetchFirst());
    }
}

 

- 비교

코드 라인 수는 크게 차이가 없지만, 자바 언어로 작성하면서 컴파일 타임에 문법 오류를 잡을 수 있다는 장점과 쿼리가 확실하게 눈에 들어온다는 장점이 있다.

 


2. 동적 쿼리 (페이지네이션)

- JPQL

@Repository
@RequiredArgsConstructor
public class JpaPerfDetailRepository implements PerfDetailRepository {
    private final EntityManager em;

    @Override
    public List<PerfDetailResponseDto> findAllByPerf(PerfSearchDto perfSearchDto) {
        String jpql = "select p from PerformanceDetail p where " +
                "(:perfId is null or p.performance.id = :perfId) and " +
                "(:title is null or p.artist like concat('%', :title, '%')) and " +
                "(:button is null or " +
                "(:button = 'next' and p.id > :index) or " +
                "(:button = 'previous' and p.id < :index)) ";
                
        TypedQuery<PerformanceDetail> query = em.createQuery(jpql, PerformanceDetail.class);
        if (perfSearchDto.getPerfId() != null) {
            query.setParameter("perfId", perfSearchDto.getPerfId());
        } else {
            query.setParameter("perfId", null);
        }
        if (perfSearchDto.getIndex() != null) {
            query.setParameter("index", perfSearchDto.getIndex());
        } else {
            query.setParameter("index", null);
        }
        if (perfSearchDto.getButton() != null) {
            query.setParameter("button", perfSearchDto.getButton());
        } else {
            query.setParameter("button", null);
        }
        if (perfSearchDto.getTitle() != null) {
            query.setParameter("title", perfSearchDto.getTitle());
        } else {
            query.setParameter("title", null);
        }
        query.setMaxResults(perfSearchDto.getSize());

        List<PerformanceDetail> resultList = query.getResultList();
        return resultList.stream()
                .map(p -> new PerfDetailResponseDto(p))
                .collect(Collectors.toList());
    }
}

 

- QueryDSL

@Repository
@RequiredArgsConstructor
public class JpaPerfDetailRepository implements PerfDetailRepository {
    private final EntityManager em;
    private final JPAQueryFactory queryFactory;

    @Override
    public List<PerformanceDetail> findAllByPerf(PerfSearchDto perfSearchDto) {
        return queryFactory
                .select(performanceDetail)
                .from(performanceDetail)
                .where(equalCategoryId(perfSearchDto.getPerfId()),
                        likeTitleName(perfSearchDto.getTitle()),
                        greaterThanIndex(perfSearchDto.getIndex()))
                .limit(perfSearchDto.getSize())
                .fetch();
    }
}

- 비교

사실 이 동적 쿼리에서 QueryDSL이 대단하다고 느꼈다.

이전에 JPQL을 통해 작성한 코드를 보면.. 누가 봐도 같이 일하기 싫게 생긴 코드이다.

 

QueryDSL 덕분에 가독성이 굉장히 좋아졌다고 생각한다. 특히, where 절에 다중 조건을 넣는 방식이 편리했고 null을 반환한다면 자동으로 제외하는 기능 덕분에 JPQL에서 덕지덕지 붙인 `if(ex != null) {}`코드를 작성하지 않아도 됐다.