QueryDSL 적용

LeeYulhee·2023년 7월 21일
0

👉QueryDSL이란?


  • QueryDSL은 자바 기반의 오픈 소스 라이브러리로, 데이터베이스 쿼리 작성을 위한 도메인 특화 언어(DSL: Domain Specific Language)를 제공하는 프레임워크
  • 주로 JPA(Java Persistence API)를 사용하는 프로젝트에서 데이터베이스 쿼리를 편리하게 작성하고 유지보수하는 데 도움 됨
  • 기존의 SQL을 문자열로 직접 작성하는 방식 대신, QueryDSL을 활용하면 자바 코드로 쿼리를 작성할 수 있음


👉동적 쿼리 & 정적 쿼리


📌 정적 쿼리

  • 쿼리의 구조가 실행 시점에서 변하지 않고 고정된 쿼리를 의미
  • 값의 여부와 상관 없이 쿼리가 고정되어 있음

📌 동적 쿼리

  • 실행 시점에 쿼리 구조가 변경될 수 있는 쿼리를 의미
  • 사용자의 입력값이나 프로그램의 조건에 따라 쿼리의 조건절이나 정렬 방식 등이 동적으로 변할 수 있음
    • QueryDSL, Criteria API등이 이에 해당
  • 간단하게 말하자면, 경우에 따라(값이 없을 때 등) WHERE에 AND 등이 추가되거나 없어지면 동적 쿼리라고 볼 수 있음


👉기존 코드


MatchingController

@GetMapping("/list/filter")
public String filterMatching(@RequestParam(name = "genretype", required = false) String genreTypeStr,
                             @RequestParam(name = "starttime", required = false) Integer startTime,
                             @RequestParam(name = "gender", required = false) String gender,
                             @RequestParam(defaultValue = "0") int page,
                             @RequestParam(defaultValue = "8") int size,
                             @RequestParam(defaultValue = "createDate") String sortCode,
                             @RequestParam(defaultValue = "DESC") String direction,
                             Model model) {

    GenreTagType genreType = null;
    if (genreTypeStr != null && !genreTypeStr.isEmpty()) {
        genreType = GenreTagType.ofCode(genreTypeStr);
    }

    Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.fromString(direction), sortCode));
    Page<Matching> matchingList = matchingService.filterMatching(genreType, startTime,gender, pageable);
    model.addAttribute("matchingList", matchingList);
    model.addAttribute("currentPage", page);
    model.addAttribute("genretype", genreTypeStr); // This line is changed
    model.addAttribute("starttime", startTime);
    model.addAttribute("gender", gender);

    return "matching/list";
}

MatchingService

public Page<Matching> filterMatching(GenreTagType genreType, Integer startTime, String gender, Pageable pageable) {

    if (genreType != null && startTime != null) {
        // 장르와 시간
        return matchingRepository.findByGenreAndStartTime(genreType, startTime, pageable);
    } else if (genreType != null) {
        // 장르
        return matchingRepository.findByGenre(genreType, pageable);
    } else if (startTime != null) {
        // 시작시간
        return matchingRepository.findByStartTime(startTime, pageable);
    } else if (!gender.isEmpty()) {
        // 성별
        return matchingRepository.findByGender(gender, pageable);
    } else if (genreType != null && gender.isEmpty()) {
        // 장르와 성별
        return matchingRepository.findByGenreAndGender(genreType, gender, pageable);
    } else if (startTime != null && gender.isEmpty()) {
        // 성별과 시간
        return matchingRepository.findByStartTimeAndGender(startTime, gender, pageable);
    } else if (genreType != null && startTime != null && gender.isEmpty()){
        // 장르, 성별, 시간
        return matchingRepository.findByGenreAndStartTimeAndGender(genreType, startTime, gender, pageable);
    }
    return matchingRepository.findAll(pageable);
}


👉개선할 점


  • 주먹구구식으로 구현한 if-else if 제거
    • ⭐ QueryDSL 적용해보기


👉개선한 코드


MatchingRepository

public interface MatchingRepository extends JpaRepository<Matching, Long>, MatchingRepositoryCustom{
    Optional<Matching> findById(Long id);

    Page<Matching> findAll(Pageable pageable);

    Page<Matching> findByTitleContainingIgnoreCase(String keyword, Pageable pageable);

    Page<Matching> findByContentContainingIgnoreCase(String keyword, Pageable pageable);
}

