정규 레시피 도수 별 Pagination

구현 화면

스크린샷 2023-09-03 오후 10 23 37

정규 레시피 도수 별 Pagination 구현

RegularRecipe

@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class RegularRecipe {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false, unique = true)
    private Long id;
    @Column(nullable = false)
    private String imageUrl;
    @Column(nullable = false)
    private String name;
    @Column(nullable = false)
    private String description;
    @Column(nullable = false)
    private String recipe;
    @Column(nullable = false)
    private String ingredient;
    @Column(nullable = false, columnDefinition = "TINYINT")
    private Integer alcVol;
    @Column(nullable = false)
    private String baseAlc;
    @Column(nullable = false, columnDefinition = "TIMESTAMP")
    private final LocalDateTime createdAt = LocalDateTime.now();
    @Column(columnDefinition = "TIMESTAMP")
    private LocalDateTime modifiedAt;
    @Column(nullable = false)
    private boolean deleted;
}

RegularRecipeResponse

@Getter
@Setter
@NoArgsConstructor
public class RegularRecipeResponse {
    private Long id;
    private String name;
    private String imageUrl;
    private String description;

    public static RegularRecipeResponse of(RegularRecipe regularRecipe) {
        RegularRecipeResponse response = new RegularRecipeResponse();
        response.setId(regularRecipe.getId());
        response.setName(regularRecipe.getName());
        response.setImageUrl(regularRecipe.getImageUrl());
        response.setDescription(regularRecipe.getDescription());
        return response;
    }

    public static List<RegularRecipeResponse> listOf(List<RegularRecipe> regularRecipes) {
        return regularRecipes.stream()
                .map(RegularRecipeResponse::of)
                .collect(Collectors.toList());
    }
}

먼저 앞서 보여드렸던 구현화면을 보면 description은 필요 없어 보일 것 입니다. 하지만 프론트엔드 동료분이 카드형식으로 아래 그림과 같이 설명을 넣고 싶다고 해서 같이 보내줬습니다.

스크린샷 2023-09-03 오후 11 11 30

of() 메서드를 보면 RegularRecipe의 객체를 받아와서 해당 객체의 정보를 사용하여 RegularRecipeResponse 객체를 생성하고 반환합니다.

listOf()메서드는 RegularRecipe의 엔티티 객체들의 리스트를 받아와서 이 리스트를 RegularRecipeResponse 객체들의 리스트로 변환하여 반환합니다.

우선 서비스에 들어가기에 앞서, Pagination부터 설명하겠습니다.

PageInfo

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class PageInfo {
    private int page;
    private int size;
    private int totalPage;
    private long totalSize;

    public static <T> PageInfo of(Page<T> page) {
        return new PageInfo(page.getNumber() + 1, page.getSize(), page.getTotalPages(), page.getTotalElements());
    }
}

우선 프로젝트 회의 때 Pagination을 사용하기로 했는데, 이유는 칵테일 레시피가 엄청나게 많은데, 그럴 때마다 계속 많은 양의 데이터를 보내면 속도도 느려서 사용자 측면에서 불편하기 때문에 적용하기로 했습니다.

이제 코드를 살펴보겠습니다.

  • page = 페이지 번호

  • size = 한 페이지 당 항목 수(페이지 크기)

  • totalPage = 전체 페이지 수

  • totalSize = 전체 항목 수

그림으로 보면 좀 더 편한데, 알딸딸 프로젝트로 보면 아래와 같습니다.

스크린샷 2023-09-03 오후 11 33 23

of() 메서드를 보면, Spring Data JPA의 Page 객체를 받아와서 해당 페이지 정보를 기반으로 PageInfo 객체를 생성하고 반환합니다.

아래 그림을 보면 Spring Data JPA에서 제공해주는 것을 알 수 있습니다.

스크린샷 2023-09-03 오후 11 35 50

해당 JpaRepository에 들어가면 아래 그림처럼 PagingAndSortingRepository기능을 제공해주는 것을 확인할 수 있습니다.

스크린샷 2023-09-03 오후 11 34 47

page.getNumber() + 10부터 시작하는 페이지 번호를 1부터 시작하는 번호로 변경하는 것 입니다.

MultiResponseDto

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class MultiResponseDto<T> {
    private List<T> data;
    private PageInfo pageInfo;

    public static <T> MultiResponseDto<T> of(List<T> data, PageInfo pageInfo) {
        MultiResponseDto<T> multiResponseDto = new MultiResponseDto(data, pageInfo);
        return multiResponseDto;
    }
}

MultiResponseDto 클래스는 data와 페이징 정보가 포함된 응답을 처리하기 위한 클래스입니다.

위에서 만들어 놓은 PageInfo 클래스를 활용했습니다.

어떻게 보면 데이터 목록과 페이징 정보를 캡슐화하여 설계하는 것과 같습니다.

