정규 레시피 도수 별 Pagination
구현 화면
정규 레시피 도수 별 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은 필요 없어 보일 것 입니다. 하지만 프론트엔드 동료분이 카드형식으로 아래 그림과 같이 설명을 넣고 싶다고 해서 같이 보내줬습니다.
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 = 전체 항목 수
그림으로 보면 좀 더 편한데, 알딸딸 프로젝트로 보면 아래와 같습니다.
of()
메서드를 보면, Spring Data JPA의 Page 객체를 받아와서 해당 페이지 정보를 기반으로 PageInfo 객체를 생성하고 반환합니다.
아래 그림을 보면 Spring Data JPA에서 제공해주는 것을 알 수 있습니다.
해당 JpaRepository
에 들어가면 아래 그림처럼 PagingAndSortingRepository
기능을 제공해주는 것을 확인할 수 있습니다.
page.getNumber() + 1
는 0부터 시작하는 페이지 번호를 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 타입인 regularRecipeResponses
와 pageInfo
를 넣고 반환시킵니다.
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
어노테이션을 사용해 JPQL로 startRange
, endRange
를 정의했습니다.
기능 동작
간단하게 예시로 dml로 만들어 둔 데이터를 활용하여 결과를 보여주면 아래 그림처럼 정상 작동합니다.
Last updated