지난 시간 동안 공부 JPAQueryFactory를 사용한 동적 검색 쿼리 작성
1️⃣ 구현한 기능 요약
- 키워드 기반 일정 검색 (제목, 내용, 작성자 이름에 포함된 항목)
- Page<SchedulePageResponseDto> 형태로 페이징 처리
- BooleanBuilder를 활용한 조건 동적 구성
BooleanBuilder condition = new BooleanBuilder();
if(keyword != null && !keyword.isBlank()) {
condition.and(
schedule.title.containsIgnoreCase(keyword)
.or(schedule.contents.containsIgnoreCase(keyword))
.or(author.name.containsIgnoreCase(keyword))
);
}
2️⃣ 전체 쿼리 예시 (ScheduleQueryRepositoryImpl)
더보기
(QuerydslConfig.java)
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager em;
@Bean
public JPAQueryFactory queryFactory() {
return new JPAQueryFactory(em);
}
}
(ScheduleQueryRepository.java)
/**
* 일정(Schedule) ScheduleRepository에 대한 QueryDSL리포지토리 인터페이스입니다.
*/
public interface ScheduleQueryRepository {
Page<SchedulePageResponseDto> searchSchedulesByKeyword(String keyword, Pageable pageable);
}
(ScheduleQueryRepositoryImpl.java)
/**
* QueryDSL + JPAQueryFactory 리포지토리
*/
@RequiredArgsConstructor
public class ScheduleQueryRepositoryImpl implements ScheduleQueryRepository {
private final JPAQueryFactory jpaQueryFactory;
/**
*
* @param keyword 검색어 (제목, 내용, 작성자 이름에 대해 부분 일치 검색)
* @param pageable 페이지 번호, 사이즈 등의 페이징 정보
* @return SchedulePageResponseDto 형태의 페이징된 일정 목록
*/
@Override
public Page<SchedulePageResponseDto> searchSchedulesByKeyword(String keyword, Pageable pageable) {
QSchedule schedule = QSchedule.schedule;
QAuthor author = QAuthor.author;
QComment comment = QComment.comment;
// 동적 쿼리 where 조건 생성을 위한 BooleanBuilder
BooleanBuilder condition = new BooleanBuilder();
// 검색어가 있을 때, 제목/내용/작성자 이름에서 해당 키워드 포함 여부 검사
if(keyword != null && !keyword.isBlank()) {
condition.and(
schedule.title.containsIgnoreCase(keyword)
.or(schedule.contents.containsIgnoreCase(keyword))
.or(author.name.containsIgnoreCase(keyword))
);
}
// 실제 페이징된 결과 목록 조회 (select 절에는 DTO로 바로 매핑)
List<SchedulePageResponseDto> content = jpaQueryFactory
.select(Projections.constructor(SchedulePageResponseDto.class,
schedule.title, // 일정 제목
schedule.contents, // 일정 내용
author.name, // 작성자 이름
comment.countDistinct(), // 댓글 수 (중복 제거)
schedule.createdDate, // 생성일
schedule.updatedDate // 수정일
))
.from(schedule) // FROM schedule
.join(schedule.author, author) // INNER JOIN author
.leftJoin(comment).on(comment.schedule.eq(schedule)) // 레프트 조인
.where(condition) // 동적 where 조건 적용
.groupBy(schedule) // COUNT 사용을 위한 그룹핑
.offset(pageable.getOffset()) // 몇 번째부터 가져올지 (페이지 시작 인덱스)
.limit(pageable.getPageSize()) // 한 페이지에 몇 개 가져올지
.orderBy(schedule.updatedDate.desc()) // 최신순 정렬
.fetch(); // 결과 리스트로 반환
// 전체 개수 조회 (페이징의 total count를 위함)
long total = Optional.ofNullable(
jpaQueryFactory
.select(schedule.count()) // 총 개수 select
.from(schedule)
.join(schedule.author, author)
.where(condition) // 동일한 where 조건 적용
.fetchOne() // 단일 결과 조회
).orElse(0L); // 결과가 null일 경우 0으로 처리 (NPE 방지)
return new PageImpl<>(content, pageable, total);
}
}
(ScheduleService.java)
/**
* 키워드와 페이징 처리된 일정 목록을 조회
*
* @param keyword 검색 키워드
* @param pageable 페이징 정보
* @return 일정 페이지 응답 DTO
*/
Page<SchedulePageResponseDto> searchSchedulesByKeyword(String keyword, Pageable pageable);
}
(ScheduleServiceImpl.java)
@Service
@RequiredArgsConstructor
public class ScheduleServiceImpl implements ScheduleService {
private final ScheduleRepository scheduleRepository;
private final AuthorRepository authorRepository;
/**
* 키워드와 페이징 처리된 일정 목록을 조회
*
* @param keyword 검색 키워드
* @param pageable 페이징 정보
* @return 일정 페이지 응답 DTO
*/
@Override
public Page<SchedulePageResponseDto> searchSchedulesByKeyword(String keyword, Pageable pageable) {
return scheduleRepository.searchSchedulesByKeyword(keyword, pageable);
}
}
(ScheduleController.java)
@RestController
@RequiredArgsConstructor
@RequestMapping("/schedules")
public class ScheduleController {
private final ScheduleService scheduleService;
/**
* 일정 키워드 검색 조회
*
* @param keyword 검색 조건
* @param page 조회할 페이지 번호 (기본값: 0)
* @param size 한 페이지당 항목 수 (기본값: 10)
* @return 검색 조건에 맞는 페이징된 일정 목록과 200(OK) 응답
*/
@GetMapping("/search")
public ResponseEntity<Page<SchedulePageResponseDto>> searchSchedulesByKeyword(
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "updateDate"));
Page<SchedulePageResponseDto> schedulePageResponseDto = scheduleService.searchSchedulesByKeyword(keyword, pageable);
return new ResponseEntity<>(schedulePageResponseDto, HttpStatus.OK);
}
}
🚨 에러 정리
❗️ 원인 : Q타입(QSchedule)이 생성 경로를 인식하지 않음
💡 해결 방법 : build.gradle에 Q타입 생성 경로 추가
// Q파일 인식 경로 추가
sourceSets {
main.java.srcDirs += 'build/generated/sources/annotationProcessor/java/main'
}
❗️ 원인 : 테스트 실행 시 BeanDefinitionOverrideException
- 테스트 클래스(SchedulerJpaApplicationTests)에 @EnableJpaAuditing을 추가했는데,
- 본 애플리케이션 설정에도 이미 @EnableJpaAuditing이 있어서 중복 선언으로 충돌 발생
💡 해결 방법 : @EnableJpaAuditing 중복 선언 되어 빈 중복 에러 발생
// ✅ 테스트 클래스에서 @EnableJpaAuditing 제거
@SpringBootTest
class SchedulerJpaApplicationTests {
@Test
void contextLoads() {
}
}
'TIL' 카테고리의 다른 글
| [250402 TIL] JWT 인증 로그아웃 구현(Redis 활용) (1) | 2025.04.02 |
|---|---|
| [250401 TIL] Spring Security + JWT 예외 처리 정리 (2) | 2025.04.01 |
| [250327 TIL]일정 관리 앱 JPA (2) | 2025.03.27 |
| [250326 TIL] JPA + ENTITY (1) | 2025.03.26 |
| [250325 TIL] Spring 숙련 강의 1주차 (1) | 2025.03.25 |