반응형
배운 점 및 느낀 점
기존에는 편리한 Spring Data JPA만 사용하다가 “가독성 측면”, “컴파일 시점에서 오류 확인”, “조건 메서드 재사용” 등 여러 장점을 가진 QueryDSL을 사용하게 되었습니다.
페이징, 검색, 필터링 조건을 적용한 쿼리문을 작성하면서 QueryDSL을 사용하는 이유와 장점을 느꼈습니다. 이를 통해 상황에 따라 효율적으로 Spring Data JPA와 QueryDSL을 사용할 수 있게 되었습니다.
익힌 QueryDSL
1. PageableExecutionUtils을 통해 Count 쿼리 최적화
2. 검색 및 필터링 조건 적용
3. DTO 클래스로 한 번에 변환
Controller: 검색, 필터링, 페이징 API
@GetMapping("/recruits/applications")
public ApiResponse<CustomPage<ApplicationList>> findRecruitsApplications(
@RequestParam(name = "page", defaultValue = "0", required = false) int page,
@RequestParam(name = "size", defaultValue = "10", required = false) int size,
@RequestParam(name = "filter", defaultValue = "ALL", required = false) String filter,
@RequestParam(name = "q", required = false) String value) {
PageRequest pageRequest = PageRequest.of(page, size);
SearchVO searchVO = SearchVO.of(filter, value);
List<Recruit> recruits = recruitQueryService.getNowRecruit();
List<Long> applicationIds = lotteryQueryService.getApplicationsByLotteries(
recruits);
Page<ApplicationList> applicationPage = applicationQueryService.getNowApplications(
applicationIds, pageRequest, searchVO);
return ApiResponse.onSuccess(new CustomPage<>(applicationPage));
}
- Pageable을 통해 페이징 정보를 받을 수 있지만, Swagger 명세와 필요한 부분만 받아서 처리하도록 page, size만 처리하였습니다.
- PageRequest.of(page, size)를 통해 페이징 정보를 만들었습니다.
- SearchVO는 Record 자료형으로 조건에 해당하는 파라미터를 담아 서비스, 레포지토리 층으로 전달하였습니다.
CustomRepositoryImpl: QueryDsl 쿼리문
@Repository
@RequiredArgsConstructor
public class ApplicationRepositoryCustomImpl implements ApplicationRepositoryCustom {
private final JPAQueryFactory jpaQueryFactory;
@Override
public Page<ApplicationList> getApplicationPage(List<Long> applicationIds, Pageable pageable,
SearchVO searchVO) {
//** 1단계: Content 페이징 조회 **//
List<ApplicationList> content = jpaQueryFactory
.select(application)
.from(application)
.join(application.employee, employee).fetchJoin()
.where(application.id.in(applicationIds), searchEmployee(searchVO.value()),
filterAccept(searchVO.filter()))
.orderBy(application.id.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch()
.stream()
.map(AdminResponse::toApplicationList)
.toList();
//** 2단계: 카운트 조회 **//
JPAQuery<Long> countQuery = jpaQueryFactory
.select(application.count())
.from(application)
.where(application.id.in(applicationIds), searchEmployee(searchVO.value()),
filterAccept(searchVO.filter()));
//** 3단계: 총합 조회 **//
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
}
}
1단계: Content 페이징 조회
//** 1단계: Content 페이징 조회 **//
List<ApplicationList> content = jpaQueryFactory
.select(application)
.from(application)
.join(application.employee, employee).fetchJoin()
.where(application.id.in(applicationIds), searchEmployee(searchVO.value()),
filterAccept(searchVO.filter()))
.orderBy(application.id.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch()
.stream()
.map(AdminResponse::toApplicationList)
.toList();
- 신청서와 직원에 정보를 한 번에 처리해야 하는 상황이어서, join(application.employee, employee). fetchJoin()
페치 조인을 통해 N+1 문제를 방지했습니다. - where 절에는 조건문을 적용하여 원하는 데이터를 추출할 수 있습니다.
- application.id.in(applicationIds): 특정 신청서 아이디를 가진 데이터를 조회한다.
- searchEmployee(searchVO.value()): 직원을 searchVO.value() 값을 통해 검색한다.
- filterAccept(searchVO.filter()): 승인 필터링을 searchVO.filter() 값을 통해 필터링한다.
- orderBy()를 통해 특정 속성을 기준으로 정렬합니다.
- asc(): 오름차순 / desc(): 내림차순
- offset()을 통해 어느 위치부터 페이징 할지 지정합니다.
- Controller에서 전달받은 페이징 정보를 통해 pageable.getOffset()을 지정합니다.
- limit()을 통해 몇 개의 데이터를 페이징할 지 지정합니다.
- Controller에서 전달받은 페이징 정보를 통해 pageable.getPageSize()을 지정합니다.
장점 1
fetch()에서 끝나면 List <Application>이 조회됩니다. 하지만 QueryDSL을 통해 쿼리문을 작성하고 JAVA 문법의 Stream을 통해 바로 DTO 매핑을 할 수 있습니다.
2단계: Count 조회
//** 2단계: 카운트 조회 **//
JPAQuery<Long> countQuery = jpaQueryFactory
.select(application.count())
.from(application)
.where(application.id.in(applicationIds), searchEmployee(searchVO.value()),
filterAccept(searchVO.filter()));
- select(application.count()): 조회한 데이터의 카운트를 조회하여 페이징 처리에 사용합니다.
- where 절에는 1단계에서 조회할 때 적용한 같은 조건을 작성해 줘야 올바른 페이징 처리가 가능합니다.
조건 여부 - BooleanExpression
//** 검색 조건 **//
private BooleanExpression searchEmployee(String value) {
return value != null ? employee.accountId.containsIgnoreCase(value) : null;
}
//** 승인여부 필터링 **//
private BooleanExpression filterAccept(String filter) {
return !filter.equals("ALL") ? application.isAccept.eq(Accept.valueOf(filter)) : null;
}
- searchEmployee()는 직원의 accountId를 검색하는 필터 메서드입니다.
- 검색한 값(value)이 있으면 containsIgnoreCase(value)를 통해 value를 포함하는지 검색합니다. 없는 경우 null을 반환합니다. (containsIgnoreCase: 대소문자 상관없이 조회)
- filterAccept()는 승인 여부를 필터링하는 메서드입니다.
- filter의 값이 ALL인 경우 null을 반환하고, 아닌 경우네는 Accept의 Enum 값과 비교하여 데이터를 조회합니다.
장점 2
BooleanExpression이 where절에 적용되고, null인 경우에는 해당 조건 메서드는 무시되어 나머지 조건을 바탕으로 조회할 수 있습니다.
장점 3
PageableExecutionUtils을 통해 Count 쿼리 최적화
fetchResults를 사용하지 않고 content와 count를 분리하여 조회하면서 페이징 조회 시 발생하는 Count 쿼리를 최적화할 수 있습니다.
Count 쿼리가 최적화되는 경우 (count 쿼리가 생략되는 경우)
- 첫 페이지이면서, 전체 Content 사이즈가 PageSize보다 작은 경우
- ex) 하나의 페이지에 50개의 콘텐츠를 보여주는데, 총데이터가 50개가 안 되는 경우 count 쿼리가 생략되어 조회합니다.
- 마지막 페이지인 경우
- Offset + Content Size를 더해서 전체 Size를 구할 수 있어 count 쿼리가 생략되어 조회됩니다.
반응형
'Spring Framework > QueryDSL' 카테고리의 다른 글
Querydsl 날짜 연산 문제 해결 : Interval 예약어 미지원 - Java 날짜 객체를 사용하기 (1) | 2024.11.15 |
---|---|
Querydsl OrderSpecifier를 활용한 동적 정렬 방법 - Pathbuilder, Sort (0) | 2024.10.18 |
[Querydsl] JPAExpressions를 활용한 Querydsl 서브쿼리 작성 방법 (0) | 2024.10.17 |
[Querydsl] QueryDSL @QueryProjection 프로젝션 활용법 : DTO, Bean, Field, Constructor 사용법 (0) | 2024.09.16 |
[QueryDsl] QueryDsl groupBy 여러 개 적용하기 (0) | 2024.08.10 |