☘️ λ°±μ—”λ“œ: Backend

Spring λ¬΄ν•œμŠ€ν¬λ‘€ κ΅¬ν˜„ (2) - μ˜€ν”„μ…‹ 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜

🐀 쀀콩이 2023. 1. 1. 10:36
이전 κΈ€μ—μ„œ λ¬΄ν•œμŠ€ν¬λ‘€, μ»€μ„œ 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜μ— λŒ€ν•΄ μ†Œκ°œν–ˆμŠ΅λ‹ˆλ‹€.

 

 

πŸ€” μ˜€ν”„μ…‹ κΈ°λ°˜μ€ μ–Έμ œ μ‚¬μš©ν• κΉŒ?

 

이전 κΈ€μ—μ„œ μ–ΈκΈ‰ν–ˆλ˜ λŒ€λ‘œ μ˜€ν”„μ…‹ 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜μ€ μ»€μ„œ 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜μ— λΉ„ν•΄ μ„±λŠ₯상 λ–¨μ–΄μ§‘λ‹ˆλ‹€.

 

ν•˜μ§€λ§Œ μ΄λŠ” offset κ°’이 컀짐에 따라 λ°œμƒν•˜λŠ” 단점이기 λ•Œλ¬Έμ— μ‘°νšŒν•  λ°μ΄ν„°μ˜ 양이

λ§Žμ§€ μ•Šλ‹€λ©΄ μ˜€ν”„μ…‹ 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜λ„ μΆ©λΆ„νžˆ ν™œμš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

μ»€μ„œ 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜μ€ μ •λ ¬ 쑰건이 λ³΅μž‘ν•΄μ§€λ©΄ λ³΅μž‘ν•΄μ§ˆμˆ˜λ‘ μ»€μ„œλ₯Ό μ„ μ •ν•˜λŠ”λ° 어렀움이 μžˆμŠ΅λ‹ˆλ‹€.

ν•˜μ§€λ§Œ μ˜€ν”„μ…‹ 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜μ€ Pageable μΈν„°νŽ˜μ΄μŠ€λ₯Ό ν™œμš©ν•΄ 데이터 정렬을 νŽΈλ¦¬ν•˜κ²Œ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

πŸ‘‰ μ‘°νšŒν•  데이터가 λ§Žμ§€ μ•Šκ±°λ‚˜ μ •λ ¬ 쑰건이 λ³΅μž‘ν•  경우 μ˜€ν”„μ…‹ 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜μ„ ν™œμš©ν•΄λ³΄μž.

 

 

πŸ›  μ˜€ν”„μ…‹ 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜ κ΅¬ν˜„

 