MatchingRepositoryCustom

public interface MatchingRepositoryCustom {

    Page<Matching> filterByGenreAndStartTimeAndGender(GenreTagType genreType, Integer startTime, String gender, Pageable pageable);
}

MatchingRepositoryImpl

@RequiredArgsConstructor
public class MatchingRepositoryImpl implements MatchingRepositoryCustom{

    private final JPAQueryFactory queryFactory;

    @Override
    public Page<Matching> filterByGenreAndStartTimeAndGender(GenreTagType genreType, Integer startTime, String gender, Pageable pageable) {
        QMatching matching = QMatching.matching;

        // 필터링 조건을 담을 Predicate 변수 선언
        BooleanBuilder predicate = new BooleanBuilder();

        // 각각의 파라미터가 null이 아닌 경우에만 해당 필터를 추가
        if (genreType != null) {
            predicate.and(matching.genre.eq(genreType));
        }

        if (startTime != null) {
            predicate.and(matching.startTime.eq(startTime));
        }

        if (gender != null && !gender.isEmpty()) {
            predicate.and(matching.gender.eq(gender));
        }

        // 정렬 조건 생성
        OrderSpecifier<?> orderSpecifier = sortMatching(pageable);

        // fetch()와 select(Expressions.ONE).fetchOne()를 따로 사용
        List<Matching> results = queryFactory.selectFrom(matching)
                .where(predicate)
                .orderBy(orderSpecifier) // 외부에서 정렬 조건을 넘겨받음
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        Long total = queryFactory.select(Expressions.ONE.count()).from(matching)
                .where(predicate)
                .fetchOne();

        return new PageImpl<>(results, pageable, total);
    }

    private OrderSpecifier<?> sortMatching(Pageable page) {
        QMatching matching = QMatching.matching;

        //서비스에서 보내준 Pageable 객체에 정렬조건 null 값 체크
        if (!page.getSort().isEmpty()) {
            //정렬값이 들어 있으면 for 사용하여 값을 가져온다
            for (Sort.Order order : page.getSort()) {
                Order direction = order.getDirection().isAscending() ? Order.ASC : Order.DESC;
                switch (order.getProperty()){
                    case "createDate":
                        return new OrderSpecifier(direction, matching.createDate);
                }
            }
        }
        return null;
    }
}

📌 궁금한 내용 정리

❓BooleanBuilder와 BooleanExpression
    BooleanExpression과 BooleanBuilder 모두 QueryDSL에서 제공하는 클래스로써, 동적 쿼리를 작성할 때 사용

❓BooleanExpression
    조건 쿼리의 반환 타입으로, 여러 BooleanExpression들을 결합해서(AND, OR) 새로운 BooleanExpression을 생성할 수 있음
    쿼리를 조건으로 변환하는데 사용되는 클래스

❓BooleanBuilder
    여러 BooleanExpression들을 결합하여 하나의 조건 쿼리를 만들 때 사용
    BooleanBuilder는 내부적으로 BooleanExpression을 조합하고 관리하는 역할

❗정리하자면
    BooleanExpression은 BooleanBuilder에 추가될 수 있는 독립적인 조건(즉, 조건의 표현식)을 표현하는 반면, BooleanBuilder는 여러 BooleanExpression들을 관리하고 결합하여 최종적인 쿼리 조건을 생성


MatchingService

public Page<Matching> filterMatching(GenreTagType genreType, Integer startTime, String gender, Pageable pageable) {
        return matchingRepository.filterByGenreAndStartTimeAndGender(genreType, startTime, gender, pageable);
}

QueryDSLConfiguration

@Configuration
public class QueryDSLConfiguration {

    @PersistenceContext
    private EntityManager entityManager;

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


👉 결과 및 느낀 점 등


✔️ Service의 코드가 훨씬 깨끗해졌다.
✔️ 실행 시, 필요한 쿼리만 출력되는 것을 볼 수 있었다.
✔️ BuildGradle에 의존성 추가할 때, 오류가 많이 발생했어서 QueryDSL이 맞게 적용된 건지 확신이 없었다.
     ➕ 찾아보니 QueryDSL이 적용되면 프로젝트 src → generated → Entity들에 Q가 붙은 Q클래스들이 생성되는 것을 알 수 있었고, 내 프로젝트에도 적용되어 있었다.

profile
공부 중인 신입 백엔드 개발자입니다

0개의 댓글