Spring - QueryDSL을 통해 페이징처리 구현

TopOfTheHead·2025년 11월 29일

Spring JPA

목록 보기
9/9

QueryDSL

상황설정 : 특정 유저가 주문한 주문상품상품이름에 대해 키워드 검색Paging 처리하여 반환
유저 1 : 주문 N 관계
주문 1 : 주문상품 N 관계
주문상품 N : 상품 1 관계

  • 주문상품에 대한 DTO POJO 객체를 생성
    。해당 주문상품 Entity 객체를 그대로 반환하는 경우 해당 Entity 객체연관관계에 존재하는 Entity가 같이 반환되므로 필요한 Field축약하여 담는 용도의 POJO객체를 생성
    DTO 객체를 정의하는 방법은 총 3가지 존재 정의하는 방법은 총 3가지 존재.
    POJO객체

    @QueryProjectionDTO생성자메서드에 선언하여 컴파일QClass 클래스가 등록되도록 설정
public interface OrderProductResponse {
	@Schema(name = "OrderProductResponse.Search")
	record Search(
		Long id,
		Long orderId,
		String productName,
		Long quantity,
		String receiverName,
		OrderStatus orderStatus,
		LocalDateTime deliveredAt
	){
		@QueryProjection
		public Search{}
	}
}
  • {도메인명}RepositoryCustom 인터페이스 + {도메인명}RepositoryImpl 구현체 생성
    {도메인명}RepositoryImpl 구현체@Repository 선언 및 QueryDSL에 대한 Configuration을 수행하는 클래스에서 정의한 JPAQueryFactory객체의존성주입
public interface OrderProductRepositoryCustom {
	Page<OrderProductResponse.Search> getOrderProductsByKeyword(Long userId, String keyword, Pageable pageable);
}
@Repository
@RequiredArgsConstructor
public class OrderProductRepositoryImpl implements OrderProductRepositoryCustom {
	private final JPAQueryFactory jpaQueryFactory;
	@Override
	public Page<OrderResponse.Search> getOrderProductsByKeyword(Long userId, String keyword, Pageable pageable) {
		// 로직
	}
}
  • 기존 JpaRepository 인터페이스확장하는 Repository 인터페이스에서 해당 {도메인명}RepositoryCustom 인터페이스를 확장
@Repository
public interface OrderProductRepository extends JpaRepository<OrderProductEntity, Long>,  OrderProductRepositoryCustom  {}

{도메인명}RepositoryImpl 구현체에서 내부로직 작성
。내부 Query에 필요한 QClass객체의존성주입

select()절에 데이터를 받을 QClass DTO 객체를 설정

。각 필드에 대한 조건을 정의한 BooleanExpression 객체를 정의 후 JPAQueryFactory객체.where(BooleanExpression객체)에 정의
keyword == null인 경우 where(null)where이 적용되지않도록 설정

Pageable 객체에 포함된 page 번호( getOffset() ) 및 page에 표현될 데이터수(getPageSize())를 통해 해당 범위의 데이터만 가져오기
▶ 향후 return될 Page객체에는 해당 데이터 + 데이터 총 갯수 + Pageable객체를 포함하여 생성 후 반환

@Repository
@RequiredArgsConstructor
public class OrderProductRepositoryImpl implements OrderProductRepositoryCustom {
	private final JPAQueryFactory jpaQueryFactory;
	private final QMemberEntity qMember = QMemberEntity.memberEntity;
	private final QOrderEntity qOrder =  QOrderEntity.orderEntity;
	private final QOrderProductEntity qOrderProduct = QOrderProductEntity.orderProductEntity;
	private final QProductEntity qProduct = QProductEntity.productEntity;
	// 키워드가 null인지 판별 및 null이 아닌경우 해당 Product의 name에 keyword를 포함하는것을 찾음
	public BooleanExpression containsProductName(String keyword){
		return (Strings.isNotBlank(keyword))?
			qProduct.name.containsIgnoreCase(keyword) : null;
	}
	@Override
	public Page<OrderProductResponse.Search> getOrderProductsByKeyword(Long userId, String keyword, Pageable pageable) {
		//
		BooleanBuilder booleanBuilder = new BooleanBuilder()
			.and(containsProductName(keyword))
			.and(qMember.id.eq(userId));
		//
		List<OrderProductResponse.Search> searches = jpaQueryFactory
			.select(new QOrderProductResponse_Search(
				qOrderProduct.id,
				qOrder.id,
				qProduct.name,
				qOrderProduct.quantity,
				qOrder.receiver.name,
				qOrder.orderStatus,
				qOrder.deliveredAt
			)).from(qMember)
			.join(qOrder).on(qMember.id.eq(qOrder.member.id))
			.join(qOrderProduct).on(qOrder.id.eq(qOrderProduct.order.id))
			.join(qProduct).on(qOrderProduct.id.eq(qProduct.id))
			.where(booleanBuilder)
			.orderBy(qOrderProduct.id.desc())
			.offset(pageable.getOffset())
			.limit(pageable.getPageSize())
			.fetch();
		//
		int total = jpaQueryFactory
			.select(new QOrderProductResponse_Search(
				qOrderProduct.id,
				qOrder.id,
				qProduct.name,
				qOrderProduct.quantity,
				qOrder.receiver.name,
				qOrder.orderStatus,
				qOrder.deliveredAt
			)).from(qMember)
			.join(qOrder).on(qMember.id.eq(qOrder.member.id))
			.join(qOrderProduct).on(qOrder.id.eq(qOrderProduct.order.id))
			.join(qProduct).on(qOrderProduct.id.eq(qProduct.id))
			.where(booleanBuilder).fetch().size();
		//
		Page<OrderProductResponse.Search> page =
			new PageImpl<OrderProductResponse.Search>(searches, pageable, total);
		//
		return page;
	}
}
  • Service Layer에서 서비스코드 작성
