[Spring] 페이지네이션

노유성·2023년 7월 25일
0
post-thumbnail

들어가며

페이지네이션(Pagination)은 웹 애플리케이션, 모바일 앱 또는 다른 형태의 소프트웨어에서 데이터를 여러 페이지로 나누어 표시하는 기술이나 방법을 의미합니다. 이를 통해 많은 양의 데이터를 사용자에게 보다 쉽게 제공하고, 사용자 경험을 향상시키며, 서버 부하를 분산시킬 수 있습니다.
보통 데이터베이스나 외부 API에서 대량의 데이터를 가져와서 사용자에게 보여줄 때 사용됩니다. 예를 들어, 검색 결과, 게시물 목록, 제품 리스트 등이 많은 데이터를 가질 수 있습니다. 이러한 데이터를 한 번에 모두 표시하면 사용자 경험이 저하되고, 로딩 시간이 길어질 수 있습니다. 페이지네이션은 이러한 문제를 해결하기 위해 데이터를 여러 페이지로 나누고, 사용자가 원하는 페이지를 선택하여 볼 수 있도록 하는 것입니다.
-chatGPT

쉽게 얘기해서 예를 들어 게시글의 검색 결과가 10000개라고 가정했을 때, 사용자가 10000개의 데이터를 한 번에 다 받으면은 사용자 경험도 좋지 않고 데이터를 제공하는 서버도 부하가 크므로 데이터를 나눠서 주자는 것이 페이지네이션의 개념이다. 한 페이지랑 몇 개씩 보여줄 것인지, 원하는 페이지가 몇 번째 페이지인지를 명시를 해주면은 서버에서는 그에 맞춰서 페이지 단위로 데이터를 보여주는 것이다.

여기서 Pageable 이라는 인터페이스를 활용할 수 있다.

Pageable

Pageable은 데이터를 페이지 기준으로 나누고 특정 페이지의 데이터를 검색하는 데 사용되는 인터페이스이다.

테스트 코드를 보면서 Pageable 인터페이스가 어떻게 사용되는지 알아보자.

@Test
@Description("30개의 이벤트를 10개씩 두번째 페이지 조회하기")
void queryEvents() throws Exception {
    ResultActions perform = this.mockMvc.perform(get("/api/events")
            .param("page","1")
            .param("size","10"));
   	...

위와 같이 특정 앤드포인트로 get 요청을 보내는데 페이지네이션은 query string으로 구현될 수 있다. 클라이언트가 한 페이지당 사이즈를 몇 개를 할지, page는 몇 번째 페이지를 볼 지를 명시해서 요청을 보내면은 서버는 이를 받아서 페이지네이션을 할 수 있다.

@GetMapping
public ResponseEntity queryEvents(Pageable pageable) {
    Page<Event> page = this.eventRepository.findAll(pageable);

    return ResponseEntity.ok(page);



그러면 위와 같이 데이터와 함께 메타 데이터도 받아서 볼 수 있다. 하지만 이 정도로는 hateoas를 만족할 수 없기에 hateoas에서 제공하는 API를 통해 hateoas를 만족시킬 수 있다.

PagedResourcesAssembler

@GetMapping
public ResponseEntity queryEvents(Pageable pageable,
				PagedResourcesAssembler<Event> assembler) {
    Page<Event> page = this.eventRepository.findAll(pageable);

    PagedModel<EntityModel<Event>> model = assembler.toModel(page);
    return ResponseEntity.ok(model);
}

이렇게 hateoas의 assembler를 통해서 받아온 page를 감싸주면은

기존의 데이터는 _embedded 라는 키로 다시 한 번 감싸지게 되고,

상태 전이가 가능한 주소와 page의 메타 정보에 대해서 알려준다.

하지만 이것만으로는 부족하다. 왜냐하면 self-descriptive하지 않기 때문이다.

EntityModel

public class EventResource extends EntityModel<Event> {
    public EventResource(Event event) {
        super(event);
        add(linkTo(EventController.class).slash(event.getId()).withSelfRel());
    }
}

다음과 같이 event를 객체로 받아 link를 추가해주는 hateoas의 API를 이용해서

@GetMapping
public ResponseEntity queryEvents(Pageable pageable, PagedResourcesAssembler<Event> assembler) {
    Page<Event> page = this.eventRepository.findAll(pageable);

    PagedModel<EntityModel<Event>> model = assembler.toModel(page,e -> new EventResource(e));
    model.add(Link.of("/docs/index.html#resources-events-list").withRel("profile"));
    return ResponseEntity.ok(model);
}

다음과 같이 코드를 작성할 수 있다. toModel의 2번째 인자는 데이터타입은 RepresentationModel인데 EventResource는 EntityModel에게 상속받고 EntityModel은 RepresentationModel을 상속받기 때문에 가능하다. 그래서 인자로 EventResource 개체를 넘겨주고 model의 링크에 self-description이 가능하도록 API 명세서의 주소를 넘겨주면은 RESTful하다고 할 수 있다.


실제로 결과를 보면은 PagedResourceAssembler가 만들어준 _links 속성에 profile 링크가 추가된 것을 알 수 있다. 이로써 toModel의 2번째 인자에 RepresentationModel 객체를 넘겨주면은 _link를 추가해주는 것을 유추할 수 있다.

번외

ID로 데이터 조회하기

@Test
@Description("기존의 이벤트를 하나 조회하기")
void getEvent() throws Exception {
    // Given
    Event event = this.generateEvent(100);
    
    // When
    ResultActions perform = this.mockMvc.perform(get("/api/events/{id}", event.getId()));
    
    // Then
    perform.andExpect(status().isOk())
            .andExpect(jsonPath("name").exists())
            .andExpect(jsonPath("id").exists())
            .andExpect((jsonPath("_links.profile")).exists())
    ;

}

이벤트를 하나 생성한 다음에 해당 이벤트의 id로 get 요청을 보내는 테스트 코드이다.

@GetMapping("/{id}")
public ResponseEntity getEvent(@PathVariable Integer id) {
    Optional<Event> optionalEvent = this.eventRepository.findById(id);
    if(optionalEvent.isEmpty()) {
        return ResponseEntity.notFound().build();
    }

    Event event = optionalEvent.get();
    EventResource eventResource = new EventResource(event);
    eventResource.add(Link.of("/docs/index.html#resources-events-get").withRel("profile"));

    return ResponseEntity.ok(eventResource);
}

그리고 이를 받아주는 controller이다.

이런 식으로 path variable을 통해서 동적인 URI처리를 할 수 있고, 좀 더 사용자에게 명확한 404에러의 이유를 주기 위해서

private ResponseEntity badRequest(Errors errors) {
    return ResponseEntity.badRequest().body(new ErrorsResource(errors));
}

위와 같이 명확하게 거절 사유에 대해서 이유를 전달할 수 있다.

ErrorResource.java

public class ErrorsResource extends EntityModel<Errors> {
    public ErrorsResource(Errors content, Link... links) {
        super(content, List.of(links));
        add(linkTo(methodOn(IndexController.class).index()).withRel("index"));
    }
}
profile
풀스택개발자가되고싶습니다:)

0개의 댓글