μ˜€ν”„μ…‹ 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜μ„ ν™œμš©ν•œ μ‹€μ œ μ»¨νŠΈλ‘€λŸ¬μž…λ‹ˆλ‹€.
@ApiOperation("[인증] νŠΉμ • 쑰건에 ν•΄λ‹Ήν•˜λŠ” ν‹°μΌ“ λͺ©λ‘μ„ νŽ˜μ΄μ§€λ„€μ΄μ…˜μœΌλ‘œ μ‘°νšŒν•©λ‹ˆλ‹€.")
@Auth
@GetMapping("/v1/ticket")
public ApiResponse<TicketPagingResponse> retrieveTickets(
		@Valid RetrieveTicketsRequestDto request,
    		@AllowedSortProperties({"createdAt", "rating"}) Pageable pageable,
    		@ApiIgnore @UserId Long userId) {
    return ApiResponse.success(SuccessCode.READ_TICKET_SUCCESS, ticketRetrieveService.retrieveTicketsUsingPaging(request, pageable, userId));
}

 

  • μœ„ μš”μ²­μ€ 티켓을 μΉ΄ν…Œκ³ λ¦¬λ‘œ 필터링 ν•œ ν›„, 생성 μ‹œκ°„, ν‰μ μœΌλ‘œ μ •λ ¬ν•΄μ„œ νŽ˜μ΄μ§€λ„€μ΄μ…˜μœΌλ‘œ μ‘°νšŒν•˜λŠ” μš”μ²­μž…λ‹ˆλ‹€.
  • μ»¨νŠΈλ‘€λŸ¬μ—μ„œ μœ„μ™€ 같이 Pageable μΈν„°νŽ˜μ΄μŠ€λ₯Ό νŒŒλΌλ―Έν„°λ‘œ 받을 수 μžˆμŠ΅λ‹ˆλ‹€.그러면 λ‹€μŒκ³Ό 같이 page , size , sort νŒŒλΌλ―Έν„°λ₯Ό μž…λ ₯λ°›κ²Œ λ©λ‹ˆλ‹€.localhost:8080/v1/ticket?category=&page=&size=&sort=

 

 

ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ 전달할 dto인 TicketPagingResponse ν΄λž˜μŠ€μž…λ‹ˆλ‹€.
@ToString
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TicketPagingResponse {

    private static final long LAST_PAGE = -1L;

    private List<TicketInfoResponse> contents = new ArrayList<>();
    private long lastPage;
    private long nextPage;

    private TicketPagingResponse(List<TicketInfoResponse> contents, long lastPage, long nextPage) {
        this.contents = contents;
        this.lastPage = lastPage;
        this.nextPage = nextPage;
    }

    public static TicketPagingResponse of(Page<Ticket> ticketPaging) {
        if (!ticketPaging.hasNext()) {
            return TicketPagingResponse.newLastScroll(ticketPaging.getContent(), ticketPaging.getTotalPages() - 1);
        }
        return TicketPagingResponse.newPagingHasNext(ticketPaging.getContent(), ticketPaging.getTotalPages() - 1, ticketPaging.getPageable().getPageNumber() + 1);
    }

    private static TicketPagingResponse newLastScroll(List<Ticket> ticketPaging, long lastPage) {
        return newPagingHasNext(ticketPaging, lastPage, LAST_PAGE);
    }

    private static TicketPagingResponse newPagingHasNext(List<Ticket> ticketPaging, long lastPage, long nextPage) {
        return new TicketPagingResponse(getContents(ticketPaging), lastPage, nextPage);
    }

    private static List<TicketInfoResponse> getContents(List<Ticket> ticketPaging) {
        return ticketPaging.stream()
                .map(TicketInfoResponse::of)
                .collect(Collectors.toList());
    }
}

 

  • 이전 κΈ€μ—μ„œ μ†Œκ°œν•œ Response dto 의 ν˜•νƒœμ™€ μœ μ‚¬ν•˜κΈ° λ•Œλ¬Έμ— μžμ„Έν•œ μ„€λͺ…은 μƒλž΅ν•˜κ² μŠ΅λ‹ˆλ‹€.
  • μ—¬κΈ°μ„œ μ§‘μ€‘ν•΄μ„œ 봐야할 것은 of λ©”μ†Œλ“œμ˜ 인자둜 Page<T> μΈν„°νŽ˜μ΄μŠ€λ₯Ό λ°›λŠ” κ²ƒμž…λ‹ˆλ‹€.
  • Page<T> μΈν„°νŽ˜μ΄μŠ€λŠ” getContentgetTotalPagesgetPageable κ°™μ€ λ©”μ†Œλ“œλ₯Ό κ°€μ§€κ³  있고이 λ©”μ†Œλ“œλ“€μ„ ν™œμš©ν•΄μ„œ 기쑴의 방식과 λ™μΌν•˜κ²Œ ν˜„μž¬ νŽ˜μ΄μ§€κ°€ λ§ˆμ§€λ§‰ νŽ˜μ΄μ§€μΈμ§€,λ‹€μŒ νŽ˜μ΄μ§€λŠ” λͺ‡ νŽ˜μ΄μ§€μΈμ§€λ₯Ό ν™•μΈν•΄μ„œ ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ 데이터λ₯Ό κ°€κ³΅ν•˜μ—¬ μ „λ‹¬ν•©λ‹ˆλ‹€.

 

