스프링 부트, JPA, Thymeleaf를 이용한 페이징 처리 3 - 페이징 구현 (서버)

Entity는 생성했고 view도 어느정도 구색을 갖췄기 때문에 DB에서 데이터를 가져다 view에다 보내는 코드가 필요하다.
이 프로젝트에서는 MyBatis와 같이 직접 쿼리를 작성하여 페이징 게시판을 개발하지 않을 것이다.
대신 손쉽고 빠른 개발을 위해 Spring data JPA를 사용하여 페이징 게시판을 개발할 것이다.

JPA를 쓰면 좋은 이유??

  • 개발이 정말 빠르다. 회사에서는 MyBatis를 사용하는데 뭐만 하려고만하면 쿼리짜고 mapper interface 생성하고… 그러는 동안 개발의 흐름이 자주 끊기곤 했다. 반면 JPA를 사용하면 쿼리 작성 없이 바로바로 개발을 이어갈 수 있다.
  • DB 별로 다른 SQL 문법을 자동으로 적용해서 쿼리를 생성해준다. Oracle SQL 문법 다르고, MySQL 문법 다르고, DB 별로 크고 작게 문법이 서로 다르다(문법이라고 표현했지만 주로 방언 - Dialect 라고 한다). 만약 개발이 다 끝났는데 DB가 바뀐다면?? 이때 Paging 쿼리가 한,두개가 아니라면(Oracle ROWNUM…)?? 이러한 문제를 Dialect를 변경하고 약간의 테스트를 거치면 빠르게 DB 변경이 가능하다.

  • Pageable 간단하게 짚고가기

JPA를 사용하기에 앞서 Pageable에 대해 먼저 짚고 넘어 가보자.
Pageable은 Spring에서 페이징 기능을 위한 파라미터들을 추상화 시킨 인터페이스이다.
(인터페이스 자체는 매우 간단하고 메서드명도 직관적이기 때문에 소스를 까보는 것도 좋다)
이 Pageable을 아래와 같이 Controller의 RequestMapping 메서드 인자로 넣을 수 있다.

  • BoardController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.devson.pagination.web.controller;

import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class BoardController {

@GetMapping("/boards")
public String boardView(@PageableDefault Pageable pageable) {
return "board";
}
}

Pageable을 디버그 모드로 확인해보면 pageable 객체에 page, size, sort 프로퍼티가 있는 것을 확인할 수 있다.
앞으로 pageable을 통해 게시판 페이징 기능을 구현할 것이다.

(여기서 page가 0 부터 시작하는 것을 기억하자)


  • Repository 생성

그럼 JPA를 사용해서 DB 데이터를 조회하는 Repository 클래스를 생성해보자

  • UserRepository
1
2
3
4
5
6
7
package com.devson.pagination.web.repository;

import com.devson.pagination.web.domain.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<UserEntity, Long> {
}
  • BoardRepository
1
2
3
4
5
6
7
package com.devson.pagination.web.repository;

import com.devson.pagination.web.domain.BoardEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BoardRepository extends JpaRepository<BoardEntity, Long> {
}

단순히 Repository 인터페이스에 JpaRepository를 상속 받으면 끝이다. @Repository도 필요없다.
이제 기본적인 CRUD는 생성한 Repository를 통해 할 수 있다.


TMI
JpaRepository를 소스를 까보면 @NoRepositoryBean가 있는데
이는 해당 어노테이션이 달려있는 인터페이스를 스프링이 Repository로 등록하지 못하게 하기 위함이다.
JpaRepository는 bean으로 등록이 돼있지 않지만 JpaRepository를 상속 받은 인터페이스는 bean으로 등록이 되는 이유이다.

1
2
3
4
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
// 생략
}

  • Service 생성

  • BoardService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.devson.pagination.web.service;

import com.devson.pagination.web.domain.BoardEntity;
import com.devson.pagination.web.repository.BoardRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Service
public class BoardService {
private BoardRepository boardRepository;

public BoardService(BoardRepository boardRepository) {
this.boardRepository = boardRepository;
}

public Page<BoardEntity> getBoardList(Pageable pageable) {
int page = (pageable.getPageNumber() == 0) ? 0 : (pageable.getPageNumber() - 1); // page는 index 처럼 0부터 시작
pageable = PageRequest.of(page, 10);

return boardRepository.findAll(pageable);
}
}

boardRepository.findAll 메서드의 파라미터로 Pageable 객체를 사용할 수 있다.
Page 객체를 리턴하며 Pageable과 마찬가지로 페이징 기능을 위해 추상화 시킨 인터페이스이다.
getBoardList 메서드를 한 줄씩 확인해보면

1
int page = (pageable.getPageNumber() == 0) ? 0 : (pageable.getPageNumber() - 1);

Pageable의 page는 index 처럼 0 부터 시작이다.
하지만 주로 게시판에서는 1 부터 시작하기 때문에 사용자가 보려는 페이지에서 -1 처리를 해준 것이다.

