ποΈ Spring Data JPA Specification
Spring Data JPA Specificationμ λμ 쿼리λ₯Ό λ§€μ° μ°μνκ³ κ°μ²΄μ§ν₯μ μΌλ‘ μ²λ¦¬ν μ μλ κ°λ ₯ν λꡬμ λλ€.
π€ Spring Data JPA Specificationμ΄λ?
JPA Specification(μ¬μ) μ΄λ, μ¬λ¬ κ²μ 쑰건μ βκ°μ²΄βμ²λΌ 쑰립νμ¬ λμ 쿼리λ₯Ό μμ±νκ² ν΄μ£Όλ νλ‘κ·Έλλ° μΈν°νμ΄μ€(API) μ λλ€.
β¨ ν΅μ¬ νΉμ§
- 볡μ‘ν
if-else
λ¬Έμ΄λ λ¬Έμμ΄ μΏΌλ¦¬ μ‘°ν© μμ - νμ μ μμ ν(Type-safe) μ½λλ‘ κ²μ 쑰건 μ²λ¦¬
- νμν κ²μ 쑰건λ€μ λ§μΉ λ κ³ λΈλ‘μ²λΌ λΆμλ€ λλ€ κ°λ₯
- λ΄λΆμ μΌλ‘λ JPAμ Criteria APIλ₯Ό μ¬μ©νμ¬ λμ
π μ§κ΄μ λΉμ : λ§μΆ€ν νΌμ μ£Όλ¬Έ
Specificationμ λ§μΉ νΌμ κ°κ²μμ ν νμ μμ λ‘κ² μΆκ°νκ³ λΉΌλ κ²κ³Ό κ°μ΅λλ€.
β μ ν΅μ μΈ λ°©μ (Repository λ©μλ νλ°)
// λͺ¨λ κ²½μ°μ μλ₯Ό λ©μλλ‘ λ§λ€μ΄μΌ ν¨
List<Order> findByMemberName(String memberName);
List<Order> findByShippingStatus(ShippingStatus status);
List<Order> findByMemberNameAndShippingStatus(String memberName, ShippingStatus status);
List<Order> findByMemberNameAndShippingStatusAndCreatedAtBetween(...);
// π΄ μ‘°ν©μ΄ λμ΄λ μλ‘ λ©μλκ° κΈ°νκΈμμ μΌλ‘ μ¦κ°!
λ¬Έμ μ : βννΌλ‘λλ λΉΌκ³ μ¬λ¦¬λΈμ λ²μ―μ μΆκ°ν΄μ£ΌμΈμβ κ°μ 볡μ‘ν μμ²μ λ©λ΄μ μμΌλ©΄ μ²λ¦¬νκΈ° νλλλ€.
β Specification λ°©μ (λμ μ‘°ν©)
// κ²μ 쑰건λ€μ λ κ³ λΈλ‘μ²λΌ μμ λ‘κ² μ‘°ν©
Specification<Order> spec = Specification
.where(OrderSpecification.withMemberName(memberName)) // π§© ν ν 1
.and(OrderSpecification.withShippingStatus(status)) // π§© ν ν 2
.and(OrderSpecification.betweenDates(startDate, endDate)); // π§© ν ν 3
List<Order> orders = orderRepository.findAll(spec); // π λ§μΆ€ν νΌμ μμ±!
μ₯μ : μλ(μλΉμ€ κ³μΈ΅)μ΄ ν ν(Specification κ°μ²΄)λ€μ κ³ λ₯΄λ©΄, μ£Όλ°©μ₯(JPA)μ΄ μ¦μμμ λ§μΆ€ν νΌμ(λμ 쿼리)λ₯Ό λ§λ€μ΄ μ€λλ€.
π λ² μ€νΈ νλν°μ€: μ€λ¬΄ μμ μ½λ
κ°μ₯ ν¨κ³Όμ μΈ Specification μ¬μ©λ²μ βμ¬μ¬μ© κ°λ₯ν Specificationλ€μ λ³λμ ν΄λμ€λ‘ λΆλ¦¬νμ¬ κ΄λ¦¬νλ κ²β μ λλ€.
π Step 1: Specification ν΄λμ€ μμ± (Best Practice)
κ²μ 쑰건λ€μ μ μνλ OrderSpecification
ν΄λμ€λ₯Ό μλ‘ λ§λλλ€.
repository/specification/OrderSpecification.java
(μ κ· νμΌ)
package com.kobe.productmanagement.repository.specification;
import com.kobe.productmanagement.common.ShippingStatus;
import com.kobe.productmanagement.domain.Member;
import com.kobe.productmanagement.domain.Order;
import jakarta.persistence.criteria.Join;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalTime;
public class OrderSpecification {
// π νμ μ΄λ¦μΌλ‘ κ²μνλ Specification
public static Specification<Order> withMemberName(String memberName) {
return (root, query, criteriaBuilder) -> {
// π’ null/λΉ λ¬Έμμ΄ μ²΄ν¬λ‘ μμ μ± ν보
if (!StringUtils.hasText(memberName)) {
return null; // μ‘°κ±΄μ΄ μμΌλ©΄ 무μλ¨
}
Join<Order, Member> memberJoin = root.join("member");
return criteriaBuilder.like(memberJoin.get("memberName"), "%" + memberName + "%");
};
}
// π¦ λ°°μ‘ μνλ‘ κ²μνλ Specification
public static Specification<Order> withShippingStatus(ShippingStatus status) {
return (root, query, criteriaBuilder) -> {
if (status == null) {
return null;
}
return criteriaBuilder.equal(root.get("shippingStatus"), status);
};
}
// π
μ£Όλ¬Έ λ μ§ λ²μλ‘ κ²μνλ Specification
public static Specification<Order> betweenDates(LocalDate startDate, LocalDate endDate) {
return (root, query, criteriaBuilder) -> {
if (startDate == null || endDate == null) {
return null;
}
return criteriaBuilder.between(
root.get("createdAt"),
startDate.atStartOfDay(),
endDate.atTime(LocalTime.MAX)
);
};
}
}
π§ Step 2: Serviceμμ Specification μ‘°ν©νμ¬ μ¬μ©
μ΄μ OrderServiceImpl
μμλ μλ‘ λ§λ OrderSpecification
μ λ©μλλ€μ μ‘°ν©νμ¬ ν¨μ¬ λ κΉλνκ² μΏΌλ¦¬λ₯Ό μ€νν μ μμ΅λλ€.
service/OrderServiceImpl.java
(리ν©ν λ§)
package com.kobe.productmanagement.service;
import com.kobe.productmanagement.domain.Order;
import com.kobe.productmanagement.dto.request.OrderSearchRequest;
import com.kobe.productmanagement.dto.request.OrderStatusUpdateRequest;
import com.kobe.productmanagement.dto.response.OrderResponse;
import com.kobe.productmanagement.repository.OrderRepository;
import com.kobe.productmanagement.repository.specification.OrderSpecification; // π’ μ κ· import
import lombok.RequiredArgsConstructor;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Transactional
public class OrderServiceImpl implements OrderService {
private final OrderRepository orderRepository;
@Override
@Transactional(readOnly = true)
public List<OrderResponse> getAllOrders(OrderSearchRequest searchRequest) {
// π§© Specificationλ€μ λ κ³ λΈλ‘μ²λΌ μ²΄μΈ ννλ‘ μ‘°ν©
Specification<Order> spec = Specification
.where(OrderSpecification.withMemberName(searchRequest.getMemberName()))
.and(OrderSpecification.withShippingStatus(searchRequest.getShippingStatus()))
.and(OrderSpecification.betweenDates(searchRequest.getStartDate(), searchRequest.getEndDate()));
return orderRepository.findAll(spec).stream()
.map(OrderResponse::from)
.collect(Collectors.toList());
}
@Override
public OrderResponse updateOrderState(String orderId, OrderStatusUpdateRequest request) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("ν΄λΉ μ£Όλ¬Έμ μ°Ύμ μ μμ΅λλ€ ID: " + orderId));
order.updateDetails(request.getStatus());
return OrderResponse.from(order);
}
// ποΈ κΈ°μ‘΄μ 볡μ‘ν private searchOrders λ©μλλ μ΄μ μμ !
}
π― μ΄ λ°©μμ ν΅μ¬ μ₯μ
1. π μ¬μ¬μ©μ± (Reusability)
// λ€λ₯Έ μλΉμ€μμλ λμΌν Specification μ¬μ¬μ© κ°λ₯
public class AdminService {
public List<Order> getVipOrders() {
return orderRepository.findAll(
OrderSpecification.withShippingStatus(ShippingStatus.DELIVERED)
.and(OrderSpecification.withMemberName("VIP"))
);
}
}
2. π κ°λ μ± (Readability)
// β κΈ°μ‘΄: 볡μ‘ν 쿼리 μμ± λ‘μ§μ΄ μλΉμ€μ μμ
if (memberName != null) {
// 볡μ‘ν Criteria API μ½λ...
}
if (status != null) {
// λ λ€λ₯Έ 볡μ‘ν μ½λ...
}
// β
κ°μ : μ μΈμ μ΄κ³ λͺ
νν μλ νν
Specification<Order> spec = Specification
.where(OrderSpecification.withMemberName(memberName))
.and(OrderSpecification.withShippingStatus(status));
3. π§© μ‘°ν©μ μ μ°μ± (Flexibility)
// πͺ 볡μ‘ν 쑰건λ μμ μμ¬λ‘ μ‘°ν© κ°λ₯
Specification<Order> complexSpec = Specification
.where(OrderSpecification.withMemberName("κΉμ² μ"))
.and(OrderSpecification.withShippingStatus(ShippingStatus.SHIPPED))
.or(OrderSpecification.betweenDates(startDate, endDate))
.and(Specification.not(OrderSpecification.withShippingStatus(ShippingStatus.CANCELLED)));
π μ ν΅μ λ°©μ vs Specification λΉκ΅
κ΅¬λΆ | μ ν΅μ λ°©μ | Specification λ°©μ |
---|---|---|
λ©μλ μ | β μ‘°ν©λ³λ‘ λ©μλ νλ° | β μ¬μ¬μ© κ°λ₯ν μ‘°κ°λ€ |
μ μ§λ³΄μ | β 쑰건 μΆκ° μ λ©μλ μ¦κ° | β Specificationλ§ μΆκ° |
κ°λ μ± | β 볡μ‘ν 쿼리 μμ± λ‘μ§ | β μ μΈμ μ΄κ³ λͺ νν μλ |
νμ μμ μ± | β λ¬Έμμ΄ κΈ°λ° μΏΌλ¦¬ μν | β μ»΄νμΌ νμ μ²΄ν¬ |
ν μ€νΈ | β κ° λ©μλλ³ κ°λ³ ν μ€νΈ | β Specification λ¨μ ν μ€νΈ |
π‘ μ€λ¬΄ ν
π¨ λ€μ΄λ° 컨벀μ
// β
μ’μ λ€μ΄λ°: μλκ° λͺ
νν¨
OrderSpecification.withMemberName()
OrderSpecification.betweenDates()
OrderSpecification.hasShippingStatus()
// β λμ λ€μ΄λ°: μλκ° λΆλΆλͺ
OrderSpecification.memberName()
OrderSpecification.dates()
OrderSpecification.status()
π null 체ν¬λ νμ
// π’ νμ null 체ν¬λ₯Ό ν΅ν΄ μμ μ± ν보
public static Specification<Order> withMemberName(String memberName) {
return (root, query, criteriaBuilder) -> {
if (!StringUtils.hasText(memberName)) {
return null; // null λ°ν μ ν΄λΉ 쑰건μ 무μλ¨
}
// μ€μ 쑰건 λ‘μ§...
};
}
πͺ 볡μ‘ν 쑰건 μ‘°ν© μμ
// μ€λ¬΄μμ μμ£Ό μ¬μ©λλ 볡μ‘ν κ²μ 쑰건
Specification<Order> spec = Specification
.where(OrderSpecification.withMemberName(searchRequest.getMemberName()))
.and(OrderSpecification.withShippingStatus(searchRequest.getStatus()))
.and(OrderSpecification.betweenDates(searchRequest.getStartDate(), searchRequest.getEndDate()))
.and(OrderSpecification.withMinAmount(searchRequest.getMinAmount()))
.or(OrderSpecification.isVipOrder());
π κ²°λ‘
Spring Data JPA Specificationμ 볡μ‘ν λμ 쿼리λ₯Ό μ°μνκ³ κ°μ²΄μ§ν₯μ μΌλ‘ μ²λ¦¬ν μ μλ μ΅κ³ μ μ루μ μ€ νλμ λλ€.
π― ν΅μ¬ κ°μΉ
- π§© λ κ³ λΈλ‘μ μ‘°ν©: νμν κ²μ 쑰건λ€μ μμ μμ¬λ‘ 쑰립
- π‘οΈ νμ μμ μ±: μ»΄νμΌ νμμ μ€λ₯ λ°κ²¬ κ°λ₯
- π μ μΈμ μ½λ: λΉμ¦λμ€ μλκ° λͺ ννκ² λλ¬λλ μ½λ
- π λμ μ¬μ¬μ©μ±: ν λ² λ§λ Specificationμ μ¬λ¬ κ³³μμ νμ©
볡μ‘ν κ²μ κΈ°λ₯μ΄ νμν μ€λ¬΄ νλ‘μ νΈμμλ λ°λμ κ³ λ €ν΄λ³Ό λ§ν κ°λ ₯ν λꡬμ λλ€!