사용자 정의 리포지토리 구현
스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동으로 생성한다. 편리한 장점이지만 이것으로 오는 단점도 존재한다. 스프링 데이터 JPA가 제공하는 리포지토리 인터페이스를 직접 구현할 때 구현해야하는 기능이 너무 많다는 것이다. 실무에서 많이 사용한다.
스프링 데이터 JPA의 리포지토리를 직접 구현해야 할 때는 다음과 같다.
- JPA 직접 사용(EntityManager)할 때
- 스프링 JDBC Template 사용할 때
- MyBatis를 사용할 때
- 데이터베이스 커넥션을 직접 사용할 때
- Querydsl을 사용할 때
1. 사용자 정의 리포지토리 구현 방법 (구식)
먼저 사용자 정의 리포지토리의 인터페이스를 만들어준다.
📌 MemberRepositoryCustom 인터페이스
package study.datajpa.repository;
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
사용자 정의 페이스를 만들었으니 리포지토리와 상속관계를 맺어준다. 사용자 정의 인터페이스의 이름은 아무렇게 지어도 된다.
📌 MemberRepository
package study.datajpa.repository;
// 기능 확장시 MemberRepositoryCustom 상속 추가
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
...
}
리포지토리가 사용자정의 인터페이스를 상속받아야한다. 이제 리포지토리에서 사용자 정의 인터페이스의 기능을 사용할 수 있도록 사용자 정의 인터페이스의 구현 클래스를 만들어야 한다.
📌 MemberRepositoryImpl
package study.datajpa.repository;
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
}
여기서 구현 클래스의 이름이 중요하다. '리포지토리_인터페이스_이름 + Impl' 이렇게 구현 클래스 이름을 지어야 스프링 데이터 JPA가 해당 구현 클래스를 찾아서 스프링 빈으로 등록해준다.
📌 MemberRepositoryTest
@Test
void callCustom() {
List<Member> result = memberRepository.findMemberCustom();
}
사용자 정의 메서드를 그냥 memberRepository의 메서드 처럼 사용하면 된다.
실무에서는 주로 QueryDsl이나 SpringJdbcTemplate를 함께 사용할 때 사용자 정의 리포지토리 기능을 자주 사용한다. 하지만 항상 사용자 정의 리포지토리가 필요한 것은 아니다. 임의의 리포지토리를 만들어도 된다. 예를 들어서 MemberQueryRepository를 인터페이스가 아닌 클래스로 만들고 스프링 빈으로 등록해서 직접 사용해도 된다. 물론 이 경우 스프링 데이터 JPA와는 아무런 관계 없이 별도로 동작한다.
참고로 핵심 비즈니스 로직이 있는 리포지토리와 뷰를 위한 로직이 있는 리포지토리를 서로 분리하는 것이 좋다.
2. 사용자 정의 리포지토리 구현 방법 (최신)
스프링 데이터 JPA 2.x 버전 부터는 사용자 정의 구현 클래스에 '사용자_정의_인터페이스_이름 + Impl' 방식도 지원한다.
📌 MemberRepositoryCustom 인터페이스
package study.datajpa.repository;
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
📌 MemberRepository
package study.datajpa.repository;
// 기능 확장시 MemberRepositoryCustom 상속 추가
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
...
}
여기까지는 기존 방식과 같다.
📌 MemberRepositoryCustomImpl
package study.datajpa.repository;
@RequiredArgsConstructor
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
}
여기서부터 다르다. 사용자 정의 인터페이스의 구현 클래스의 이름을 '사용자_정의_인터페이스_이름' + 'Impl'로 지어도 스프링 데이터 JPA가 알아서 찾아서 스프링 빈으로 등록시켜준다. 그리고 이 방식으로 구현 클래스 이름을 지을 것을 추천한다. 기존 방식 보다 이 방식이 사용자 정의 인터페이스 이름과 구현 클래스 이름이 더 비슷하므로 더 직관적이다. 추가로 여러 인터페이스를 분리해서 구현하는 것도 가능해서 이 방식이 더 낫다.
Auditing
엔티티를 생성하거나 변경할 때 변경한 사람과 시간을 추적하고 싶을 떄 사용한다. 기본적으로 테이블을 등록할 때 등록일, 수정일, 등록자, 수정자를 꼭 남겨야 운영할 때 편하다. 사실상 모든 테이블에 적용하는 것이 좋다. 실무에서 많이 사용한다.
1. 순수 JPA 사용
먼저 순수 JPA를 통해 Member 엔티티에 등록일, 수정일 속성을 추가해보자.
📌 JpaBaseEntity
package study.datajpa.entity;
@Getter
@MappedSuperclass // 진짜 상속 관계가 아닌 속성을 공통으로 사용할 수 있게 한다.
public class JpaBaseEntity {
@Column(updatable = false) // 등록일은 절대 수정 못하도록 지정
private LocalDateTime createDate;
private LocalDateTime updateDate;
// 영속 상태가 되기 전에 호출된다.
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
createDate = now;
updateDate = now; // 등록할 떄 업데이트에도 now 값을 넣어준 이유가 null 값 방지다. 쿼리할때 null 값이 있으면 지저분해진다.
}
// 업데이트 하기 전에 호출된다.
@PreUpdate
public void preUpdate() {
updateDate = LocalDateTime.now();
}
}
@MappedSuperclass를 통해 해당 필드 값들을 공통 속성으로 만들어 줬다. 참고로 엔티티는 아니다.
@Column(updatable=false)를 통해 등록일 컬럼을 변경 못하도록 했다.
@PrePersist는 영속성 컨텍스트에 저장되기 전에 호출된다. @PrePersist로 영속상태가 되기 직전에 prePersist()를 호출 되도록 했다. prePersist() 에서 생성일자인 createDate 속성 값에 현재 시간을 넣어줬다. 그리고, updateDate 속성에도 현재 시간으로 맞췄다. 왜냐하면 그렇지 않을 경우 처음 엔티티가 영속성 컨텍스트로 등록이 될 때 updateDate 속성 값이 null값이 된다. 테이블에 null 값이 존재할 경우 지저분해질 수 있다.
@PreUpdate는 flush나 commit을 호출해서 데이터베이스에 수정하기 직전에 호출된다.
📌 Member 엔티티
package study.datajpa.entity;
import lombok.*;
import javax.persistence.*;
import static javax.persistence.FetchType.LAZY;
import static lombok.AccessLevel.PROTECTED;
@Entity
@Getter @Setter
@NoArgsConstructor(access = PROTECTED)
@ToString(of = {"id", "username", "age"})
// 상속 받으면 된다.
public class Member extends JpaBaseEntity{
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private int age;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "team_id")
private Team team;
}
@MappedSuperclass 애노테이션이 적용된 JpaBaseEntity를 상속 받은 엔티티들은 JpaBaseEntity에 포함된 속성들을 공통 속성으로 사용할 수 있다.
📌 MemberTest
@Test
void JpaEventBaseEntity() throws Exception {
// given
Member member = new Member("member1");
memberRepository.save(member); // 이 때 @PrePersist가 호출된다.
Thread.sleep(100); // 사실 테스트코드에 sleep을 사용하는것은 안좋다.
member.setUsername("member2");
em.flush(); // 이 때 @PreUpdate가 호출된다.
em.clear();
// when
Member findMember = memberRepository.findById(member.getId()).orElse(null);
// given
System.out.println("findMember.createDate = " + findMember.getCreateDate());
System.out.println("findMember.updateDate = " + findMember.getUpdateDate());
}
findMember.createDate = 2022-04-21T03:28:31.966419
findMember.updateDate = 2022-04-21T03:28:32.149419
출력 결과를 보면 @MappedSuperclass 애노테이션이 적용된 JpaBaseEntity 속성들이 Member 엔티티의 속성으로 포함되어있다는 것을 알 수 있다.
🔍 JPA 주요 이벤트 애노테이션
- @PostLoad
- 엔티티가 영속성 컨텍스트에 조회된 직후, 또는 refresh를 호출한 후 호출된다. 2차 캐시에 저장되어 있어도 호출된다.
- 해당 엔티티를 새로 불러오거나 refresh한 이후 호출
- @PrePersist
- em.persist() 메소드가 호출된 후 엔티티가 영속성 컨텍스트에 관리되기 직전에 호출된다. 식별자 생성 전략을 사용한 경우에는 엔티티의 식별자는 존재하지 않는 상태이다. 새로운 인스턴스를 merge 할 때도 수행된다.
- 해당 엔티티를 저장하기 이전에 호출
- @PreUpdate
- flush나 commit을 호출해서 엔티티를 데이터베이스에 수정하기 직전에 호출된다.
- 해당 엔티티가 업데이트 하기 전에 호출
- @PreRemove
- em.remove() 메서드가 호출 된 후 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출된다. 또한 삭제 명령어로 영속성 전이가 일어날 때도 호출된다. orphanRemoval에 대해서는 flush나 commit 시에 호출된다.
- 해당 엔티티를 삭제하기 이전에 호출
- @PostPersist
- flush나 commit을 호출해서 엔티티를 데이터베이스에 저장한 직후에 호출된다. 식별자가 항상 존재한다. 참고로 식별자 생성 전략이 IDENTITY인 경우 식별자를 생성하기 위해 persist()를 호출하면서 데이터베이스에 해당 엔티티를 저장하므로, 이 때는 persist()를 호출한 직후에 바로 PostPersist가 호출된다.
- 해당 엔티티를 저장한 이후에 호출
- @PostUpdate
- flush나 commit을 호출해서 엔티티를 데이터베이스에 수정한 직후에 호출된다.
- 해당 엔티티를 업데이트 후 호출
- @PostRemove
- flush나 commit을 호출해서 엔티티를 데이터베이스에 삭제한 직후에 호출
- 해당 엔티티를 삭제하기 전 호출
2. 스프링 데이터 JPA 사용
이번에는 스프링 데이터 JPA를 사용하여 Member 엔티티에 등록일, 수정일, 등록자, 수정자 속성을 추가해보자.
🔍 @EnableJpaAuditing 등록
📌 DataJpaApplication
package study.datajpa;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
}
먼저 Spring Audit 기능을 활용하기 위해서는 @SpringBootApplication이 적용되어 있는 설정 클래스에 @EnableJpaAuditing을 꼭 적용시켜야 한다.
🔍 공통으로 사용하는 공통 속성 객체 생성
📌 BaseTimeEntity - 등록일, 수정일 속성
package study.datajpa.entity;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
등록자, 수정자 속성을 등록일, 수정일 속성과 나눈 이유는 어떤 엔티티에서는 등록일, 수정일만 쓰이고, 어떤 엔티티에서는 등록일, 수정일, 등록자, 수정자 모두 쓰일 수 있기 때문에 기능별로 나눴다.
@CreateDate 애노테이션은 엔티티가 생성되어 저장될 때 자동으로 현재 시간을 저장시킨다.
@LastModifiedDate 애노테이션은 엔티티의 값이 변경될 때 자동으로 현재 시간을 저장시킨다.
@EntityListerners 애노테이션은 엔티티를 DB에 적용하기 전/후에 커스텀 콜백을 요청할 수 있는 애노테이션이다. @EntityListeners의 인자로 커스텀 콜백을 요청할 클래스를 지정하면 된다. Auditing을 수행할 때는 JPA에서 제공하는 AuditingEntityListener.class를 인자로 넘기면 된다.
📌 BaseEntity - 등록자, 수정자, 등록일, 수정일 속성
package study.datajpa.entity;
@EntityListeners(AuditingEntityListener.class)
@Getter
@MappedSuperclass
public class BaseEntity extends BaseTimeEntity{
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
BaseTimeEntity를 상속받아서 BaseTimeEntity 속성인 등록일, 수정일을 모두 사용할 수 있다. 그래서 등록자, 수정자, 등록일, 수정일 총 4개의 속성을 가지고 있다.
@CreatedBy 애노테이션은 해당 엔티티를 생성될 때 생성하는 주체가 누구인지 자동으로 삽입한다. 생성하는 주체를 지정하기 위해서는 AuditorAware<T>를 지정해야한다.
@LastModifiedBy 애노테이션은 해당 엔티티가 수정될 때 수정하는 주체가 누구인지 자동으로 삽입한다. 수정하는 주체를 지정하기 위해서 AuditorAware<T>를 지정해야 한다.
AuditorAware이란, Entity의 값이 생성되고 변경될 때 누가 만들었는지, 누가 수정했는지까지 자동으로 값을 업데이트를 해주는 기능이다.
AuditorAware<T>는 스프링 시큐리티와 같이 사용해야 수정자와 등록자를 사용자 정보를 토대로 작성할 수 있다.
참고 자료 : https://umanking.github.io/2019/04/12/jpa-audit/
🔍 AditorAware 빈 등록
📌 DataJpaApplication
package study.datajpa;
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
@Bean
public AuditorAware<String> auditorProvider() {
/*
return new AuditorAware<String>() {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.empty();
}
};
*/
// 람다 표현식으로 변경
return () -> Optional.of(UUID.randomUUID().toString());
}
}
등록자, 수정자를 처리해주는 AuditorAware를 스프링 빈에 등록했다. 아직 스프링 시큐리티를 배우지 않았으므로 수정자, 등록자를 UUID 값으로 대체했다. 실무에서는 세션 정보나 스프링 시큐리티 로그인 정보에서 ID를 받는다.
등록 되거나 수정될 때 마다 auditorProvider() 를 호출해서 결과물을 꺼내간다.
Web 확장 - 도메인 클래스 컨버터
도메인 클래스 컨버터 기능은 컨트롤러에서 파라미터로 엔티티의 ID를 받는 대신 엔티티로 받아서 객체를 바인딩 해주는 기능이다.
트랜잭션이 없는 범위에서 엔티티를 조회했기에 엔티티를 변경하지 않고 조회용으로만 사용해야 한다. 변경해도 DB에 반영이 되지 않는다.
도메인 클래스 컨버터는 리포지토리를 통해 엔티티를 조회한다.
1. 도메인 클래스 컨버터 미사용
📌 MemberController
package study.datajpa.controller;
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
// 도메인 클래스 컨버터 미사용
@GetMapping("/members/{id}")
public String findMember(@PathVariable Long id) {
Member member = memberRepository.findById(id).orElse(null);
return member.getUsername();
}
@PostConstruct
public void init() {
memberRepository.save(new Member("userA"));
}
}
@PathVariable를 통해 엔티티의 id 값으로 엔티티를 조회햇다.
2. 도메인 클래스 컨버터 사용
📌 MemberController
package study.datajpa.controller;
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
// 도메인 클래스 컨버터 사용
@GetMapping("/members2/{id}")
public String findMember2(@PathVariable("id") Member member) {
return member.getUsername();
}
@PostConstruct
public void init() {
memberRepository.save(new Member("userA"));
}
}
HTTP 요청은 회원 id를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환한다.
도메인 클래스 컨버터는 리포지토리를 사용해서 엔티티를 찾는다.
실무에서는 복잡하기 때문에 자주 쓰이진 않는다.
Web 확장 - 페이징과 정렬
스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있다.
1. 페이징과 정렬 예제
🔍 기본 페이징
📌 MemberDto
package study.datajpa.dto;
@Data
public class MemberDto {
private Long id;
private String username;
private String teamName;
public MemberDto(Long id, String username, String teamName) {
this.id = id;
this.username = username;
this.teamName = teamName;
}
public MemberDto(Member member) {
this.id = member.getId();
this.username = member.getUsername();
}
}
📌 MemberController
package study.datajpa.controller;
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
// 페이징 처리
@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
Page<MemberDto> map = page.map(MemberDto::new);
return map;
}
@PostConstruct
public void init() {
for (int i = 0; i < 100; i++) {
memberRepository.save(new Member("user" + i, i));
}
}
...
}
/* 출력 결과의 일부*/
// 반환타입이 Page이므로 TotalCount 쿼리가 생성되었다.
select count(member0_.member_id) as col_0_0_ from member member0_
파라미터로 Pageable를 받을 수 있다. Pageable은 인터페이스이다.
스프링 데이터 JPA가 Pageable 파라미터에 Pageable의 구현체인 org.springframework.data.domain.PageRequest 객체를 생성하고 값을 채워서 주입시켜준다. 이것은 스프링 부트가 Pageable을 처리해주는 argumentResovler를 자동으로 등록해줘서 가능하다.
Pageable 인터페이스는 페이징의 파라미터 정보다. Page 인터페이스는 페이징의 결과 정보다.
memberRepository.findAll(pageable) 처럼 스프링 데이터 JPA의 기본 메서드에 Pageable을 파라미터로 전달하면 된다.
엔티티를 외부에 절대 노출하면 안되기 때문에 page.map()을 통해 Member 엔티티를 DTO 객체로 변환시켰다.
포스트맨으로 출력 결과를 보면 처음 부분에는 페이징 조건에 맞는 MemberDto들을 보여준다. 그리고 마지막 부분에는 Page 정보를 보여준다.
참고로 페이징의 기본 size는 20이다. (한 페이지에 20개씩 보여준다는 의미)
📌 PagingAndSortingRepository 인터페이스
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
/**
* Returns all entities sorted by the given options.
*
* @param sort the {@link Sort} specification to sort the results by, can be {@link Sort#unsorted()}, must not be
* {@literal null}.
* @return all entities sorted by the given options
*/
Iterable<T> findAll(Sort sort);
/**
* Returns a {@link Page} of entities meeting the paging restriction provided in the {@link Pageable} object.
*
* @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
* {@literal null}.
* @return a page of entities
*/
Page<T> findAll(Pageable pageable);
}
스프링 데이터 JPA의 기본 메서드에 파라미터로 Pageable을 전달하면 PagingAndSortingRepository 인터페이스의 findAll() 메서드가 호출된다.
📌 JpaRepository<T, ID> 인터페이스
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
/*
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#findAll()
*/
@Override
List<T> findAll();
/*
* (non-Javadoc)
* @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort)
*/
@Override
List<T> findAll(Sort sort);
...
}
스프링 데이터 JPA의 기본 메서드에 파라미터로 아무것도 전달하지 않으면 JpaRepository 인터페이스의 findAll() 메서드가 호출된다.
🔍 기본 페이징에 현재 페이지 / 사이즈 / 정렬 조건을 요청 파라미터로 전달
위와 동일한 코드일 때 현재 페이지 / 사이즈 / 정렬 조건을 요청 파라미터로 전달할 수 있다.
요청 파라미터 예시) /members?page=0&size=2&sort=id,desc&sort=username,desc
page : 현재 페이지, 0부터 시작한다.
size : 한 페이지에 노출할 데이터 건수
sort : 정렬 조건
2. 페이징 기본값 설정하기
🔍 글로벌 설정 - 스프링 부트 이용
📌 application.yml
spring:
data:
web:
pageable:
default-page-size: 5
max-page-size: 2000
default-page-size : 기본 페이지 사이즈 (한 페이지 당 노출할 데이터 건 수)
max-page-size : 최대 페이지 사이즈
📌 MemberController
package study.datajpa.controller;
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
// 페이징 처리
@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
Page<MemberDto> map = page.map(MemberDto::new);
return map;
}
@PostConstruct
public void init() {
for (int i = 0; i < 100; i++) {
memberRepository.save(new Member("user" + i, i));
}
}
...
}
글로벌 설정대로 size가 5개로 설정 됐다.
🔍 개별 설정 - @PageDefault 애노테이션 이용
📌 MemberController
package study.datajpa.controller;
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
// 페이징 처리 - 개별 설정
@GetMapping("/members/pageSelfConfig")
public Page<MemberDto> listSelfConfig(@PageableDefault(size = 4, sort = "username") Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
Page<MemberDto> map = page.map(MemberDto::new);
return map;
}
@PostConstruct
public void init() {
for (int i = 0; i < 100; i++) {
memberRepository.save(new Member("user" + i, i));
}
}
}
결과를 보면 개별 설정대로 size 4개와 username의 정렬조건이 적용된 것을 알 수 있다.
글로벌 설정보다 개별 설정이 우선권을 가진다.
3. 접두사 처리
페이징 정보가 둘 이상이면 접두사로 구분할 수 있다.
📌 MemberController
package study.datajpa.controller;
@RestController
@RequiredArgsConstructor
public class MemberController {
// 접두사 처리
@GetMapping("/members/twoListPage")
public Page<MemberDto> twoListPage(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable) {
...
}
...
}
@Qualifier 애노테이션에 접두사명을 추가하면 된다. ->"{접두사명}_xxx”
요청 파라미터 예시 : /members?member_page=0&order_page=1
4. Page를 1부터 시작하기
스프링 데이터는 Page를 0부터 시작한다.
만약에 Page를 1부터 시작하게 만들고 싶을 때는 2가지 방법이 존재한다.
🔍 Pageable, Page를 파라미터와 응답 값으로 사용하지 않고 직접 클래스를 만들어서 처리해야 한다.
package study.datajpa.controller;
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
// pageable, page 직접 구현
@GetMapping("/members/page")
public MyPage<MemberDto> listPage() {
PageRequest request = PageRequest.of(new MyPageable());
Page<Member> page = memberRepository.findAll(request);
MyPage<MemberDTO> myPage = page.map(MemberDto::new);
return myPage;
}
...
}
대충 이런식으로 MyPageable 객체와 MyPage 객체를 따로 만들어서 직접 구현하면 된다.
참고자료)
https://blog.naver.com/nateen7248/222387121939
https://velog.io/@chlee4858/Jpa-Pagination-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0
🔍 스프링 부트 설정으로 변경
📌 application.yml
spring:
data:
web:
pageable:
one-indexed-parameters: true
spring.data.web.pageable.one-indexed-parameters를 true로 설정한다. 그러면 자동으로 페이지를1부터 시작할 수 있다. 그러나 이 방법에는 한계가 명확하다. 이 방법은 web에서 page 파라미터를 -1 처리할 뿐이다. 따라서 응답값인 Page에 모두 0페이지 인덱스를 사용하는 한계가 존재한다.
📌 MemberController
package study.datajpa.controller;
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
// 페이징 처리
@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
Page<MemberDto> map = page.map(MemberDto::new);
return map;
}
@PostConstruct
public void init() {
for (int i = 0; i < 100; i++) {
memberRepository.save(new Member("user" + i, i));
}
}
...
}
보면 페이지가 1부터 시작됐지만 Page 정보는 바뀌지 않고 0부터 시작한다는 점을 알 수 있다.
그냥 Page 인덱스를 0부터 쓰는 것이 제일 깔끔하다.
참고) 페이징 처리를 요청 파라미터로 하는 것을 막는 방법
스프링 부트 페이징을 사용하지 않고, 직접 페이징 처리를 하면, 컨트롤러에서 예외를 터트리거나(최대 사이즈는 xxx를 넘을 수 없다. 이런식으로 메시지를 반환하면 됩니다.) 또는 최대 사이즈가 넘으면 최대 사이즈로 제한해서 리포지토리에 넘기면 된다.
스프링 부트 설정
👀 참고 자료
https://devbksheen.tistory.com/219
https://velog.io/@minji104/CreatedBy-UpdatedBy-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84
'[JPA] > 실전! 스프링 데이터 JPA' 카테고리의 다른 글
[JPA] 나머지 기능들 (0) | 2022.04.22 |
---|---|
스프링 데이터 JPA 분석 (0) | 2022.04.21 |
[JPA] 쿼리 메소드 기능 (0) | 2022.04.17 |
[JPA] 공통 인터페이스 기능 (0) | 2022.04.16 |
[JPA] 예제 도메인 모델 (0) | 2022.04.16 |