Get API를 구현하면서 search field를 추가하고 있다. 기존 FastAPI를 작성할때는 조건에 따라 쿼리를 추가해주는 방식으로 구현을 했던 부분인데, Spring으로 변경하려고 찾아보면서 Specification과 JpaSpecificationExecutor라는 인터페이스를 알게되어 적용해보면서 내용을 정리해본다.
Specification 과 JpaSpecificationExecutor 는 Spring Data JPA에서 동적 쿼리를 생성하기 위해 사용되는 인터페이스이다. 이들을 사용하면 복잡한 검색 조건과 여러 필터링 기준을 쿼리에 유연하게 적용할 수 있다.
- 복잡한 검색 조건이 필요할 때
- 여러 필드에 대한 다양한 조합의 필터링이 필요할 때
- 사용자의 입력에 따라 변하는 쿼리 조건이 필요할 때
정적쿼리(static query)는 애플리케이션 실행 전에 이미 쿼리의 형태가 완전히 정해져 있는 쿼리이다. 실행 시간에 쿼리의 구조가 변경되지 않으므로 주로 고정된 데이터 구조에 대한 조회에 사용된다.
내가 구현하고자 한 search field는 사용자의 입력, 특정 조건에 따라 쿼리의 구조가 달라지므로 동적쿼리(Dynamic query)를 사용해야 하는데, 이 때 Specification 과 JpaSpecificationExecutor 를 사용할 수 있는 것이다.
나는 지금 매우 적극적으로 Spring 프레임워크에 대해서 익혀나가고 있는 중이니 당장 사용해본다.
Specification은 람다 표현식 또는 익명 클래스를 통해 구현된다. toPredicate 메서드를 오버라이드하여 검색 조건을 정의한다.
fun someSpecification(): Specification<EntityType> {
return Specification { root, query, criteriaBuilder ->
// 여기에 검색 조건을 정의합니다.
criteriaBuilder.equal(root.get<EntityType>("fieldName"), value)
}
}
Specification 과 Criteria API를 사용하는 경우 root, query, criteriaBuilder 는 쿼리를 구성하는데 필요한 주요 구성 요소로, 동적 쿼리를 생성하는데 사사용되는 JPA Criteria API의 핵심 요소이다.
<T>쿼리의 루트 엔티티로, 쿼리하고자 하는 엔티티의 속성에 접근하기 위해 사용된다. 예를들어 root.get("name")은 쿼리 대상 엔티티의 name 속성에 접근한다.
<?>구성된 쿼리 자체를 나타낸다. 쿼리의 구조와 반환할 결과 타입을 정의한다. 쿼리의 SELECT, WHERE, GROUP BY, ORDER BY 등의 절을 정의하는데 사용되며, 최종적으로 생성되는 쿼리의 골격을 결정한다.
Specification 인터페이스의 toPredicate 메서드에서 특정 조건(Predicate)을 생성하는 것이 주 목적이다.
쿼리를 구성하는 데 사용되는 다양한 조건과 표현식을 생성하는 팩토리이다. 쿼리의 조건과 함수를 생성하는데 사용된다. CriteriaBuilder는 쿼리를 프로그래밍 방식으로 안전하게 구성할 수 있는 API를 제공한다.
criterialBuilder.equal(...), criteriaBuilder.greaterThan(...)과 같이 특정 조건을 만드는데 사용된다.
일반적인 Specification 구현 예시는 아래와 같다.
Specification<MyEntity> spec = new Specification<MyEntity>() {
@Override
public Predicate toPredicate(Root<MyEntity> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
// 여기서 조건을 정의하고 Predicate를 반환합니다.
Predicate condition = criteriaBuilder.equal(root.get("myField"), someValue);
return condition;
}
};
root는 MyEntity 클래스에 접근하는 데 사용되며, criteriaBuilder를 통해 생성된 Predicate는 MyEntity의 myField가 someValue와 같은지 비교하는 조건이다. query는 이러한 조건을 포함할 최종 쿼리를 나타낸다.
Repository 인터페이스에 JpaSpecificationExecutor를 확장하여, Specification을 사용할 수 있는 메서드를 제공한다.
interface MyRepository : JpaRepository<EntityType, Long>, JpaSpecificationExecutor<EntityType> {
// 기본적인 CRUD 메서드와 함께 Specification을 사용할 수 있는 메서드가 포함됩니다.
}
Specification 객체를 생성한 후, JpaSpecificationExecutor의 findAll 메서드 등과 함께 사용한다.
val spec = someSpecification()
val results = myRepository.findAll(spec)
Specification과 JpaSpecificationExecutor는 복잡한 쿼리 조건을 동적으로 처리하는 데 매우 유용하다. 사용자의 요구에 따라 검색 조건이 자주 변경되거나, 다양한 필터링 옵션이 필요한 경우 특히 유용하다. 그러나, 쿼리의 복잡도가 높아질수록 성능 최적화와 코드의 명확성에 대해 신중하게 고려해야 한다.
Specification, CriteriaBuilder로 간단하게 search 구현 완료!
알면 알수록 스프링 참 잘되어있구만
이 글은 코틀린과 스프링을 시작한지 1개월도 안된 왕초보가 서치와 chatGPT를 통해 공부하면서 정리한 내용입니다. 틀린 내용이 있을 수 있고 정리된 내용보다 훨씬 효과적인 방법이 있을 수 있습니다.