JPA를 이용해서 개발을 하다보면, 복잡한 검색조건, 단순히 검색조건이 한 두개만 늘어나도 상당히 코드의 가독성이 확 떨어지는 경우가 생깁니다. 그런 상황에서 동적쿼리라는 말이 들리고, Specification이라는 인터페이스도 들리게 됩니다. Specification이란 무엇인지 한 번 알아보도록 하겠습니다.
Specification이란, Spring Data JPA에서 제공하는 API중에 하나입니다. 복잡한 검색 조건, 다양한 조합의 필터링 로직을 쉽게 구현할 수 있는 API입니다. 특히 검색이나 필터링 조건이 많은 REST API를 개발할 때 매우 유용한 API입니다.
왜 써야 하는가?
테이블에서 필드에서 조건이 맞는 데이터를 가져온다고 생각해봅시다. 그럼 JPA로 간단하게 인터페이스의 method명을 조건을 명시해서 가져올 수 있습니다.
검색 조건이 1개일때
import org.springframework.data.jpa.repository.JpaRepository;
public interface SomeRepository extends JpaRepository<Some, Long> {
List<Some> findByIsActiveFalse();
}
만약 어떤 Entity 테이블에서 isActive라는 필드가 false인 데이터들의 List를 가져온다고 하면, Repository 인터페이스에 이런 method만 만들어줘도, 원하는 데이터를 가져올 수 있습니다.
검색 조건이 2개일때
그럼 조건이 하나 늘어나면 어떻게 될까요?
import org.springframework.data.jpa.repository.JpaRepository;
public interface SomeRepository extends JpaRepository<Some, Long> {
List<Some> findByIsActiveFalseAndIsVerifiedFalse();
}
isActive 필드와 isVerified 필드 둘 다 false인 데이터를 가져온다고 하면 이렇게 됩니다. 벌써 method명이 읽기가 힘들어지고 있는게 보입니다.
검색 조건이 2개, 생성 날짜 기준으로 정렬할 때
그럼 2개의 조건에 맞는 데이터를 데이터 생성 날짜 필드를 기준으로 오래된 것부터 최신 데이터 순으로 정렬을 해서 가져온다고 하면 어떻게 될까요?
import org.springframework.data.jpa.repository.JpaRepository;
public interface SomeRepository extends JpaRepository<Some, Long> {
List<Some> findByIsActiveFalseAndIsVerifiedFalseOrderByRegistrationTimeAsc();
}
method명이 끔찍하게 길어지는 것을 볼 수 있습니다.
그럼 이것들을 JPQL이나, 네이티브 SQL을 이용한다면 쿼리를 이용해서 어느정도 무슨말인지는 알겠지만 그래도 굉장히 읽기가 힘들게 되는데요.
JPQL, 네이티브 SQL
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
public interface SomeRepository extends JpaRepository<Some, Long> {
@Query("SELECT e FROM Some e WHERE e.isActive = false AND e.isVerified = false ORDER BY e.registrationTime ASC")
List<YourEntity> findByConditionsOrderByRegistrationTimeAsc();
}
JPQL
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
public interface SomeRepository extends JpaRepository<Some, Long> {
@Query(value = "SELECT * FROM some WHERE is_active = FALSE AND is_verified = FALSE ORDER BY registration_time ASC", nativeQuery = true)
List<YourEntity> findByConditionsOrderByRegistrationTimeAsc();
}
네이티브SQL
둘 다 아까보다는 그래도 보기 쉬워졌지만, 그리고 주석을 처리해서 어떤 메서드인지 어떤 기능을 하는지 작성 해줄 수 있겠지만, 여전히 보기 힘든 코드인 것은 다름 없습니다.
이런 상황에서 Entity의 외래키를 이용해서 조인까지 한다면 더 복잡하고, 긴 코드가 될 것입니다. Specification을 사용해서 동적쿼리를 만들게 되면, 코드를 보기 쉽게 만들 수 있습니다.
Specification 활용
import org.springframework.data.jpa.domain.Specification;
public class SomeSpecifications {
public static Specification<Some> hasEmail(String email) {
return (root, query, cb) -> cb.equal(root.get("user").get("email"), email);
}
public static Specification<Some> isActiveFalse() {
return (root, query, cb) -> cb.isFalse(root.get("isActive"));
}
public static Specification<Some> isVerifiedFalse() {
return (root, query, cb) -> cb.isFalse(root.get("isVerified"));
}
}
이렇게 Specification의 클래스를 만들어주고, 기능을 하는 method를 정의해줍니다. Specification 메서드는 3개의 파라미터를 받습니다.
root: 'Root<Some>' 타입으로, 쿼리의 루트 엔티티를 나타냅니다. 쿠러 대상 엔티티의 필드에 접근할 때 사용됩니다.
query: 'CriteriaQuery<?>' 타입으로, 쿼리 자체를 구성할 때 사용될 수 있습니다.
cb: 'CriteriaBuilder'타입으로, 쿼리의 조건을 생성하는데 사용되는 다양한 메서드를 제공합니다.
그래서 위의 메서드들을 살펴보면 cb를 이용해서 대상 엔티티의 user필드의 email 필드의 값과, 입력받은 값이 같은 값을 equal method를 통해서 반환 받습니다. 그리고 각각 검색 조건은 대상 엔티티의 값이 false값이면 반환받습니다.
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface SomeRepository extends JpaRepository<Some, Long>, JpaSpecificationExecutor<Some> {
// JpaSpecificationExecutor를 상속 받아 사용합니다.
}
repository 클래스에 JpaSpecificationExecutor를 상속받고,
List<Some> results = SomeRepository.findAll(
where(hasEmail(email))
.and(isActiveFalse())
.and(isVerifiedFalse()),
Sort.by("registrationTime").ascending()
);
이렇게 서비스에서 아까 정의한 method들을 활용해서 동적인 쿼리를 작성 할 수 있습니다. 이 코드가 무엇을 하는 쿼리인지 조금 더 명확하게 표현되는 것을 볼 수 있습니다. 이렇게 복잡한 검색을 해야할 때 JPA에서는 Specification API를 활용해서 조금 더 보기 쉽게 구현할 수 있습니다.
'IT Tech > Java' 카테고리의 다른 글
React - Java Spring Boot 프로젝트에서 카카오 로그인 api 기능 구현하기 (0) | 2024.07.29 |
---|---|
SpringBoot 프로젝트에 SPA 프로젝트 연결하기 (0) | 2024.07.29 |
자바 - 최대공약수를 가장 빠르게 찾는 방법: 유클리드 호제법 알고리즘 (0) | 2024.01.24 |
JPA의 영속성 전파 cascade 옵션에 대해서 알아보기 (0) | 2024.01.17 |
자바(JAVA)의 Map에서 키와 값으로 요소 찾기 - containsKey와 containsValue 활용하기 (0) | 2024.01.09 |