본문 바로가기

spring

JPA Specification - 복잡한 검색 조건 처리하기 (vs QueryDSL)

JPA Specification이란?
: JPA에서 동적 쿼리를 조합할 수 있게 도와주는 WHERE 절 조건 빌더
조건을 메서드 단위로 나누고, 조합할 수 있도록 함

→ where 절의 조건 하나하나를 Specification 객체로 만들고, 이를 .and(), .or() 등으로 조립한다 !

 

 

기본 사용법

 

1. Repository에 JPASpecificationExecutor 확장

public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}

 

2. Specification 정의

public class UserSpecification {

    public static Specification<User> hasName(String name) {
        return (root, query, cb) -> cb.equal(root.get("name"), name);
    }

    public static Specification<User> createdAfter(LocalDateTime time) {
        return (root, query, cb) -> cb.greaterThan(root.get("createdAt"), time);
    }

    public static Specification<User> hasStatus(String status) {
        return (root, query, cb) -> cb.equal(root.get("status"), status);
    }
}

 

 

3. 사용 예시

Specification<User> spec = Specification
    .where(UserSpecification.hasName("name"))
    .and(UserSpecification.hasStatus("ACTIVE"))
    .and(UserSpecification.createdAfter(LocalDateTime.now().minusDays(30)));

List<User> result = userRepository.findAll(spec);

 

 

파라미터 소개
파라미터 설명
root 쿼리의 루트 (FROM 절의 엔티티)
query CriteriaQuery 객체 (보통은 잘 안 씀)
cb CriteriaBuilder (조건 생성기)
CriteriaBuilder란?
JPA의 Criteria API에서 조건을 생성하는 빌더 객체

 

 

CriteriaBuilder의 메서드
메서드 예시
equal cb.equal(root.get("name), "kim")
notEqual cb.notEqual(root.get("age"), 20)
graterThan, lessThan cb.greaterThan(root.get("age"), 18)
like cb.like(root.get("name"), "%kim%")
in cb.in(root.get("status")).value("ACTIVE")
and, or cb.and(cond1, cond2)
not cb.not(cb.equal(...))
isNull, isNotNull cb.isNull(root.get("email"))
between cb.between(root.get("age"), 20, 30)

 

 

optional filtering (조건 null 체크 처리)
public static Specification<User> hasNameIfPresent(String name) {
    return (root, query, cb) ->
        name == null ? null : cb.equal(root.get("name"), name);
}
  • null을 반환하면 해당 조건은 무시됨

 

JOIN 처리
public static Specification<Post> hasAuthorName(String name) {
    return (root, query, cb) -> {
        Join<Post, User> author = root.join("author");
        return cb.equal(author.get("name"), name);
    };
}

 

 

페이징 + 정렬과 함께 사용
Pageable pageable = PageRequest.of(0, 10, Sort.by("createdAt").descending());
Page<User> page = userRepository.findAll(spec, pageable);
  • repository 메서드에 spec과 pageable 전달

 

❗ JpaSpecificationExecutor 실무 사용이 권장되지 않는 이유

 

1. 내부적으로 JPA Criteria API 기반

  • Criteria API는 문법이 복잡하고 가독성이 매우 떨어지는 단점이 있음
  • 조건이 복잡해질수록 코드가 길고 비직관적 → 유지보수성이 낮아짐

2. 타입 안정성이 없음

  • 필드명을 문자열로 입력 → 오타가 있어도 컴파일 타임 오류가 발생하지 않아 버그 유발 위험 증가
cb.equal(root.get("name"), "deang"); // "name" 오타 나도 컴파일 통과

 

 

Specification vs QueryDSL

 

Specification은 JPA의 공식적인 방법으로, 추가적인 라이브러리나 의존성 없이 바로 사용 가능

QueryDSL은 추가적인 의존성이 필요하지만, 가독성이 좋고 유지보수가 용이

 

간단한 동적 조건 조합 → Specification으로 구현해볼 수 있음 !

복잡한 쿼리, 실무 서비스 레벨 → QueryDSL을 사용하는 것이 안정적, 유지보수에 유리

 

복잡한 조건 조립은 QueryDSL로 깔끔하게 구현하자 ~