μ„œλΉ„μŠ€ λ‘œμ§μž…λ‹ˆλ‹€.
public TicketPagingResponse retrieveTicketsUsingPaging(RetrieveTicketsRequestDto request, Pageable pageable, Long userId) {
    User user = UserServiceUtils.findUserById(userRepository, userId);
    return TicketPagingResponse.of(ticketRepository.findTicketByFilterConditionUsingPaging(user.getOnboarding(), request.getCategory(), pageable));
}

 

  • TicketPagingResponse.of λ©”μ†Œλ“œμ˜ 인자둜 Page<T> μΈν„°νŽ˜μ΄μŠ€λ₯Ό λ°›λŠ”λ°,이λ₯Ό querydsl κΈ°λŠ₯을 ν™œμš©ν•΄μ„œ μ‘°νšŒν•©λ‹ˆλ‹€.

 

import static com.ticco.domain.ticket.QTicket.ticket;

@RequiredArgsConstructor
public class TicketRepositoryImpl implements TicketRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    @Override
    public Page<Ticket> findTicketByFilterConditionUsingPaging(Onboarding onboarding, @Nullable TicketCategory category, Pageable pageable) {
        List<OrderSpecifier> orders = getAllOrderSpecifiers(pageable);
        List<Ticket> tickets = queryFactory
                .selectFrom(ticket).distinct()
                .where(
                        ticket.onboarding.eq(onboarding),
                        eqCategory(category)
                )
                .orderBy(orders.toArray(OrderSpecifier[]::new))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        return new PageImpl<>(tickets, pageable, queryFactory
                .selectFrom(ticket).distinct()
                .where(
                        ticket.onboarding.eq(onboarding),
                        eqCategory(category)
                ).fetch().size());
    }

    private BooleanExpression eqCategory(TicketCategory category) {
        if (category == null) {
            return null;
        }
        return ticket.category.eq(category);
    }

    private List<OrderSpecifier> getAllOrderSpecifiers(Pageable pageable) {
        List<OrderSpecifier> orders = new ArrayList<>();
        for (Sort.Order order : pageable.getSort()) {
            Order direction = order.getDirection().isAscending() ? Order.ASC : Order.DESC;
            Path<Object> fieldPath = Expressions.path(Object.class, ticket, order.getProperty());
            orders.add(new OrderSpecifier(direction, fieldPath));
        }
        return orders;
    }
}

 

  • eqCategory λ©”μ†Œλ“œ : μ»¨νŠΈλ‘€λŸ¬μ—μ„œ category μΈμžλ‘œ null μ„ λ°›μœΌλ©΄ 필터링을 ν•˜μ§€ μ•Šκ³ ,μΉ΄ν…Œκ³ λ¦¬λ₯Ό λ°›μœΌλ©΄ ν•΄λ‹Ή μΉ΄ν…Œκ³ λ¦¬μ— λ§žλŠ” ν‹°μΌ“λ§Œ μ‘°νšŒν•  수 μžˆλ„λ‘ BooleanExpression μ„ return ν•΄μ£ΌλŠ” λ©”μ†Œλ“œμž…λ‹ˆλ‹€.
  • getAllOrderSpecifiers λ©”μ†Œλ“œ : μ»¨νŠΈλ‘€λŸ¬μ—μ„œ sort νŒŒλΌλ―Έν„°λ‘œ λ°›μ•˜λ˜ κ°’μœΌλ‘œμ •λ ¬ 쑰건을 return ν•΄μ£ΌλŠ” λ©”μ†Œλ“œμž…λ‹ˆλ‹€.
  • Page<Ticket> findTicketByFilterConditionUsingPaging λ©”μ†Œλ“œ : 쑰건에 맞게 티켓을 ν•„ν„°λ§ν•˜κ³ sort νŒŒλΌλ―Έν„°λ‘œ 받은 μ •λ ¬ 쑰건에 따라 데이터듀을 μ •λ ¬ν•©λ‹ˆλ‹€.그리고 μž…λ ₯ 받은 offset , limit νŒŒλΌλ―Έν„°μ— 따라 데이터λ₯Ό μ‘°νšŒν•©λ‹ˆλ‹€.Page<T> νƒ€μž…μœΌλ‘œ λ¦¬ν„΄ν•˜κΈ° μœ„ν•΄ return new PageImpl<> μ„ μ‚¬μš©ν•©λ‹ˆλ‹€.

 

 

🀩 μ‹€μ œ Response 확인

 

