- 주요 객체
1. Page<T> : 페이지 정보를 담게 되는 인터페이스
2. Pageable : 페이지 처리에 필요한 정보를 담게 되는 인터페이스
- 관계 및 사용
1. PageRequest에 의해 Pageable에 페이징 정보가 담겨 객체화 된다.
2. Pageable이 JpaRepository가 상속된 인터페이스의 메서드에 파라미터로 전달된다.
3. 2번의 메서드의 return으로 Page<T>가 전달 된다.
4. 전달된 Page<T>에 담겨진 Page 정보를 바탕으로 로직을 처리하면 된다.
- PageRequest의 메서드
1. of(int page, int size) : 0부터 시작하는 페이지 번호와 개수, 정렬이 지정되지 않은 상태
2. of(int page, int size, Sort sort) : 페이지 번호와 개수, 정렬 관련 정보
- Page<T>의 메서드
1. getTotalPages() : 총 페이지 수
2. getTotalElements() : 전체 개수
3. getNumber() : 현재 페이지 번호
4. getSize() : 페이지 당 데이터 개수
5. hasnext() : 다음 페이지 존재 여부
6. isFirst() : 시작페이지 여부
7. getContent(), get() : 실제 컨텐츠를 가지고 오는 메서드 .getContext는 List<Entity> 반환, get()은 Stream<Entity> 반환
- PostController.java
.....
@GetMapping("/posts")
// public List<PostResponse> getList(@RequestParam int page) {
// 위 코드는 결국 수동으로 만들어준 값을 받아오기 때문에 yaml에 작성한 page 보정이 적용되지 않는다.
public List<PostResponse> getList(Pageable pageable) {
return postService.getList(pageable);
}
}
- 페이징을 할 때 위처럼 @RequestParam을 통해 page를 받아 페이징을 할 수 있지만 위의 경우 클래스를 직접 생성하여 사용하는 것이기 때문에 yaml에 작성한 page의 보정이 적용되지 않는다.
- 반면에 Pageable 클래스를 사용하게 되면, 스프링에서 제공하는 클래스를 사용하는 것이기 때문에 yaml 파일에 작성한 page에 관한 설정들이 적용된다.
- Json 방식으로 PostResponse의 객체들이 body를 통해 클라이언트에게 전달된다.
- PostService.java
...
public List<PostResponse> getList(Pageable pageable) {
return postRepository.findAll(pageable).stream()
.map(post -> new PostResponse(post))
.collect(Collectors.toList());
}
- Controller를 통해 넘어온 Pageable 정보를 가지고, DB로부터 Post 객체들을 가져온다.
- findAll()메서드의 경우 매개변수로 Pageable 참조형을 받을 수 있다. Pageable을 매개변수로 받게되면, DB로부터 데이터를 받아올 때 원하는 만큼의 데이터를 받아올 수 있다.
- 또한 반환값이 PostResponse이기 때문에 DB로부터 Post 객체를 받아오지만, map을 통해 받아온 Post 엔티티들을 PostResponse 객체로 변환한다.
- PostServiceTest.java
@Test
@DisplayName("글 1페이지 조회")
void test3() {
//givne
List<Post> requestPosts = IntStream.range(1, 31)
.mapToObj(i -> Post.builder()
.title("Title : " + i)
.content("Content : " + i)
.build())
.collect(Collectors.toList());
postRepository.saveAll(requestPosts);
Pageable pageable = PageRequest.of(0, 5, Sort.Direction.DESC, "id");
//when
List<PostResponse> postResponses = postService.getList(pageable);
//then
assertEquals(5L, postResponses.size());
assertEquals("Title : 30", postResponses.get(0).getTitle());
assertEquals("Title : 26", postResponses.get(4).getTitle());
}
}
- Test 코드에서 Stream을 통해 총 30개의 Post 객체를 생성하고, List에 담고 해당 리스트를 DB에 저장한다.
- PageRequest.of()를 통해 페이징 처리 정보를 생성한다. 페이징 정보를 0 페이지부터 5개의 데이터를 id를 기준으로 내림차순으로 출력하라는 정보가 담겨있다.
- DB에 데이터를 저장 한 뒤 getList()메서드에 pageable 정보를 넣어 DB에 저장된 데이터를 페이징 정보에 따라 출력한다.
- PostControllerTest.java
.....
@Test
@DisplayName("글 여러개 조회")
void test5() throws Exception {
//given
List<Post> requestPosts = IntStream.range(1, 31)
.mapToObj(i -> Post.builder()
.title("Title : " + i)
.content("Content : " + i)
.build())
.collect(Collectors.toList());
postRepository.saveAll(requestPosts);
//expected
mockMvc.perform(MockMvcRequestBuilders.get("/posts?page=1&sort=id,desc")
.contentType(APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()", Matchers.is(5)))
.andExpect(jsonPath("$[0].id").value(30))
.andExpect(jsonPath("$[0].title").value("Title : 30"))
.andExpect(jsonPath("$[0].content").value("Content : 30"))
.andDo(print());
}
}
- Controller에서의 테스트는 MockMvc를 통해 Test 할 수 있다.
- 먼저 엔티티를 DB에 저장하고, get 방식으로 /post?page=1&sort=id,desc의 요청이 들어왔을 때, 요청에 맞게 데이터가 반환되는지 확인해본다.
* 페이징의 경우 Get 요청을 통해 넘어온 파라미터를 우선순위로 하고, Get 요청을 통해 넘어온 파라미터가 없을 경우 PageRequest나 yml 파일에 설정된 값을 가져와 페이징 한다.
- Querydsl
- build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// --------------- Querydsl 추가
implementation 'com.querydsl:querydsl-core'
implementation 'com.querydsl:querydsl-jpa'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
// ---------------
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
- Querydsl를 사용하기 위해 gradle에 코드를 추가해준다.
- classes를 통해 소스 코드를 컴파일 한다.
- classes를 통해 소스 코드를 컴파일하면 위와 같이 build 폴더가 생성되고 위의 경로를 따라가면 QPost라는 클래스가 새롭게 생성된 것을 확인할 수 있다.
- QPost를 통해 쿼리와 유사한 코드를 작성할 수 있음과 동시에 Java 코드로 작성하기 때문에 컴파일 단계에서 오류를 발견할 수 있다.
- QueryDslConfig.java
@Configuration
public class QueryDslConfig {
@PersistenceContext
// 엔티티 매니저를 주입받을 때 사용된다.
public EntityManager em;
// JPA 구현체가 엔티티 매니저를 자동으로 주입해준다.
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
- Querydsl을 사용하기 위해선 JPAQueryFactory 클래스를 통해 엔티티 매니저를 파라미터로 넘겨 사용한다.
- JPAQueryFactory를 사용하는 이유는 다음과 같다.
1. 타입 안정성과 컴파일 타임 검사
2. 가독성
3. 코드 재사용
4. 성능 최적화
- PostRepositoryCustom.java
public interface PostRepositoryCustom {
List<Post> getList(PostSearch postSearch);
}
- Custom 클래스의 경우 Jpa에서 제공하는 find(), save()와 같이 쿼리를 호출하기 위한 메서드를 만들기 위해서 사용하며, Qyerydsl의 장점인 java 객체를 가지고 쿼리를 짤 수 있다.
- PostRepositoryImpl.java
@RequiredArgsConstructor
public class PostRepositoryImpl implements PostRepositoryCustom {
private final JPAQueryFactory jpaQueryFactory;
@Override
public List<Post> getList(PostSearch postSearch) {
return jpaQueryFactory.selectFrom(QPost.post)
.limit(postSearch.getSize())
// .offset((long)(postSearch.getPage()-1) * postSearch.getSize())
.offset(postSearch.getOffset())
.orderBy(QPost.post.id.desc())
.fetch();
}
}
- PostRepositoryCustom 인터페이스를 구현한 클래스로 getList() 메서드를 구현한 클래스이다. Querydsl을 사용하기 위해 JPAQueryFactory 객체를 사용하고, Querydsl를 통해 생성된 Q객체를 통해 쿼리를 작성한다.
- PostSearch 클래스를 통해 Controller -> Service를 통해 넘어온 필드의 값을 통해 DB에서 가져올 데이터를 조절한다.
- PostRepository.java
public interface PostRepository extends JpaRepository<Post, Long>, PostRepositoryCustom {
}
- PostRepository 인터페이스가 JpaRepository, PostRepositoryCustom 인터페이스를 상속 받는다.
- PostRepositoryCustom의 구현체로 PostRepositoryImpl이 있고, Impl의 경우 Querydsl을 사용하고 있다.
- PostService.java
@Slf4j
@Service
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
public List<PostResponse> getList(PostSearch postSearch) {
return postRepository.getList(postSearch).stream()
.map(post -> new PostResponse(post))
.collect(Collectors.toList());
}
}
- Querydsl로 생성한 getList() 메서드를 통해 쿼리를 실행하여 원하는 DB에서 원하는 값을 얻는다.
- PostSearch 클래스를 통해 DB에서 데이터를 가져올 데이터를 조절한다.
- PostResponse 클래스의 생성자 중 Post를 파라미터로 받는 생성자에 Post 객체를 전달하게 되면 id, title, content를 받아오게된다.
- PostServiceTest.java
@Test
@DisplayName("글 1페이지 조회")
void test3() {
List<Post> requestPosts = IntStream.range(1, 31)
.mapToObj(i -> Post.builder()
.title("Title : " + i)
.content("Content : " + i)
.build())
.collect(Collectors.toList());
postRepository.saveAll(requestPosts);
PostSearch postSearch = PostSearch.builder()
.page(1)
.size(10)
.build();
//when
List<PostResponse> postResponses = postService.getList(postSearch);
//then
assertEquals(10L, postResponses.size());
assertEquals("Title : 30", postResponses.get(0).getTitle());
}
}
- PostSearch 클래스를 통해 개발자가 원하는 페이징 정보를 포함하여 body에 페이징 정보에 따라 정제된 데이터를 클라이언트에게 전달한다.
- PostControllerTest.java
@Test
@DisplayName("페이지를 0으로 요청하면 첫 페이지를 가져온다.")
void test6() throws Exception {
//given
List<Post> requestPosts = IntStream.range(1, 31)
.mapToObj(i -> Post.builder()
.title("Title : " + i)
.content("Content : " + i)
.build())
.collect(Collectors.toList());
postRepository.saveAll(requestPosts);
//expected
mockMvc.perform(MockMvcRequestBuilders.get("/posts?page=0&size=10")
.contentType(APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()", Matchers.is(10)))
.andExpect(jsonPath("$[0].id").value(30))
.andExpect(jsonPath("$[0].title").value("Title : 30"))
.andExpect(jsonPath("$[0].content").value("Content : 30"))
.andDo(print());
}
}
- 실제 웹 애플리케이션을 실행 시킨 뒤 DB에 20개의 데이터를 저장한 뒤 위와 같이 페이징 정보를 전달하게 페이징 정보에 대한 데이터가 Json 형태로 출력되는 것을 확인할 수 있다.
'Programming > Spring' 카테고리의 다른 글
Spring, Java - Builder 패턴 분석 (0) | 2023.09.06 |
---|---|
Spring - 게시글 수정 / 삭제 (0) | 2023.09.05 |
Spring - 게시글 조회 (다건 조회, 임시 H2 활용한 실제 서비스 확인) (0) | 2023.08.30 |
Spring - 게시글 조회, 클래스 분리 (단건) (0) | 2023.08.29 |
Spring - 클래스 분리, 데이터 저장 (Builder, ObjectMapper, 상황 별Controller 반환 값 등...) (0) | 2023.08.29 |