Repository 메서드 폭증, 왜 발생하는가
Spring Data JPA를 실무에서 사용하다 보면 어느 순간 Repository 파일을 열었을 때 당혹스러운 광경과 마주하게 된다. PM이 새로운 검색 필터를 요청할 때마다 메서드가 하나씩 추가되고, 어느새 findByStatusAndCreatedAtAfter, findByUserIdAndStatusAndRegionAndCreatedAtBetween 같은 메서드들이 20개, 30개씩 쌓여 있는 것이다. 이는 단순히 보기 불편한 수준이 아니라, 유지보수 비용을 기하급수적으로 늘리는 Repository Debt의 전형적인 모습이다.
4년차 이상 개발자라면 이 문제를 단순히 "코드 스타일"의 문제로 보아선 안 된다. 메서드가 늘어날수록 테스트 케이스도 선형적으로 증가하고, 신규 입사자의 온보딩 난이도도 올라간다. 조건 조합이 조금만 달라져도 새 메서드를 만들어야 하는 구조는 확장성 측면에서 근본적인 한계를 가진다.
Spring Data JPA Specifications: 이미 프레임워크 안에 있다
많은 개발자들이 이 문제를 해결하기 위해 QueryDSL을 도입하거나 직접 JPQL을 작성하는 방향을 선택한다. 물론 QueryDSL은 강력한 대안이지만, 별도 의존성 추가와 Q클래스 코드 생성 설정이 필요하다는 진입 장벽이 존재한다.
그런데 Spring Data JPA Specifications는 추가 라이브러리 없이 Spring Data JPA 내에 이미 포함되어 있다. JpaSpecificationExecutor 인터페이스를 Repository에 추가하는 것만으로 사용할 수 있으며, 검색 조건을 Specification 객체로 캡슐화하여 조합하는 방식으로 동작한다.
// Repository에 JpaSpecificationExecutor 추가
public interface OrderRepository
extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {
}
// 조건을 Specification으로 분리
public class OrderSpecs {
public static Specification<Order> hasStatus(OrderStatus status) {
return (root, query, cb) ->
cb.equal(root.get("status"), status);
}
public static Specification<Order> createdAfter(LocalDate date) {
return (root, query, cb) ->
cb.greaterThan(root.get("createdAt"), date);
}
}
// 필요한 조건만 조합
orderRepository.findAll(
OrderSpecs.hasStatus(PENDING).and(OrderSpecs.createdAfter(startDate))
);
이처럼 각 조건을 독립적인 Specification으로 분리하면, 조건 조합이 달라질 때마다 새 메서드를 추가할 필요가 없어진다. 조건 단위로 테스트도 작성할 수 있어 테스트 유지보수성도 크게 향상된다.
실무 적용 시 고려할 점
Specifications 패턴은 특히 검색/필터링 기능이 많고 조건 조합이 다양한 도메인에서 효과가 크다. 관리자 백오피스, 주문 조회, 사용자 검색처럼 다양한 조건이 복합적으로 사용되는 기능에 적용하면 Repository를 단순하게 유지하면서도 유연한 쿼리 구성이 가능해진다.
다만 Specifications가 만능은 아니다. 조인이 복잡하거나 성능 튜닝이 필요한 쿼리에서는 Criteria API 기반의 Specifications보다 JPQL이나 QueryDSL이 더 명확한 표현과 최적화를 제공할 수 있다. 따라서 단순~중간 복잡도의 동적 쿼리는 Specifications로, 고복잡도 쿼리는 별도로 관리하는 하이브리드 전략이 현실적이다.
기존에 쌓인 Repository Debt를 한 번에 걷어내려 하기보다는, 신규 기능 개발 시점부터 Specifications를 적용하고 리팩터링이 필요한 영역을 점진적으로 전환하는 방식이 리스크를 줄이는 현실적인 접근이다.
정리
- Repository 메서드 폭증은 단순 코드 스멜이 아닌 유지보수 비용과 직결되는 구조적 부채다.
- Spring Data JPA Specifications는 별도 라이브러리 없이 동적 쿼리 조건을 조합 가능하게 해준다.
- 단순~중간 복잡도의 동적 필터링에 우선 적용하고, 고복잡도 쿼리는 QueryDSL과 병행하는 전략이 효과적이다.