졜초 μš”μ²­μ˜ page κ°’μœΌλ‘œλŠ” 첫번째 νŽ˜μ΄μ§€μΈ 0을 λ‹΄μ•„μ„œ λ³΄λƒ…λ‹ˆλ‹€.

 

GET localhost:8080/v1/ticket?category=MUSICAL?page=0?size=1?sort=createdAt,DESC
{
  "status": 200,
  "success": true,
  "message": "ν‹°μΌ“ λͺ©λ‘ 쑰회 μ„±κ³΅μž…λ‹ˆλ‹€.",
  "data": {
    "contents": [
      {
        "ticketId": 6,
        "category": "MUSICAL",
        "rating": 2.7
      }
    ],
    "lastPage": 4,
    "nextPage": 1
  }
}

 

λ‹€μŒ μš”μ²­μœΌλ‘œλŠ” page μ— nextPage κ°’인 1 을 λ‹΄μ•„μ„œ λ³΄λƒ…λ‹ˆλ‹€.

 

GET localhost:8080/v1/ticket?category=MUSICAL?page=1?size=1?sort=createdAt,DESC
{
  "status": 200,
  "success": true,
  "message": "ν‹°μΌ“ λͺ©λ‘ 쑰회 μ„±κ³΅μž…λ‹ˆλ‹€.",
  "data": {
    "contents": [
      {
        "ticketId": 5,
        "category": "MUSICAL",
        "rating": 4.8
      }
    ],
    "lastPage": 4,
    "nextPage": 2
  }
}

page μ— lastPage κ°’인 4 λ₯Ό λ‹΄μ•„μ„œ μš”μ²­ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

GET localhost:8080/v1/ticket?category=MUSICAL?page=4?size=1?sort=createdAt,DESC
{
  "status": 200,
  "success": true,
  "message": "ν‹°μΌ“ λͺ©λ‘ 쑰회 μ„±κ³΅μž…λ‹ˆλ‹€.",
  "data": {
    "contents": [
      {
        "ticketId": 2,
        "category": "MUSICAL",
        "rating": 1
      }
    ],
    "lastPage": 4,
    "nextPage": -1
  }
}

 

λ§ˆμ§€λ§‰ νŽ˜μ΄μ§€μ΄κΈ° λ•Œλ¬Έμ— nextPage μ— -1 이 λ‹΄κΈ΄ λͺ¨μŠ΅μ„ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

 

μ΄λ²ˆμ—λŠ” MUSICAL μΉ΄ν…Œκ³ λ¦¬μ˜ 티켓듀을 평점 κΈ°μ€€μœΌλ‘œ μ •λ ¬λœ λͺ¨μŠ΅μ„ ν™•μΈν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

 

GET localhost:8080/v1/ticket?category=MUSICAL?page=0?size=10?sort=rating,DESC
{
  "status": 200,
  "success": true,
  "message": "ν‹°μΌ“ λͺ©λ‘ 쑰회 μ„±κ³΅μž…λ‹ˆλ‹€.",
  "data": {
    "contents": [
      {
        "ticketId": 4,
        "category": "MUSICAL",
        "rating": 5
      },
      {
        "ticketId": 5,
        "category": "MUSICAL",
        "rating": 4.8
      },
      {
        "ticketId": 3,
        "category": "MUSICAL",
        "rating": 3
      },
      {
        "ticketId": 6,
        "category": "MUSICAL",
        "rating": 2.7
      },
      {
        "ticketId": 2,
        "category": "MUSICAL",
        "rating": 1
      }
    ],
    "lastPage": 0,
    "nextPage": -1
  }
}

 

평점 κΈ°μ€€μœΌλ‘œ 정렬이 잘 됐고 데이터가 10 κ°œκ°€ μ•ˆλ˜κΈ° λ•Œλ¬Έμ— μ‘΄μž¬ν•˜λŠ” λ°μ΄ν„°κΉŒμ§€λ§Œ 쑰회되고 더 이상 μ‘°νšŒν•  데이터가 μ—†κΈ° λ•Œλ¬Έμ— nextPage μ— -1 이 λ‹΄κΈ΄ λͺ¨μŠ΅μ„ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.