[Spring] ์Šคํ”„๋ง ํŽ˜์ด์ง•(Paging) ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๋ฐฉ๋ฒ• (Thymeleaf, JPA)

๐Ÿ“Œ ์‚ฌ์šฉ ๊ธฐ์ˆ  ์Šคํƒ
- ์Šคํ”„๋ง ๋ถ€ํŠธ 2.7.12
- ํƒ€์ž„๋ฆฌํ”„
- Spring Data JPA
- H2 DB
- Lombok

 

 

์ด๋ฒˆ์— ๊ฒŒ์‹œํŒ์— ํ•„์ˆ˜์š”์†Œ์ธ ํŽ˜์ด์ง• ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๋ณด์•˜๋‹ค.

ํŽ˜์ด์ง• ๊ธฐ๋Šฅ์€ Springframework๊ฐ€ ์ œ๊ณตํ•˜๋Š” @PageableDefault ์–ด๋…ธํ…Œ์ด์…˜, Pageable, Page ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด ํฌ์ŠคํŒ…์€ ํŽ˜์ด์ง• ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ์„ค๋ช…์„ ๋‹ค๋ฃจ๋ฏ€๋กœ ํŽ˜์ด์ง•๊ณผ ๊ด€๊ณ„ ์—†๋Š” ์ฝ”๋“œ๋Š” ๊ณผ๊ฐํžˆ ๋บ์œผ๋ฉฐ, ๋‚˜๋จธ์ง€ ๋ถ€๋ถ„์€ ์•„๋ž˜ ๋งํฌ๋กœ ๋Œ€์ฒดํ•œ๋‹ค.

- ๊ฒŒ์‹œํŒ ์›น ์„œ๋น„์Šค์— ๊ด€ํ•œ ์ฝ”๋“œ : 
https://github.com/Hyeon0208/posting-webservice

- OAuth๋กœ ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ :

2023.05.30 - [JAVA/Spring] - [Spring] Google OAuth ๋กœ๊ทธ์ธ API ํ‚ค ๋ฐœ๊ธ‰ ๋ฐ ๊ตฌ๊ธ€ ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ตฌํ˜„

 

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:
    open-in-view: false
    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);
	}
}

 

ํŽ˜์ด์ง• ๊ธฐ๋Šฅ์ด ์ •์ƒ์ ์œผ๋กœ ์ ์šฉ๋˜์–ด ์ž‘๋™ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค!

ํŽ˜์ด์ง• ๊ธฐ๋Šฅ์€ ์–ด๋Š ์›น์—์„œ๋‚˜ ํ•ญ์ƒ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์ด๋ฏ€๋กœ ์•Œ์•„๋‘˜ ํ•„์š”๊ฐ€ ์žˆ์„๊ฒƒ ๊ฐ™๋‹ค.