[Spring] 스프링 페이징(Paging) 기능 구현 방법 (Thymeleaf, JPA)

반응형
📌 사용 기술 스택

- 스프링 부트 2.7.12
- 타임리프
- Spring Data JPA
- H2 DB
- Lombok

 

 

이번에 게시판에 필수요소인 페이징 기능을 추가해보았다.

페이징 기능은 Springframework가 제공하는 @PageableDefault 어노테이션, Pageable, Page 인터페이스로 구현할 수 있다.

 

이 포스팅은 페이징 기능을 구현하는 설명을 다루므로 페이징과 관계 없는 코드는 과감히 뺐으며, 나머지 부분은 아래 링크로 대체한다.

 

- 게시판 웹 서비스에 관한 코드 : 
https://github.com/Hyeon0208/posting-webservice

 

GitHub - hyeon0208/posting-webservice

Contribute to hyeon0208/posting-webservice development by creating an account on GitHub.

github.com

 

- OAuth로 소셜 로그인 구현 :

2023.05.30 - [JAVA/Spring] - [Spring] Google OAuth 로그인 API 키 발급 및 구글 소셜 로그인 구현

 

[Spring] Google OAuth 로그인 API 키 발급 및 구글 소셜 로그인 구현

Google Cloud 프로젝트 생성 1. 먼저 아래의 주소로 들어간다. Google 클라우드 플랫폼 로그인 Google 클라우드 플랫폼으로 이동 accounts.google.com 빨간색 밑줄친 부분을 선택하고 -> 새 프로젝트 생성을 클

hstory0208.tistory.com

 

aplication.yml 설정
# database 연동 설정
spring:
  profiles:
    include: oauth
  datasource:
      driver-class-name: org.h2.Driver
      url: jdbc:h2:tcp://localhost/~/board
      username: sa
  thymeleaf:
    cache: false

  # spring data jpa 설정
  jpa:
    show-sql: true
    hibernate:
      dialect: org.hibernate.dialect.MySQLInnoDBDialect
      ddl-auto: create

게시판 페이징 기능 구현하기

1. 먼저 각 페이지마다 등록된 데이터(여기서는 게시글)이 있으므로 Entity 객체를 만들어 DB에 등록한다.

 

Posts
@Getter
@NoArgsConstructor
@Entity
public class Posts {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    @Column
    private String title;

    @NotNull
    @Column(length = 1000)
    private String content;

    @Column
    private String author;

    @Builder
    public Posts(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }
}

 

2. Posts 엔티티 객체를 저장할 레포지토리를 생성한다.

 

PostsRepository
public interface PostsRepository extends JpaRepository<Posts, Long> {
}

 

3. 테스트용 데이터를 DB에 추가

페이징기능 구현에 대한 설명만을 포함하고 미리 여러 게시글을 등록해논 상태로 페이징 기능이 잘 작동하는지 확인하기 위해 테스트용 데이터를 추가하겠다.

 

TestDataInit
@RequiredArgsConstructor
public class TestDataInit {

    private final PostsRepository postsRepository;

    @EventListener(ApplicationReadyEvent.class)
    public void initData() {
        postsRepository.save(Posts.builder()
                .title("글1")
                .content("가나다라마바사")
                .author("김부각")
                .build());

        postsRepository.save(Posts.builder()
                .title("글2")
                .content("zxcbzxcv")
                .author("맛김치")
                .build());

        postsRepository.save(Posts.builder()
                .title("글3")
                .content("qwetyhgbz")
                .author("쫄면")
                .build());

        postsRepository.save(Posts.builder()
                .title("글4")
                .content("asdfasdfa")
                .author("춤추는네오")
                .build());

        postsRepository.save(Posts.builder()
                .title("글5")
                .content("fasdfas")
                .author("배개에 파묻힌 프로도")
                .build());
    }
}

 

 

4. DB에서 데이터를 넘겨 받을 DTO 생성

DB와 View 사이의 역할을 분리하고,Entity 객체를 보호하기 위해Entity와 DTO를 분리해서 사용하였다.

 

PostsResponseDto
  • Posts 엔티티에 작성한 ID 값과 Colum을 다 사용할 것이기에 다음과 같이 DTO객체를 생성.
  • 생성자는 PostsRepository에서 Posts Entity 객체를 받아 그 값을 갖도록 하였다.
@Getter
public class PostsResponseDto {
    private Long id;
    private String title;
    private String content;
    private String author;

    public PostsResponseDto(Posts entity) {
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.content = entity.getContent();
        this.author = entity.getAuthor();
    }
}

 

5. Respository에 접근할 PostsService를 만들자.

스프링이 제공하는 Pageable을 매개변수로 받아 paging 로직 구현하였다.

 

PostsService
  • paging로직은 PostsRepository에서 있는 모든 데이터를 아래의 주석과 같은 조건으로 가져와 Posts객체타입을 같은 Page를 반환한다.
  • 이 Page<Posts>는 리스트와 같은 개념이다.
  • 가져온 모든 Posts 엔티티 객체들을 PostsResponseDto로 변환해 이 객체 값들을 갖는 Page<PostsResponseDto>를 반환한다.