of() 메서드는 이 둘을 포함한 객체를 생성해 반환합니다.

RegularRecipeController

@RestController
@RequiredArgsConstructor
@Api(tags = "regularRecipe",description = "정규 레시피 API")
@RequestMapping("/regular")
public class RegularRecipeController {
    private final RegularRecipeService regularRecipeService;

    @ApiOperation(value = "정규 레시피 전체 조회")
    @GetMapping("/findAll/{alc_vol}")
    public ApiResponse<MultiResponseDto<RegularRecipeResponse>> findRecipes(@PathVariable("alc_vol") Integer alcVolRange, @RequestParam int page, @RequestParam int size) {
        return ApiResponse.ok(regularRecipeService.findAlcVolRange(alcVolRange, page - 1, size));
    }
}

우선 클라이언트는 요청할 때 알코올 도수를 입력합니다. 반환은 위에서 만들어 놓은 데이터와 페이징을 포함한 MultiResponseDto 안에 반환할 데이터 타입인 RegularRecipeResponse 를 넣고, 이 둘을 커스텀 응답 메세지인 ApiResponse 의 안에 넣어줍니다.

Controller는 결과만 받아 올 뿐입니다.

RegularRecipeService

@Service
@RequiredArgsConstructor
public class RegularRecipeService {
    private final RegularRecipeRepository regularRecipeRepository;
    private final MemberService memberService;
		
    @Transactional(readOnly = true)
    public MultiResponseDto<RegularRecipeResponse> findAlcVolRange(Integer alcVolRange, int page, int size) {
        int startRange;
        int endRange;

        if (alcVolRange == 0) {
            startRange = 0;
            endRange = 0;
        } else if (alcVolRange < 10) {
            startRange = alcVolRange;
            endRange = 9;
        } else if (alcVolRange < 20) {
            startRange = alcVolRange;
            endRange = 19;
        } else if (alcVolRange < 30) {
            startRange = 20;
            endRange = 29;
        } else {
            startRange = 30;
            endRange = Integer.MAX_VALUE;
        }

        Page<RegularRecipe> pages = regularRecipeRepository.findAllByAlcVolRange(startRange, endRange, PageRequest.of(page, size, Sort.by("alcVol").descending()));
        List<RegularRecipeResponse> regularRecipeResponses = RegularRecipeResponse.listOf(pages.getContent());
        PageInfo pageInfo = PageInfo.of(pages);
        return MultiResponseDto.of(regularRecipeResponses, pageInfo);
    }
}

우선 프로젝트 요구 사항은 클라이언트 요청이 0을 누르면 논알콜, 1 ~ 9를 누르면 1 ~ 9인 칵테일, 10 ~ 19를 누르면 10 ~ 19인 칵테일 ~ 이렇게 쭉 이어지다가 30이상인 알코올 도수의 칵테일들을 그 뒤로 나오게 진행했습니다.

@Transactional(readOnly = true)로 메서드가 GET 전용임을 알려주고,

if() 문으로 우선 요구 사항의 범위를 필터링 해주는데, if() 문이 끝난 알코올 도수는 알코올 도수 기준으로 정규 레시피를 필터링 하는데 사용했습니다.

그리고 regularRecipeRepository.findAllByAlcVolRange()를 사용하여 지정된 범위 내에 속하는 정규 레시피를 검색합니다.

Sort.by("alcVol").descending() 를 사용하여 "alcVol" 를 기준으로 결과를 내림차순으로 정렬했습니다. 즉, 알코올 도수가 높은 순으로 각 페이지에 먼저 표시됩니다.

그 다음 RegularRecipeResponse 클래스의 listOf 메소드를 호출하여 RegularRecipeResponse 객체의 List를 생성하고, pages.getContent() 를 사용하여 Page 객체에서 RegularRecipe 항목 목록을 검색합니다.

PageInfo에 앞에서 설명 한 pageInfo.of() 메서드에 pages를 넣어주고 할당했습니다.

마찬가지로 MultiResponseDto.of() 에 data 타입인 regularRecipeResponsespageInfo 를 넣고 반환시킵니다.

RegularRecipeRepository

public interface RegularRecipeRepository extends JpaRepository<RegularRecipe, Long> {
    @Query("SELECT r FROM RegularRecipe r WHERE r.alcVol >= :startRange AND r.alcVol <= :endRange")
    Page<RegularRecipe> findAllByAlcVolRange(int startRange, int endRange, Pageable pageable);

}

@Query 어노테이션을 사용해 JPQLstartRange, endRange를 정의했습니다.

기능 동작

스크린샷 2023-09-04 오전 12 17 30

간단하게 예시로 dml로 만들어 둔 데이터를 활용하여 결과를 보여주면 아래 그림처럼 정상 작동합니다.

스크린샷 2023-09-04 오전 12 21 39
스크린샷 2023-09-04 오전 12 22 14

Last updated