본문 바로가기
프로젝트/개인 프로젝트 V1

(6) 페이징 처리

by Thumper 2022. 8. 2.

메인 페이지에 보여줄 상품 페이징 구현에 대해 정리해보자 한다.

Spring Data 의  Pageble 과 Page를 Querydsl과 함께 사용해보도록 하자.

 

요약하자면 

  • SpringData에서 Pageable은 페이지 요청에 대한 데이터를 담을때 사용하는 인터페이스이다.
  • 응답할때는 Page를 사용한다.    
  •  where 절 을 통해 동적쿼리를 만든다.
  • 검색 키워드를 담을 ItemSearchCondition(DTO), 반환되는 데이터를 담을 MainItemDto(DTO)를 만들었다.

 


사용자 정의 리포지토리를 만든다.

  • 사용자 정의 인터페이스 구현
  • 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속

 

 

먼저 리파지토리에 JPAQueryFactory를 등록한다.

생성자에서 직접 생성하여 주입해준다.

public class ItemRepositoryCustomImpl implements ItemRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    public ItemRepositoryCustomImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }
    ....

 

 

 

MainItemDto  : 반환되는 데이터를 담는다.

  •  @QueryProjection을 통해서 Dto 조회를 하려한다.
  • Dto 생성후 gradle에서 compileQueryDsl로 Q타입 객체가 생성되 도록 해야한다.
@Data
public class MainItemDto {

    private Long id;
    private String itemName;
    private Integer price;
    private Integer quantity;
    private String imgName;
    private String itemType;
    private String categoryType;

    @QueryProjection
    public MainItemDto(Long id, String itemName, Integer price, Integer quantity, String imgName, String itemType, String categoryType) {
        this.id = id;
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
        this.imgName = imgName;
        this.itemType = itemType;
        this.categoryType = categoryType;
    }
}

 

 

 

 

 

 

ItemSearchCondition : 검색 키워드를 담는다.

  • 검색 조건을 담는 Dto이다.
  • 각 필드가 조건의 파라미터가 된다.
@Data
public class ItemSearchCondition {
    private String itemName;
    private String categoryType;
    private User loginMember;
}

 

 

 

 

where 절 파라미터를 통한 동적쿼리를 만든다.

  • where 절에 직접 만든 메서드를 통해 컨디션을 추가한다 .
  • 메서드에서는 컨디션의 null 여부를 따져 검색조건을 반환한다. 

BooleanExpression

  • and 와 or 같은 메소드들을 이용해서 BooleanExpression 을 조합해서 새로운 BooleanExpression 을 만들 수 있다. 
  • 그러므로 재사용성이 높다.
  • BooleanExpression 은 null 을 반환하게 되면 Where 절에서 조건이 무시되기 때문에 안전하다.
	private BooleanExpression itemNameContains(String itemName) {
        return StringUtils.hasText(itemName) ? item.itemName.contains(itemName) : null;
    }

    private BooleanExpression categoryTypeContains(String categoryType) {
        return StringUtils.hasText(categoryType) ? item.categoryType.contains(categoryType) : null;
    }

 

 

 

 

 

 

 

조회할땐 Entity 보다는 Dto 를 우선적으로 가져오기

Dto 조회할때 필요한 칼럼만 가져온다.

필요한 칼럼만 조회하도록 하고 Entity 를 조회하면 그에따른 OneToOne 관계에서는 N+1 문제가 발생할 수도 있으니까

조심하도록 한다.

 

PageableExecutionUtils를 사용하여 응답한다.

  • 컨텐츠를 가져오는 쿼리와 카운트 쿼리를 분리한다.
  • 컨텐츠를 가져오는 쿼리는 fetch()로 한다.
  • 카운트 쿼리는 fetch()하지 않는다.
  • PageableExecutionUtils.getPage()는 PageImpl과 같은 역할을 한다.
  • 마지막 인자로 함수를 전달하는데 내부 작동에 의해서 토탈카운트가 페이지사이즈 보다 적거나 , 마지막페이지 일 경우 해당 함수를 실행하지 않아 쿼리를 조금더 줄일 수 있다.
  • PageableExecutionUtils.getPage()을 사용하면 조금 더 성능 최적화가 된다.
  • 쿼리하나가 아쉬울 때는 PageImpl 보다는 PageableExecutionUtils.getPage를 사용하도록 한다.
    @Override
    public Page<MainItemDto> searchPageSort(ItemSearchCondition condition, Pageable pageable) {
        List<MainItemDto> content = queryFactory
                .select(new QMainItemDto(
                        item.id.as("itemId"),
                        item.itemName.as("itemName"),
                        item.price,
                        item.stockQuantity,
                        itemImg.imgName,
                        item.itemType,
                        item.categoryType)
                       )
                .from(itemImg)
                .join(itemImg.item, item)
                .where(itemImg.reImgYn.eq("Y"))
                .where(itemNameContains(condition.getItemName()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        JPAQuery<MainItemDto> query = queryFactory
                .select(new QMainItemDto(
                        item.id.as("itemId"),
                        item.itemName.as("itemName"),
                        item.price,
                        item.stockQuantity,
                        itemImg.imgName,
                        item.itemType,
                        item.categoryType)
                       )
                .from(itemImg)
                .join(itemImg.item, item);

        return PageableExecutionUtils.getPage(content, pageable, ()->query.fetch().size());

    }

 

 

 

 

 


 

느낀점/ 보완사항

  • Querydsl 사용하는 부분에서 성능 최적화할 수 있는지 고민해본다.
  • 파라미터 null값 예외처리를 추가해야 한다. 
  • 카테고리별로 상품 페이징하기, 회원별 등록상품 페이징하기도 위의 코드와 같은 방식이다.
  • 카테고리별로 상품 페이징하는 부분에서 아쉬운점이 있다.

 

 

아쉬운 점

  • rest api 접근 방식 규칙을 지키지 못한 점이 많다.
  • ItemCategory, ItemType을 @Data로 등록하는 방식 말고 다른 방식으로 교체가능한지 고민이다.
  • Item의 정보 중 ItemCategory(상품 카테고리), ItemType(상품 상태등급)을 컨트롤러에서 @ModelAttribute으로 등록해 사용하는 방법이 좋은건지 모르겠다.
//ItemCategory
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ItemCategory {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "category_id")
    private Long category_id;

    private String code;
    private String categoryName;

    public ItemCategory(String code, String categoryName) {
        this.code = code;
        this.categoryName = categoryName;
    }
}
//controller --->
    @ModelAttribute("itemCategories")
    public List<ItemCategory> itemCategoryList() {
        List<ItemCategory> itemCategories = new ArrayList<>();
        itemCategories.add(new ItemCategory("BOOK", "책"));
        itemCategories.add(new ItemCategory("MUSIC", "음반"));
        return itemCategories;
    }

댓글