@RequiredArgsConstructor
@Service
@Transactional	 // 트랜잭션의 원자성 보장
public class MemberServiceImpl implements MemberService {
	private final OrderProductRepository orderProductRepository;
	@Override
	@Transactional
	public Page<OrderProductResponse.Search> getOrderProducts(
		String keyword,
		Pageable pageable,
		Long memberId
	){
		return orderProductRepository.getOrderProductsByKeyword(
			memberId,
			keyword,
			pageable
		);
	}
}
  • @ModelAttribute에 의해 바인딩Paging 관련 DTO 작성 및 컨트롤러 정의
    。해당 DTO 객체컨트롤러매개변수에 선언
    /orders?page=숫자1&size=숫자2Query String을 전달하는 경우 해당 DTO객체Field바인딩
public record Paging(
	@Min(value = 1, message = "Page 번호는 1 이하이어야합니다.") int page,
	@Range(min = 1, max = 20, message = "size는 1 이상 20 이하이어야합니다.") int size
) {
	public Pageable getPageable() { return PageRequest.of(page - 1, size); }
}
// /orders?keyword=문자열&page=숫자&size=숫자 전달되는 경우
	@GetMapping("/orders")
	@ResponseStatus(HttpStatus.OK)
	public ApiResult<Page<OrderProductResponse.Search>> getOrders(
		@RequestParam String keyword,
		@ModelAttribute Paging paging,
		@AuthenticationPrincipal DefaultCurrentUser defaultCurrentUser
	){
		Pageable pageable = paging.toPageable();
		Page<OrderProductResponse.Search> result = memberservice.getOrderProducts(
			keyword,
			pageable,
			defaultCurrentUser.getId()
		);
		return ApiResult.ok(result);
	}
  • 테스트코드 작성
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Transactional
@AutoConfigureMockMvc
class MemberControllerTest {
	@Autowired MemberRepository memberRepository;
	@Autowired OrderRepository orderRepository;
	@Autowired OrderProductRepository orderProductRepository;
	@Autowired ProductRepository productRepository;
	@Autowired MockMvc mockMvc;
	@Autowired ObjectMapper objectMapper;
	MemberEntity testMember;
	OrderEntity testOrder;
	OrderProductEntity testOrderProduct;
	ProductEntity testProduct;
	@BeforeEach
	void setUp() throws Exception {
		testMember = memberRepository.save(
			MemberEntity.normalMember(
				"wjdtn",
				"1234",
				"쩡수",
				"wjdtn747@naver.com",
				"010-7615-6014",
				Gender.MALE,
				LocalDate.now()
			)
		);
		testProduct = productRepository.save(
			ProductEntity.of(
				"계란소시지상품",
				10000L,
				100L
			)
		);
		testOrder = orderRepository.save(
			OrderEntity.of(
				new Receiver(
					"이정수",
					"주소",
					"01076156131"
				),
				testMember
			)
		);
		testOrderProduct = orderProductRepository.save(
			OrderProductEntity.of(
				4L,
				testOrder,
				testProduct
			)
		);
	}
	@Test
	void 키워드_주문상품_검색_성공() throws Exception {
		DefaultCurrentUser defaultCurrentUser = new DefaultCurrentUser(
			testMember.getId(),
			testMember.getLoginId()
		);
		mockMvc.perform(
				get("/members/orders")
					.with(user(defaultCurrentUser))
					.param("keyword", "계란")
					.param("page", "1")
					.param("size", "10")
			).andExpect(status().isOk())
			.andExpect(jsonPath("$.data.content[0].id").value(testOrderProduct.getId()))
			.andExpect(jsonPath("$.data.content[0].orderId").value(testOrder.getId()));
	}
}
  • Page<Entity Class> : org.springframework.data.domain.Page
    클라이언트에게 페이징 요소를 모두 포함한 최종 페이징 결과를 포함하여 반환용도로 활용되는 인터페이스

    Page<Entity> page = new PageImpl<Entity>(List<Entity>객체, Pageable객체 , 총 데이터 수);

    구현체를 통해 페이지 범위에 해당하는 조회된 List<Entity객체>, Pageable ( 페이지 사이즈 , 페이지 번호 ), 총 데이터 수를 포함하여 Page객체를 생성 후 클라이언트에게 반환

    Repository 인터페이스에서 Query method( Page<Entity> findAllByNameContainin(keyword , pageable) )를 정의하여 pageable을 전달하여 Page<Entity>return 받을 수 있다.


    PageImpl<Entity Class> : org.springframework.data.domain.PageImpl
    Page<Entity Class>구현체
    생성자데이터, 총 페이지 수, 총 데이터 수를 전달받아서 객체 생성



  • Pageable 인터페이스 : org.srpingframework.data.domain.Pageable
    페이징페이지번호페이지 사이즈를 포함하는 DTO용도로 사용

    Pageable pageable = PageRequest.of(page-1 , size);

    클라이언트에게 Controller에서 페이지번호페이지 사이즈를 입력받아 구현체를 생성 및 Service로 전달되는 DTO로 활용되거나 조회 후 Page<Entity>에 포함되어 클라이언트에게 반환되는 용도
    페이지1번부터 시작하나 SQL에서 offset 0 * 데이터수부터 시작하므로 페이지-1 처리


    PageRequest : org.srpingframework.data.domain.PageRequest
    Pageable 인터페이스 구현체
    Pageable pageable = PageRequest.of(페이지번호, 페이지사이즈)Pageable 인터페이스 구현체 생성

    생성자protected로 보호되어있으므로 정적팩토리메서드 : of로 생성
profile
공부기록 블로그

0개의 댓글