1
pageable = PageRequest.of(page, 10);

Pageable 인터페이스를 확인해보면 알겠지만 getter는 있지만 setter는 없다.
그래서 PageRequest.of 메서드를 사용하여 새로운 pageable 객체를 생성한다.

1
return boardRepository.findAll(pageable);

JPA가 정말 개발에 편리한 이유 중에 하나이다.
사용하는 SQL의 문법(방언)에 맞게 페이징 쿼리를 만들어서 DB에 데이터를 가져온다!
토이프로젝트를 만들 때 SQL 쿼리 짜는데 고통을 받지 않아도 된다!


  • 페이징에 필요한 데이터 넣기

페이징을 확인하기 위해서는 데이터가 필요하기 때문에 서버 실행 후에 DB에 데이터를 넣어주는 코드를 추가한다.
여기서는 Main Application class에 CommandLineRunner를 리턴하는 메서드를 생성하여 코드를 추가했다.

  • PaginationApplication
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.devson.pagination;

import com.devson.pagination.web.domain.BoardEntity;
import com.devson.pagination.web.domain.UserEntity;
import com.devson.pagination.web.repository.BoardRepository;
import com.devson.pagination.web.repository.UserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import java.util.stream.IntStream;

@SpringBootApplication
public class PaginationApplication {

public static void main(String[] args) {
SpringApplication.run(PaginationApplication.class, args);
}

/**
* 154 개의 사용자와 게시물을 생성
* @param userRepository
* @param boardRepository
* @return
*/
@Bean
public CommandLineRunner initData(UserRepository userRepository, BoardRepository boardRepository) {
return args ->
IntStream.rangeClosed(1, 154).forEach(i -> {
UserEntity user = UserEntity.builder()
.name("tester" + i)
.build();

userRepository.save(user);

BoardEntity board = BoardEntity.builder()
.title("test" + i)
.writer(user)
.build();

boardRepository.save(board);
});
}
}

페이징을 확인하기 위해 일부러 어중간하게 154개의 데이터를 넣었다.

데이터가 제대로 들어갔는지 http://localhost:8080/h2-console 에서 데이터를 확인해보자


  • model에 담아 view로 Page 정보 넘기기

데이터는 제대로 들어갔으니 이제 model에 담아 view에 넘겨주기만 하면 (여기서는 복잡한 로직이 없어서) Controller의 역할은 끝이다.

  • BoardController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.devson.pagination.web.controller;

import com.devson.pagination.web.domain.BoardEntity;
import com.devson.pagination.web.service.BoardService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Slf4j
@Controller
public class BoardController {
private BoardService boardService;

public BoardController(BoardService boardService) {
this.boardService = boardService;
}

@GetMapping("/boards")
public String boardView(@PageableDefault Pageable pageable, Model model) {
Page<BoardEntity> boardList = boardService.getBoardList(pageable);
model.addAttribute("boardList", boardList);

log.debug("총 element 수 : {}, 전체 page 수 : {}, 페이지에 표시할 element 수 : {}, 현재 페이지 index : {}, 현재 페이지의 element 수 : {}",
boardList.getTotalElements(), boardList.getTotalPages(), boardList.getSize(),
boardList.getNumber(), boardList.getNumberOfElements());

return "board";
}
}

debug mode로 개발하는 것을 추천하지만 Page는 해당 페이지의 정보를 주로 메서드로 가져와야하기 때문에 debug log를 찍어놨다.

로그와 JPA가 자동으로 생성하는 SQL 쿼리를 확인하기 위해 application.properties에 로그 관련 설정을 추가한다.

  • application.properties
1
2
3
4
logging.level.com.devson.pagination=debug

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true

서버를 동작 시킨 후 http://localhost:8080/boards 로 접속하면 다음과 같은 로그를 확인 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
http://localhost:8080/boards 로 접속

Hibernate:
select
boardentit0_.id as id1_0_,
boardentit0_.title as title2_0_,
boardentit0_.user_id as user_id3_0_
from
board boardentit0_ limit ?
Hibernate:
select
count(boardentit0_.id) as col_0_0_
from
board boardentit0_

총 element 수 : 154, 전체 page 수 : 16, 페이지에 표시할 element 수 : 10, 현재 페이지 index : 0, 현재 페이지의 element 수 : 10


http://localhost:8080/boards?page=2 로 접속

Hibernate:
select
boardentit0_.id as id1_0_,
boardentit0_.title as title2_0_,
boardentit0_.user_id as user_id3_0_
from
board boardentit0_ limit ? offset ?
Hibernate:
select
count(boardentit0_.id) as col_0_0_
from
board boardentit0_

총 element 수 : 154, 전체 page 수 : 16, 페이지에 표시할 element 수 : 10, 현재 페이지 index : 1, 현재 페이지의 element 수 : 10

위에서 확인 할 수 있듯 Spring JPA는 편리한 기능들을 제공하니 우리는 더욱 서비스 로직에 집중할 수 있다.

Share