[Spring JPA] Specification을 이용한 게시글 동적 검색과 페이징 처리

publicguard_·2025년 7월 30일

개요

게시글 리스트를 조건에 맞게 검색하고, 페이징까지 적용해야 하는 경우가 많다. 예를 들어 "삭제되지 않은 게시글 중 약속글이 아닌 일반 게시글만 보여줘", 혹은 "카테고리가 OO이고 제목에 특정 키워드가 포함된 글만 검색해줘"와 같은 요구사항이 있을 수 있다.

이러한 복잡한 조건을 쿼리로 다 처리하려면 코드가 지저분해질 수밖에 없는데, Spring Data JPA에서는 이런 상황을 위한 강력한 도구, 바로 Specification을 제공한다.

이번 글에서는 Specification을 활용해 조건에 따라 게시글을 검색하고, 이를 Page로 반환하는 과정을 자세히 정리해본다.

메서드 개요

우선 우리가 다룰 메서드는 다음과 같다.

public Page<PostListDto> findByAll(Pageable pageable, PostSearchDTO postSearchDTO)
  • Pageable: 페이징 및 정렬을 위한 파라미터

  • PostSearchDTO: 사용자가 전달한 검색 조건 (예: 카테고리, 제목 등)

결과는 PostListDto로 매핑된 페이징 객체로 반환된다.

Specification 객체 생성

이 메서드의 핵심은 Specification<Post> 객체다. 이 객체는 JPA Criteria API를 통해 쿼리를 정의할 수 있도록 도와주며, 동적으로 조건을 붙일 수 있다.

Specification<Post> specification = new Specification<Post>() {
    @Override
    public Predicate toPredicate(Root<Post> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        ...
    }
};

여기서 Root<Post>는 쿼리 대상이 되는 Post 엔티티의 루트를 의미하고, CriteriaBuilder는 조건을 만들기 위한 유틸이다.

기본 조건 설정

우선 검색 대상은 다음 두 조건을 만족하는 게시글이다.

  • 삭제되지 않은 게시글 (delYn = 'N')
  • 약속글이 아닌 일반 게시글 (appointment = 'N')
List<Predicate> predicateList = new ArrayList<>();
predicateList.add(criteriaBuilder.equal(root.get("delYn"), "N"));
predicateList.add(criteriaBuilder.equal(root.get("appointment"), "N"));

이 두 조건은 검색과 관계없이 항상 적용되는 필수 조건이다.

선택 조건 추가 (카테고리, 제목)

그다음 PostSearchDTO에서 사용자로부터 전달받은 조건들을 확인하여 동적으로 추가한다.

    predicateList.add(criteriaBuilder.equal(root.get("category"), postSearchDTO.getCategory()));
}
if(postSearchDTO.getTitle() != null){
    predicateList.add(criteriaBuilder.like(root.get("title"), "%" + postSearchDTO.getTitle() + "%"));
}
  • 카테고리는 완전 일치 (equal)
  • 제목은 부분 일치 (like '%keyword%')

이 부분을 통해 유저가 조건을 입력하지 않으면 해당 조건은 무시되고, 입력했을 경우에만 필터링된다.

최종 조건 조합

모든 조건을 Predicate 배열로 변환하고, AND로 묶어서 하나의 최종 조건으로 만든다.

Predicate[] predicateArr = new Predicate[predicateList.size()];
for(int i = 0; i < predicateList.size(); i++){
    predicateArr[i] = predicateList.get(i);
}
Predicate predicate = criteriaBuilder.and(predicateArr);
return predicate;

쿼리 실행 및 DTO 매핑

이제 완성된 Specification을 기반으로 JPA의 findAll() 메서드를 호출하여 조건에 맞는 게시글 리스트를 가져온다.

Page<Post> postList = postRepository.findAll(specification, pageable);

그리고 Post 엔티티를 PostListDto로 변환한다. 이를 통해 클라이언트에는 필요한 정보만 담긴 DTO 형태로 반환할 수 있다.

return postList.map(a -> PostListDto.fromEntity(a));

마무리하며

이 방식은 매우 유연하고, 조건이 늘어나더라도 깔끔하게 관리할 수 있다. Specification을 활용하면 복잡한 검색 조건을 메서드 분기 없이 객체화하여 처리할 수 있고, 페이징 처리도 Pageable 하나로 자연스럽게 해결된다.

profile
백엔드 개발자로 하루하루 성장 중인 publicguard_의 개발일기입니다.

0개의 댓글