@Service
@RequiredArgsConstructor
public class PostsService {

    private final PostsRepository postsRepository;

    public Page<PostsResponseDto> paging(Pageable pageable) {
        int page = pageable.getPageNumber() - 1; // page 위치에 있는 값은 0부터 시작한다.
        int pageLimit = 3; // 한페이지에 보여줄 글 개수

        // 한 페이지당 3개식 글을 보여주고 정렬 기준은 ID기준으로 내림차순
        Page<Posts> postsPages = postsRepository.findAll(PageRequest.of(page, pageLimit, Sort.by(Direction.DESC, "id")));

        // 목록 : id, title, content, author
        Page<PostsResponseDto> postsResponseDtos = postsPages.map(
                postPage -> new PostsResponseDto(postPage));

        return postsResponseDtos;
    }
}

 

6. 컨트롤러를 생성해 url를 매핑하고 view에 데이터를 넘겨주자.

약간의 로직이 포함되어 있는데 주석을 통해 이해할 수 있을 것이다.

다만 startPage와 endPage의 계산방법은 다음과 같다고만 참고하자

 

PostsController
@Controller
@RequiredArgsConstructor
public class PostsController {

    private final PostsService postsService;

    // @PageableDefault(page = 1) : page는 기본으로 1페이지를 보여준다.
    @GetMapping("/posts/paging")
    public String paging(@PageableDefault(page = 1) Pageable pageable, @Login SessionUser user, Model model) {
        Page<PostsResponseDto> postsPages = postsService.paging(pageable);

        /**
         * blockLimit : page 개수 설정
         * 현재 사용자가 선택한 페이지 앞 뒤로 3페이지씩만 보여준다.
         * ex : 현재 사용자가 4페이지라면 2, 3, (4), 5, 6
         */
        int blockLimit = 3;
        int startPage = (((int) Math.ceil(((double) pageable.getPageNumber() / blockLimit))) - 1) * blockLimit + 1;
        int endPage = Math.min((startPage + blockLimit - 1), postsPages.getTotalPages());

        model.addAttribute("postsPages", postsPages);
        model.addAttribute("startPage", startPage);
        model.addAttribute("endPage", endPage);
        return "paging";
    }
}

 

7. paging.html

간단하게 게시판 테이블과 페이징만 나오도록 하는 HTML이다.

 

타임리프 문법에 대한 정보는 아래 링크를 클릭 

2023.04.02 - [JAVA/Thymeleaf] - Thymeleaf(타임리프)란 ? 타임리프의 기본 기능알아보기

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>
<h1>게시판 웹 서비스</h1>    
    <div class="col-md-12">
        <!-- 목록 출력 영력 -->
        <table class="table table-horizontal table-bordered">
            <tr>
                <th>게시글 번호</th>
                <th>제목</th>
                <th>작성자</th>
                <th>최종수정일</th>
            </tr>
            <tr th:each="post: ${postsPages}">
                <td th:text="${post.id}"></td>
                <td><a th:href="@{|/posts/update/${post.id}|(page=${postsPages.number + 1})}" th:text="${post.title}"></a></td>
                <td th:text="${post.author}"></td>
            </tr>
        </table>
        
        <!-- 첫번째 페이지로 이동 -->
        <a th:href="@{/posts/paging(page=1)}">첫 페이지 </a>
        
        <!-- 이전 링크 활성화 비활성화 -->
        <a th:href="${postsPages.first} ? '#' : @{/posts/paging(page=${postsPages.number})}"> 이전 </a>

        <!-- 페이지 번호 링크(현재 페이지는 숫자만)
                for(int page=startPage; page<=endPage; page++)-->
        <span th:each="page: ${#numbers.sequence(startPage, endPage)}">
        
		<!-- 현재페이지는 링크 없이 숫자만 -->
            <span th:if="${page == postsPages.number + 1}" th:text="${page}"></span>
            <!-- 현재페이지 링크 X, 다른 페이지번호에는 링크를 보여준다 -->
            <span th:unless="${page == postsPages.number + 1}">
        <a th:href="@{/posts/paging(page=${page})}" th:text="${page}"></a>
            </span>
        </span>

        <!-- 다음 링크 활성화 비활성화 -->
        <a th:href="${postsPages.last} ? '#' : @{/post/paging(page=${postsPages.number + 2})}"> 다음 </a>
        
        <!-- 마지막 페이지로 이동 -->
        <a th:href="@{/posts/paging(page=${postsPages.totalPages})}"> 마지막 페이지</a>

    </div>
    </div>
    
</body>
</html>

 

 

페이징 기능 정상 작동 확인

Main 클래스인 BoardApplication에 테스트용 데이터를 생성하는 TestDataInit을 Bean 등록해주고 실행해보자.

 

BoardApplication
@SpringBootApplication
public class BoardApplication {

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

	@Bean
	public TestDataInit testDataInit(PostsRepository postsRepository) {
		return new TestDataInit(postsRepository);
	}
}

 

페이징 기능정상적으로 적용되어 작동하는 것을 확인할 수 있다!

페이징 기능은 어느 웹에서나 항상 필요한 기능이므로 알아둘 필요가 있을것 같다.