Java/Spring & Spring Boot

Spring Data JDBC에서 Offset-Pagination 적용하기

마손리 2023. 4. 21. 22:21

페이지네이션(Pagination)

Pagination이란 데이터 베이스에 회원 정보가 100건이 저장되어 있는데 클라이언트 쪽에서 100건의 데이터를 모두 요청하는 것이 아니라 한 페이지에 일정 개수만큼만 나누어서 달라고 요청하는 것페이지네이션(Pagination)이라고 한다.

 

Pagination에는 Offset PaginationCursor Pagination이 있다.

 

Offset Pagination

출력되는 모든 결과물을 특정한 개수만큼 나누어 페이지별로 제공하는 형식이다.

일반적인 게시판 형태(검색 엔진 등)이다.

 

Cursor Pagination

정해진 개수만큼의 데이터를 가저와 로드하고 이후 로드된 마지막 데이터부터 다시 정해진 개수만큼의 데이터를 가저와 로드하는 형식이다. SNS에 주로 사용된다. (Faceboock, Instagram 등)

 

마지막 조회한 위치를 가지고 있어야 하지만 정해진 개수만큼만 로드 되므로 Offset Pagination보다 속도가 빠르다.

 

 

 

OffSet Pagination 적용

 

요청 파라미터를 위한 DTO

@Getter
@Setter
public class MemberGetDto {
    @Min(1)
    private int page;
    @Min(1)
    private int size;
}

요청으로 받는 파라미터에 대한 DTO

 

응답을 위한 DTO

여러 멤버의 정보가 담긴 GET 요청 Pagination을 위한 DTO

@Getter
@Builder
public class MemberPageResponseDto {
    private List<MemberResponseDto> data;
    private PageInfo pageInfo;


    @Getter
    @Builder
    public static class PageInfo{
        private int page;
        private int size;
        private long totalElements;
        private long totalPages;
    }
}

 

개인 멤버의 GET 요청을 위한 DTO

@Builder
@Getter
public class MemberResponseDto {
    private long memberId;
    private String email;
    private String name;
    private String phone;
}

 

Repository 인터페이스

public interface MemberRepository extends PagingAndSortingRepository<Member, Long> { //PagingAnd...
    Page<Member> findAll(Pageable pageable);
    //PagingAndSortingRepository를 구현할 경우
    
    Page<Member> findAllByOrderByMemberIdDesc(Pageable pageable);
	//CrudRepository를 구현할 경우
}

PagingAndSortingRepository를 구현할경우 PagingAndSortingRepository는 CrudRepository를 구현하므로 두가지 방법 모두 사용이 가능하다. 

 

하지만 CrudRepository를 구현할경우 예제와 같이 특별한 네이밍법칙으로 메서드를 추상화 해주어야만 Pagination을 사용 할수 있다.

 

Controller

@RestController
@RequestMapping("/v10/members")
public class MemberController {
    private final MemberService memberService;
    private final MemberMapper mapper;

    public MemberController(MemberService memberService, MemberMapper mapper) {
        this.memberService = memberService;
        this.mapper = mapper;
    }
    
    @GetMapping
    public ResponseEntity getMembers(@Valid MemberGetDto memberGetDto) {
    				//@RequestParam 에너테이션없이 DTO로 파라미터를 받아 유효성검사 진행
        Page<Member> page = memberService.findMembers(memberGetDto);
        
        MemberPageResponseDto response = mapper.pageToMemberPageResponseDto(page);
        
        return new ResponseEntity<>(response, HttpStatus.OK);
    }
}

 

Service

@Service
public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public Page<Member> findMembers(MemberGetDto memberGetDto) {
        PageRequest pageRequest = PageRequest.of(memberGetDto.getPage()-1, 
                memberGetDto.getSize(), 
                Sort.by("memberId").descending());
        
        return memberRepository.findAll(pageRequest);

//        PageRequest pageRequest = PageRequest.of(memberGetDto.getPage()-1, memberGetDto.getSize());
//        return memberRepository.findAllByOrderByMemberIdDesc(pageRequest);
//        CrudRepository 방식을 사용할 경우        
    }
}

CrudReposigory 방식을 사용할경우 네이밍에 맞게 메서드가 생성되어 옵션설정이 불필요하다.

하지만 PagingAndSortingRepository방식의 경우 정렬 방식을 옵션으로 지정해 주어야한다. 기본값은 Primary Key 기준에 오름차순으로 정렬된다.

 

Mapper

@Mapper(componentModel = "spring")
public interface MemberMapper {
    default MemberPageResponseDto pageToMemberPageResponseDto(Page<Member> page){
        List<MemberResponseDto> memberResponseDtos = page.stream().map(member->  MemberResponseDto.builder()
                .memberId(member.getMemberId())
                .name(member.getName())
                .phone(member.getPhone())
                .email(member.getEmail()).build()).collect(Collectors.toList());

        MemberPageResponseDto.PageInfo pageInfo = MemberPageResponseDto.PageInfo.builder()
                .totalPages(page.getTotalPages())
                .totalElements(page.getTotalElements())
                .page(page.getNumber()+1)
                .size(page.getNumberOfElements())
                .build();

        return MemberPageResponseDto.builder().pageInfo(pageInfo).data(memberResponseDtos).build();
    }
}

비즈니스로직에서 나온 결과값 Page<Member>를 MemberPageResponseDto로 변환해준다.

 

우선 각각의 데이터들은 Member 엔티티이므로 map()메서드를 이용해 MemberResponseDto로 변환해준 뒤 List에 넣어 주었다. 

 

이후 pageInfo는 Page에서 제공하는 메서드들로 쉽게 필요한 값들을 구할 수 있다.

 

예외처리

import org.springframework.validation.BindException;

@RestControllerAdvice
public class GlobalExceptionAdvice {
    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleBindException(BindException e){
        final ErrorResponse response = ErrorResponse.of(e.getBindingResult());
        return response;
    }
}

요청으로 파라미터를 받을때 MemberGetDto의 validation인 @Min(1)을 통과하지못하면 BindException이 발생한다. 

(주의: BindException의 출처가 2개이므로 위의 import에 해당하는 BindException을 참조해야한다.)

 

@Getter
public class ErrorResponse {
    private List<FieldError> fieldErrors;

    private ErrorResponse(final List<FieldError> fieldErrors) {
        this.fieldErrors = fieldErrors;
    }

    public static ErrorResponse of(BindingResult bindingResult) {
        return new ErrorResponse(FieldError.of(bindingResult), null);
    }

    @Getter
    public static class FieldError {
        private String field;
        private Object rejectedValue;
        private String reason;

        private FieldError(String field, Object rejectedValue, String reason) {
            this.field = field;
            this.rejectedValue = rejectedValue;
            this.reason = reason;
        }

        public static List<FieldError> of(BindingResult bindingResult) {
            final List<org.springframework.validation.FieldError> fieldErrors =
                                                        bindingResult.getFieldErrors();
            return fieldErrors.stream()
                    .map(error -> new FieldError(
                            error.getField(),
                            error.getRejectedValue() == null ?
                                            "" : error.getRejectedValue().toString(),
                            error.getDefaultMessage()))
                    .collect(Collectors.toList());
        }
    }
}

 

'Java > Spring & Spring Boot' 카테고리의 다른 글

JPA의 Entity 매핑 (feat. Auditable, GeneratedValue 전략)  (0) 2023.04.26
JPA(Java Persistence API)  (0) 2023.04.23
Spring Data JDBC  (0) 2023.04.20
JDBC(Java Database Connectivity)  (0) 2023.04.20
Exception, 예외 처리  (0) 2023.04.18