π― JpaSpecificationExecutor
JpaSpecificationExecutor
λ λμ 쿼리λ₯Ό λ€λ£¨λ λ° μμ΄ λ§€μ° μ€μν κ°λ
μ
λλ€.
π€ JpaSpecificationExecutorλ₯Ό μΆκ°λ‘ μμλ°λ μλ―Έλ?
OrderRepository
κ° JpaSpecificationExecutor
λ₯Ό μΆκ°λ‘ μμλ°λ κ²μ, κΈ°λ³Έ 리ν¬μ§ν 리(JpaRepository
)μ βλμ 쿼리 쑰립 λ₯λ ₯βμ΄λΌλ κ°λ ₯ν μΆκ° κΈ°λ₯μ μ₯μ°©νλ κ²μ μλ―Έν©λλ€.
πͺ μλΉ λΉμ λ‘ μ΄ν΄νκΈ°
π½οΈ JpaRepository = κΈ°λ³Έ λ©λ΄ν
public interface OrderRepository extends JpaRepository<Order, String> {
// π μ ν΄μ§ λ©λ΄λ€
List<Order> findAll(); // "λͺ¨λ μ£Όλ¬Έ μ‘°ν"
Optional<Order> findById(String id); // "IDλ‘ μ£Όλ¬Έ μ°ΎκΈ°"
Order save(Order order); // "μ£Όλ¬Έ μ μ₯"
void deleteById(String id); // "μ£Όλ¬Έ μμ "
}
νΉμ§:
findById
,findAll
,save
λ± μ ν΄μ§ κ·μΉμ λ¨μν μΏΌλ¦¬λ§ μ€ν κ°λ₯
π¨ JpaSpecificationExecutor = μ£Όλ°©μ₯ νΉμ μ£Όλ¬Έ
public interface OrderRepository extends JpaRepository<Order, String>,
JpaSpecificationExecutor<Order> {
// β¨ μ΄μ μΆκ°λ‘ μ¬μ© κ°λ₯ν "λ§μΆ€ν μ£Όλ¬Έ" κΈ°λ₯λ€
List<Order> findAll(Specification<Order> spec);
Page<Order> findAll(Specification<Order> spec, Pageable pageable);
Optional<Order> findOne(Specification<Order> spec);
long count(Specification<Order> spec);
}
νΉμ§: μ ν΄μ§μ§ μμ μ¬λ¬ 쑰건λ€μ μ‘°ν©νμ¬ λ§μΆ€ν 쿼리 μ€ν κ°λ₯
- μ: βνμ μ΄λ¦μ΄ βνβμΌλ‘ μμνκ³ , μ£Όλ¬Έ μνκ° βλ°°μ‘μ€βμ΄λ©°, μ§λμ£Όμ μ£Όλ¬Έν λ΄μβ
π λ³ν μμ½
κ΅¬λΆ | JpaRepositoryλ§ μμ | + JpaSpecificationExecutor μμ |
---|---|---|
쿼리 νμ | β μ μ μΏΌλ¦¬λ§ κ°λ₯ | β λμ 쿼리 쑰립 κ°λ₯ |
쑰건 μ‘°ν© | β 미리 μ μλ λ©μλλ§ | β λ°νμμ μμ λ‘μ΄ μ‘°ν© |
μ μ°μ± | β μ νμ | β 무νν νμ₯μ± |
π οΈ μμμΌλ‘ μΈν ꡬ체μ λ³κ²½μ¬ν
κ°μ₯ ν° λ³κ²½μ μ OrderRepository
λ₯Ό μ¬μ©νλ μλΉμ€ κ³μΈ΅μμ μλ‘μ΄ λ©μλλ€μ μ¬μ©ν μ μκ² λλ€λ κ²μ
λλ€.
π μΆκ°λλ ν΅μ¬ λ©μλλ€
JpaSpecificationExecutor
μΈν°νμ΄μ€κ° OrderRepository
μ μΆκ°ν΄μ£Όλ λνμ μΈ λ©μλλ€:
// π 쑰건μ λ§λ λͺ¨λ κ²°κ³Ό μ‘°ν
List<Order> findAll(Specification<Order> spec);
// π 쑰건μ λ§λ κ²°κ³Όλ₯Ό νμ΄μ§νμ¬ μ‘°ν
Page<Order> findAll(Specification<Order> spec, Pageable pageable);
// π― 쑰건μ λ§λ 첫 λ²μ§Έ κ²°κ³Ό μ‘°ν
Optional<Order> findOne(Specification<Order> spec);
// π 쑰건μ λ§λ κ²°κ³Ό κ°μ μ‘°ν
long count(Specification<Order> spec);
// β
쑰건μ λ§λ κ²°κ³Όκ° μ‘΄μ¬νλμ§ νμΈ
boolean exists(Specification<Order> spec);
πͺ ν΅μ¬ νΉμ§: Specification<Order>
νλΌλ―Έν°
λͺ¨λ λ©μλκ° Specification<Order>
νμ
μ κ°μ²΄λ₯Ό νλΌλ―Έν°λ‘ λ°λλ€λ μ μ΄ κ°μ₯ μ€μν©λλ€.
// β μ΄μ : μ ν΄μ§ λ©μλλ§ νΈμΆ κ°λ₯
List<Order> orders = orderRepository.findAll();
// β
μ΄ν: λμ 쑰건μ λ΄μ SpecificationμΌλ‘ μμ λ‘μ΄ μΏΌλ¦¬ μ€ν
Specification<Order> spec = OrderSpecification.withMemberName("νκΈΈλ")
.and(OrderSpecification.withShippingStatus(SHIPPED));
List<Order> orders = orderRepository.findAll(spec);
π λ² μ€νΈ νλν°μ€: μ€λ¬΄ μμ μ½λ
ν΅μ¬ μμΉ: βμ¬μ¬μ© κ°λ₯ν κ²μ 쑰건λ€μ λ³λμ Specification ν΄λμ€λ‘ λΆλ¦¬νμ¬ κ΄λ¦¬νκΈ°β
μ΄λ κ² νλ©΄ μλΉμ€ λ‘μ§μ κ²μ μ‘°κ±΄μ΄ μ΄λ»κ² λ§λ€μ΄μ§λμ§μ λν 볡μ‘ν λ΄μ©κ³Ό λΆλ¦¬λμ΄ ν¨μ¬ κΉλν΄μ§λλ€.
π§ Step 1: Specification ν΄λμ€ μμ± (Best Practice)
κ²μ 쑰건λ€μ μ μνλ OrderSpecification
ν΄λμ€λ₯Ό μλ‘ λ§λλλ€. κ° μ‘°κ±΄μ μ¬μ¬μ© κ°λ₯νλλ‘ static
λ©μλλ‘ μ μν©λλ€.
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)
);
};
}
// π° μ΅μ μ£Όλ¬Έ κΈμ‘μΌλ‘ κ²μνλ Specification
public static Specification<Order> withMinAmount(Integer minAmount) {
return (root, query, criteriaBuilder) -> {
if (minAmount == null) return null;
return criteriaBuilder.greaterThanOrEqualTo(root.get("totalAmount"), minAmount);
};
}
// β VIP νμ μ£Όλ¬Έ κ²μνλ Specification
public static Specification<Order> isVipOrder() {
return (root, query, criteriaBuilder) -> {
Join<Order, Member> memberJoin = root.join("member");
return criteriaBuilder.equal(memberJoin.get("memberType"), "VIP");
};
}
}
π¨ Step 2: Serviceμμ Specification μ‘°ν©νμ¬ μ¬μ©
μ΄μ OrderServiceImpl
μμλ private
λ©μλλ‘ κ²μ λ‘μ§μ μ§μ ꡬννλ λμ , μλ‘ λ§λ 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.domain.Page;
import org.springframework.data.domain.Pageable;
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()))
.and(OrderSpecification.withMinAmount(searchRequest.getMinAmount()));
// β¨ JpaSpecificationExecutorκ° μ 곡νλ findAll(spec) λ©μλ μ¬μ©
return orderRepository.findAll(spec).stream()
.map(OrderResponse::from)
.collect(Collectors.toList());
}
// π νμ΄μ§ μ²λ¦¬κ° ν¬ν¨λ κ²μ λ©μλ μΆκ°
@Override
@Transactional(readOnly = true)
public Page<OrderResponse> getOrdersWithPaging(OrderSearchRequest searchRequest, Pageable pageable) {
Specification<Order> spec = Specification
.where(OrderSpecification.withMemberName(searchRequest.getMemberName()))
.and(OrderSpecification.withShippingStatus(searchRequest.getShippingStatus()))
.and(OrderSpecification.betweenDates(searchRequest.getStartDate(), searchRequest.getEndDate()));
// β¨ νμ΄μ§κΉμ§ μ§μνλ findAll(spec, pageable) λ©μλ μ¬μ©
return orderRepository.findAll(spec, pageable)
.map(OrderResponse::from);
}
// π 볡μ‘ν 쑰건μ κ²μ μμ
@Override
@Transactional(readOnly = true)
public List<OrderResponse> getVipOrders(String memberName) {
Specification<Order> spec = Specification
.where(OrderSpecification.withMemberName(memberName))
.and(OrderSpecification.isVipOrder())
.or(OrderSpecification.withMinAmount(100000)); // 10λ§μ μ΄μ μ£Όλ¬Έ
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 λ©μλλ μ΄μ μμ !
}
π 리ν©ν λ§ μ ν λΉκ΅
β 리ν©ν λ§ μ : 볡μ‘ν μλΉμ€ λ‘μ§
@Service
public class OrderServiceImpl {
// π΄ μλΉμ€μ 볡μ‘ν 쿼리 μμ± λ‘μ§μ΄ μμ¬ μμ
private List<Order> searchOrders(OrderSearchRequest request) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Order> query = cb.createQuery(Order.class);
Root<Order> root = query.from(Order.class);
List<Predicate> predicates = new ArrayList<>();
if (StringUtils.hasText(request.getMemberName())) {
Join<Order, Member> memberJoin = root.join("member");
predicates.add(cb.like(memberJoin.get("memberName"), "%" + request.getMemberName() + "%"));
}
if (request.getShippingStatus() != null) {
predicates.add(cb.equal(root.get("shippingStatus"), request.getShippingStatus()));
}
// ... λ 볡μ‘ν λ‘μ§λ€
query.where(predicates.toArray(new Predicate[0]));
return entityManager.createQuery(query).getResultList();
}
}
β 리ν©ν λ§ ν: κΉλν μ μΈμ μ½λ
@Service
public class OrderServiceImpl {
// π’ μ μΈμ μ΄κ³ λͺ
νν μλ νν
public List<OrderResponse> getAllOrders(OrderSearchRequest searchRequest) {
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());
}
}
π‘ μ€λ¬΄ νμ© ν
π― 볡μ‘ν 쑰건 μ‘°ν© μμ
// πͺ μ€λ¬΄μμ μμ£Ό μ¬μ©λλ 볡μ‘ν κ²μ 쑰건
public List<OrderResponse> getComplexOrderSearch(OrderSearchRequest request) {
Specification<Order> spec = Specification
.where(OrderSpecification.withMemberName(request.getMemberName()))
.and(OrderSpecification.withShippingStatus(request.getStatus()))
.and(OrderSpecification.betweenDates(request.getStartDate(), request.getEndDate()))
.and(OrderSpecification.withMinAmount(request.getMinAmount()))
.or(OrderSpecification.isVipOrder())
.and(Specification.not(OrderSpecification.withShippingStatus(ShippingStatus.CANCELLED)));
return orderRepository.findAll(spec).stream()
.map(OrderResponse::from)
.collect(Collectors.toList());
}
π§ μ±λ₯ μ΅μ ν ν
// π μΉ΄μ΄νΈ 쿼리 μ΅μ ν
public long getOrderCount(OrderSearchRequest request) {
Specification<Order> spec = Specification
.where(OrderSpecification.withMemberName(request.getMemberName()))
.and(OrderSpecification.withShippingStatus(request.getStatus()));
return orderRepository.count(spec); // β¨ count μ μ© λ©μλ νμ©
}
// π νμ΄μ§ μ±λ₯ μ΅μ ν
public Page<OrderResponse> getOrdersWithOptimizedPaging(OrderSearchRequest request, Pageable pageable) {
Specification<Order> spec = Specification
.where(OrderSpecification.withMemberName(request.getMemberName()));
return orderRepository.findAll(spec, pageable) // β¨ νμ΄μ§ μ΅μ νλ 쿼리
.map(OrderResponse::from);
}
π§ͺ λ¨μ ν μ€νΈ μμ±
@Test
public void testOrderSpecification() {
// π§© κ° Specificationμ λ
립μ μΌλ‘ ν
μ€νΈ κ°λ₯
Specification<Order> spec = OrderSpecification.withMemberName("νκΈΈλ");
List<Order> orders = orderRepository.findAll(spec);
assertThat(orders).allMatch(order ->
order.getMember().getMemberName().contains("νκΈΈλ"));
}
π κ²°λ‘
JpaSpecificationExecutor
λ JpaRepositoryμ κΈ°λ³Έ κΈ°λ₯μ κ°λ ₯ν λμ 쿼리 쑰립 λ₯λ ₯μ μΆκ°ν΄μ£Όλ ν΅μ¬ μΈν°νμ΄μ€μ
λλ€.
π― ν΅μ¬ κ°μΉ
- π§© λ κ³ λΈλ‘μ μ‘°ν©: κ²μ 쑰건λ€μ μμ μμ¬λ‘ 쑰립
- π μ μΈμ μ½λ: λΉμ¦λμ€ μλκ° λͺ ννκ² λλ¬λλ μ½λ
- π λμ μ¬μ¬μ©μ±: ν λ² λ§λ Specificationμ μ¬λ¬ κ³³μμ νμ©
- π‘οΈ κ΄μ¬μ¬ λΆλ¦¬: 볡μ‘ν 쿼리 μμ± λ‘μ§μ μλΉμ€μμ λΆλ¦¬
- π νμ΄μ§ μ§μ: λμ©λ λ°μ΄ν° μ²λ¦¬λ₯Ό μν ν¨μ¨μ μΈ νμ΄μ§
π μ μ© ν¨κ³Ό
μ΄μ²λΌ Specificationμ λ³λ ν΄λμ€λ‘ λΆλ¦¬νλ©΄, μλΉμ€ κ³μΈ΅μ βμ΄λ€ 쑰건μΌλ‘ κ²μνλμ§βλ§ μ μΈμ μΌλ‘ λͺ μνλ©΄ λλ―λ‘ μ½λκ° ν¨μ¬ λ κΉλν΄μ§κ³ , κ° Specificationμ λ€λ₯Έ κ³³μμλ μ¬μ¬μ©ν μ μκ² λμ΄ μ μ§λ³΄μμ±μ΄ ν¬κ² ν₯μλ©λλ€!
볡μ‘ν κ²μ κΈ°λ₯μ΄ νμν μ€λ¬΄ νλ‘μ νΈμμλ λ°λμ κ³ λ €ν΄λ³Ό λ§ν κ°λ ₯ν λꡬμ λλ€.