[실습] Spring Data JDBC를 이용한 데이터 액세스 실습
Spring Data JDBC 기반 데이터 액세스 계층 연동 실습 개요
- 이번 실습은 실습용 샘플 프로젝트에 포함되어 있는 회원 정보 목록 조회 기능에 페이지네이션(Pagination) 기능을 적용하는 실습입니다.
페이지네이션(Pagination)이란?
예를 들어, 데이터 베이스에 회원 정보가 100건이 저장되어 있는데 클라이언트 쪽에서 100건의 데이터를 모두 요청하는 것이 아니라 한 페이지에 일정 개수만큼만 나누어서 달라고 요청하는 것을 페이지네이션(Pagination)이라고 합니다.
즉 page 번호가 1이고, 페이지에 포함되는 데이터의 개수가 10건일 경우, 데이터베이스의 테이블에서는 1 row부터 10 row까지만 조회되어야 합니다.
만약 page 번호가 2이고, 페이지에 포함되는 데이터의 개수가 10건일 경우, 데이터베이스의 테이블에서는 11 row부터 20 row까지만 조회되어야 합니다.
그런데 이렇게 조회를 하면 보통 가장 오래된 데이터부터 10건씩 조회되기 때문에 일반적으로 테이블의 row를 역순으로 10건씩 가져와서 최신 데이터부터 조회하는 경우가 대부분입니다.
- 지난 챕터까지 학습했던 구현 코드들이 기본적으로 포함이 되어 있으며, 이를 기반으로 요구 사항에 맞게 Controller, Service, Repository 클래스 및 Conroller에서 리턴하는 Response용 DTO 클래스를 적절하게 수정하면 됩니다.
- 실습에 필요한 클래스는 ‘com.springboot’ 내의 패키지에 포함되어 있습니다.
- 실습용 프로젝트 패키지는 아래와 같이 구성되어 있습니다.
- advice
- coffee
- exception
- member
- order
- response
- validator
- 실습용 프로젝트를 실행하면 인메모리 데이터베이스인 H2의 MEMBER 테이블에 20건의 데이터가 저장이 되므로, 별도의 데이터를 추가로 저장할 필요는 없습니다.
실습 사전 준비
- 실습용 샘플 프로젝트 복제
- 아래 github 링크에서 실습용 repository를 fork합니다.
- fork한 repository를 여러분의 PC에서 git clone 명령으로 local repository에 복제합니다.
- IntelliJ IDE로 clone 받은 forked local repository 디렉토리의 프로젝트를 Open합니다.
- 아래 실습 요구 사항에 따라 실습을 진행합니다.
- 작성한 코드는 main branch에 작성해 주세요.
- main branch가 아닌 별도의 branch를 생성해서 작업을 했다면 작업이 끝난 후, 반드시 main branch로 merge 해야 합니다.
페이지네이션(Pagenation) 기능 구현
공통 제한 사항
- MemberController 클래스 제한 사항
- 페이지네이션(Pagination) 관련 구현은 아래 핸들러 메서드에 요청 전송 시, 적용되어야 합니다.
- MemberController의 getMembers()
- 페이지네이션(Pagination)이 적용되는 핸들러 메서드에서는 @RequestParam 애너테이션을 이용해 다음 파라미터를 전달받아야 합니다.
- page
- 데이터 타입: int
- 파라미터 역할: 페이지 번호
- size
- 데이터 타입: int
- 파라미터 역할: 페이지 사이즈
- 설명
- 한 페이지에 포함되는 데이터 row의 개수입니다.
- page
- 파라미터인 page와 size는 아래와 같은 유효성 검증이 적용되어야 합니다.
- page
- 0보다 큰 숫자만 가능합니다.
- size
- 0보다 큰 숫자만 가능합니다.
- page
- 페이지네이션(Pagination) 관련 구현은 아래 핸들러 메서드에 요청 전송 시, 적용되어야 합니다.
- Response용 DTO 클래스 제한 사항 및 참고 사항
- 제한 사항
- MemberController클래스의 getMembers() 핸들러 메서드에서 리턴하는 Response용 DTO 클래스는 아래의 코드 h-1과 같은 구조를 갖는 응답 데이터를 클라이언트에게 전송해야 합니다.
- 참고 사항
- Response DTO 클래스는 현재 코드 상에 구현되어 있는 MemberResponseDto 클래스를 반드시 사용할 필요는 없습니다.
- 여러분이 별도의 공통 Response용 DTO 클래스를 만들어서 사용해도 무방합니다.
{ "data": [ { "memberId": 20, "email": "hgd20@gmail.com", "name": "홍길동20", "phone": "010-2020-2020" }, { "memberId": 19, "email": "hgd19@gmail.com", "name": "홍길동19", "phone": "010-1919-1919" }, { "memberId": 18, "email": "hgd18@gmail.com", "name": "홍길동18", "phone": "010-1818-1818" }, { "memberId": 17, "email": "hgd17@gmail.com", "name": "홍길동17", "phone": "010-1717-1717" }, { "memberId": 16, "email": "hgd16@gmail.com", "name": "홍길동16", "phone": "010-1616-1616" }, { "memberId": 15, "email": "hgd15@gmail.com", "name": "홍길동15", "phone": "010-1515-1515" }, { "memberId": 14, "email": "hgd14@gmail.com", "name": "홍길동14", "phone": "010-1414-1414" }, { "memberId": 13, "email": "hgd13@gmail.com", "name": "홍길동13", "phone": "010-1313-1313" }, { "memberId": 12, "email": "hgd12@gmail.com", "name": "홍길동12", "phone": "010-1212-1212" }, { "memberId": 11, "email": "hgd11@gmail.com", "name": "홍길동11", "phone": "010-1111-1111" } ], "pageInfo": { "page": 1, "size": 10, "totalElements": 20, "totalPages": 2 } }
- 코드 h-1과 같이 현재 조회하는 page의 정보가 pageInfo에 포함되어야 합니다.
- page
- 데이터 타입: int
- 설명
- 페이지 번호
- size
- 데이터 타입: int
- 설명
- 한 페이지에 포함되는 데이터 row의 개수
- totalElements
- 데이터 타입: int
- 설명
- 테이블에 저장되어 있는 데이터의 총 개수
- totalPages
- 데이터 타입: int
- 설명
- 총 페이지 수
- page
- 조회되는 데이터는 코드 h-1과 같이 memberId를 기준으로 내림차순으로 표시되어야 합니다.
- 즉 최신 데이터 순으로 표시되어야 합니다.
- 제한 사항
- 힌트
- Spring에서 지원하는 페이지네이션 API를 이용하면 의외로 손쉽게 실습을 완료할 수 있습니다.
MemberController
package com.springboot.member.controller;
import com.springboot.member.dto.MemberPageResponseDto;
import com.springboot.member.dto.MemberPatchDto;
import com.springboot.member.dto.MemberPostDto;
import com.springboot.member.dto.MemberResponseDto;
import com.springboot.member.entity.Member;
import com.springboot.member.mapper.MemberMapper;
import com.springboot.member.service.MemberService;
import com.springboot.utils.UriCreator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Positive;
import java.net.URI;
import java.util.List;
/**
* - DI 적용
* - Mapstruct Mapper 적용
* - @ExceptionHandler 적용
*/
@RestController
@RequestMapping("/v10/members")
@Validated
@Slf4j
public class MemberController {
private final static String MEMBER_DEFAULT_URL = "/v10/members";
private final MemberService memberService;
private final MemberMapper mapper;
public MemberController(MemberService memberService, MemberMapper mapper) {
this.memberService = memberService;
this.mapper = mapper;
}
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
Member member = mapper.memberPostDtoToMember(memberDto);
Member resultMember = memberService.createMember(member);
URI location = UriCreator.createUri(MEMBER_DEFAULT_URL, resultMember.getMemberId());
return ResponseEntity.created(location).build();
}
@PatchMapping("/{member-id}")
public ResponseEntity patchMember(
@PathVariable("member-id") @Positive long memberId,
@Valid @RequestBody MemberPatchDto memberPatchDto) {
memberPatchDto.setMemberId(memberId);
Member response =
memberService.updateMember(mapper.memberPatchDtoToMember(memberPatchDto));
return new ResponseEntity<>(mapper.memberToMemberResponseDto(response),
HttpStatus.OK);
}
@GetMapping("/{member-id}")
public ResponseEntity getMember(
@PathVariable("member-id") @Positive long memberId) {
Member response = memberService.findMember(memberId);
return new ResponseEntity<>(mapper.memberToMemberResponseDto(response)
, HttpStatus.OK);
}
@GetMapping
public ResponseEntity<MemberPageResponseDto> getMembers(@Positive int page, @Positive int size) {
Page<Member> memberPage = memberService.findMembers(page - 1, size);
MemberPageResponseDto.PageInfo pageInfo = MemberPageResponseDto.PageInfo.builder()
.page(page)
.size(size)
.totalElements((int) memberPage.getTotalElements())
.totalPages(memberPage.getTotalPages())
.build();
List<Member> memberList = memberPage.getContent();
List<MemberResponseDto> response = mapper.membersToMemberResponseDtos(memberList);
MemberPageResponseDto memberPageResponseDto = new MemberPageResponseDto(response, pageInfo);
return new ResponseEntity<>(memberPageResponseDto, HttpStatus.OK);
}
@DeleteMapping("/{member-id}")
public ResponseEntity deleteMember(
@PathVariable("member-id") @Positive long memberId) {
memberService.deleteMember(memberId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
MemberPageResponseDto
package com.springboot.member.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
@Getter
@AllArgsConstructor
public class MemberPageResponseDto {
private List<MemberResponseDto> data;
private PageInfo pageInfo;
@Getter
@Builder
public static class PageInfo {
private int page;
private int size;
private int totalElements;
private int totalPages;
}
}
MemberRepository
package com.springboot.member.repository;
import com.springboot.member.entity.Member;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import java.util.Optional;
// TODO 페이지네이션을 적용하세요!
public interface MemberRepository extends PagingAndSortingRepository<Member, Long> {
Optional<Member> findByEmail(String email);
}
MemberService
package com.springboot.member.service;
import com.springboot.exception.BusinessLogicException;
import com.springboot.exception.ExceptionCode;
import com.springboot.member.entity.Member;
import com.springboot.member.repository.MemberRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/**
* V2
* - 메서드 구현
* - DI 적용
* - Spring Data JDBC 적용
*/
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public Member createMember(Member member) {
// 이미 등록된 이메일인지 확인
verifyExistsEmail(member.getEmail());
return memberRepository.save(member);
}
public Member updateMember(Member member) {
Member findMember = findVerifiedMember(member.getMemberId());
// TODO 리팩토링 포인트
Optional.ofNullable(member.getName())
.ifPresent(name -> findMember.setName(name));
Optional.ofNullable(member.getPhone())
.ifPresent(phone -> findMember.setPhone(phone));
return memberRepository.save(findMember);
}
public Member findMember(long memberId) {
return findVerifiedMember(memberId);
}
public Page<Member> findMembers(int page, int size) {
// TODO 페이지네이션을 적용하세요!
PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "memberId"));
return memberRepository.findAll(pageRequest);
}
public void deleteMember(long memberId) {
Member findMember = findVerifiedMember(memberId);
memberRepository.delete(findMember);
}
public Member findVerifiedMember(long memberId) {
Optional<Member> optionalMember = memberRepository.findById(memberId);
return optionalMember.orElseThrow(() ->
new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
}
private void verifyExistsEmail(String email) {
Optional<Member> member = memberRepository.findByEmail(email);
if (member.isPresent())
throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS);
}
}