Programming/Spring

Spring - 페이징 (Jpa, Querydsl)

잇(IT) 2023. 8. 31. 12:51

- 주요 객체

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 형태로 출력되는 것을 확인할 수 있다.

 

 

728x90