[Spring Boot] 페이징 기능 구현 ( + 페이징, 정렬, 검색, 에러 메시지 포함 예제)

2024. 1. 3. 21:33· Spring Boot
목차
  1. 1. 페이징 (paging) 기능이란 ?
  2. 2. 페이징 구현 방법 (Spring Boot)
  3. 2-1. 레포지토리 (Repository) 단
  4. 2-2. 서비스 (Service) 단
  5. 2-3. 컨트롤러 (Controller) 단
  6. 2-4. 페이지 내부 정보
  7. 3. 페이징, 정렬, 검색, 에러 메시지 포함 예제
  8. 3-0. build.gradle, application.yml
  9. 3-1. Player (domain)
  10. 3-2. SearchForm (domain)
  11. 3-3. PlayerRepository (repository)
  12. 3-4. PlayerService (service)
  13. 3-5. PlayerController (controller)
  14. 3-6. home.html (templates)
  15. 3-7. error.html (templates)
  16. 4. 예제 결과 확인
  17. 4-1. 첫 화면
  18. 4-2. 검색 기능 확인
  19. 4-3. 에러 메시지 확인

1. 페이징 (paging) 기능이란 ?

  • 데이터를 일정한 크기로 나누어서 (페이지화 시켜서) 보여주는 기능
  • 많은 양의 데이터를 한 번에 보여주는 것이 아니므로 스크롤을 내리는 등의 불편함, 긴 로딩시간 등을 해소 가능
  • 페이징 기능에는 정렬, 검색 등의 기능이 포함되어 있으므로 함께 사용하면 사용자가 더 빠르고 정확하게 데이터를 찾을 수 있음
  • 즉, 대량의 데이터를 효율적으로 관리하고 사용자에게 편리한 접근성을 제공하는 기능

 

2. 페이징 구현 방법 (Spring Boot)

  • 스프링 부트에서는 페이징을 구현하는 방법 2가지 존재
  • 컨트롤러 (Controller) 단에서 사용 => Pageable, PageRequest
  • 레포지토리 (Repository), 서비스 (Service) 단 등에도 리턴 타입을 Page로 바꿔주는 등의 코드 조작이 필요

 

2-1. 레포지토리 (Repository) 단

  • JpaRepository를 extends하는 인터페이스 구현
  • findAll 등의 메서드를 리턴 타입이 Page가 되도록 설정
  • 메서드의 매개변수로 Pageable 변수를 넣어줘야 함
public interface UserRepository extends JpaRepository<User, Long> {

    Page<User> findAll(Pageable pageable);
}

 

2-2. 서비스 (Service) 단

  • 레포지토리와 동일하게 리턴 타입은 Page, 매개변수로 Pageable 변수 설정
@Service
public class UserService {
	
    private final UserRepository userRepository;
    
    public Page<User> findAll(Pageable pageable){
    	return userRepository.findAll(pageable);
    }
}

 

2-3. 컨트롤러 (Controller) 단

2-3-1) Pageable

  • @PageableDefault 어노테이션 사용
    • size : 페이지 당 표시할 데이터 개수 (기본값 : 10)
    • page : 현재 페이지 (기본값 : 0)
    • sort : 정렬 기준
    • direction : 정렬 방향 (오름차순, 내림차순)
@GetMapping("")
public Page<User> findAll(@PageableDefault(size = 5, page = 1, 
					sort = "name", direction = Sort.Direction.DESC) {
                    
	return userService.findAll(pageable);
}

 

2-3-2) PageRequest (Pageable의 구현체)

  • PageRequest 객체를 생성해서 직접 페이징 설정
  • 직접 쿼리 파라미터를 받는 작업 해야 함
  • PageRequest.of(pageNumber, pageSize, sort)
    • pageNumber : 현재 페이지
    • pageSize : 페이지 당 표시할 데이터 개수
    • sort : 정렬 기준
@GetMapping("")
public Page<User> findAll(@RequestParam(required = false, defaultValue = "1") int page) {

	PageRequest pageRequest = PageRequest.of(page - 1, 5, Sort.by("name").descending());
    return userService.findAll(pageRequest);
}

 

2-4. 페이지 내부 정보

  • Page 는 페이지에 대한 정보도 가지고 있음
  • 현재 페이지
    • result.getNumber()
  • 전체 페이지 수
    • result.getTotalPages()
  • 페이지 당 출력되는 데이터 개수
    • result.getSize()
  • 이전 / 다음 페이지 존재 여부
    • result.hasPrevious() / result.hasNext()
  • 현재 페이지가 첫 페이지 / 마지막 페이지 인지 여부
    • result.isFirst() / result.isLast()

 

3. 페이징, 정렬, 검색, 에러 메시지 포함 예제

  • (스프링 부트, gradle, MySQL 사용 예제)
  • Player 엔티티에 축구 선수의 이름, 소속팀, 연봉 데이터가 저장됨
  • 유명 축구선수 15명의 정보 입력
  • 이름 / 연봉(사이값) / 이름 + 연봉(사이값) 으로 검색 가능
  • 첫 화면에서는 엔티티에 존재하는 모든 선수들의 정보 출력 => 검색 시 검색 결과에 적합한 선수들의 정보만 출력
  • 연봉 사이값 입력이 잘못된 경우 ( '100억~900억'의 입력이 아닌 '900억~100억' 등의 입력 ) 에러 메시지 출력

 

3-0. build.gradle, application.yml

  • build.gradle
plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.1'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'study'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '21'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

  • application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/pagingDB?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
    username: [username]
    password: [password]
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true
    database-platform: org.hibernate.dialect.MySQL8Dialect
    database: mysql

  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html

 

3-1. Player (domain)

@Entity
@Getter @Setter
public class Player {

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

    private String name;
    private String team;

    private Integer salary;

}

 

3-2. SearchForm (domain)

  • 검색 폼을 위한 클래스
@Getter @Setter
public class SearchForm {

    private String name;		// 검색할 선수 이름
    private Integer startSal;	// 검색할 연봉 시작점
    private Integer endSal;		// 검색할 연봉 끝점
}

 

3-3. PlayerRepository (repository)

public interface PlayerRepository extends JpaRepository<Player, Long> {

    // findPlayersBySalaryBetween : 기존의 findAll 과 동일하다고 생각 가능!
    Page<Player> findPlayersBySalaryBetween(Integer startSal, Integer endSal, Pageable pageable);

	// findPlayersByNameContainingAndSalaryBetween : 기존의 findByNameContaining 과 동일하다고 생각 가능!
    Page<Player> findPlayersByNameContainingAndSalaryBetween(String name, Integer startSal, Integer endSal, Pageable pageable);
}
  • JpaRepository
    • find~By~Between(범위 시작점, 범위 끝점) : 범위의 시작 ~ 끝을 만족하는 데이터 반환
    • find~ByNameContaining(String name) : name을 "포함" 하는 데이터 반환
  • 컨트롤러에서 연봉의 시작, 끝이 입력되지 않으면 다음과 같이 설정함
    • 시작 : 0
    • 끝 : 99999
    • 따라서 연봉의 시작, 끝의 변수는 항상 값이 존재하게 됨
  • 따라서 1) 아무것도 입력 받지 않은 경우 2) 이름만 입력 받은 경우 3) 연봉만 입력받은 경우 4) 모두 입력받은 경우가 아닌
  • 1) 연봉만 값이 있는 경우 2) 이름과 연봉 모두 값이 있는 경우로 나눌 수 있음
    • 1) 연봉만 값이 있는 경우 : 기존의 findAll 과 동일하게 생각
    • 2) 이름과 연봉 모두 값이 있는 경우 : findByNameContaining 과 동일하게 생각 

 

3-4. PlayerService (service)

@Service
@RequiredArgsConstructor
public class PlayerService {

    private final PlayerRepository playerRepository;

    // findAll : 모든 선수 찾기
    public Page<Player> findAll(Integer startSal, Integer endSal, Pageable pageable){
        return playerRepository.findPlayersBySalaryBetween(startSal, endSal, pageable);
    }

    // findPlayersByName : 이름으로 선수 찾기
    public Page<Player> findPlayersByName(String name, Integer startSal, Integer endSal, Pageable pageable) {
        return playerRepository.findPlayersByNameContainingAndSalaryBetween(name, startSal, endSal, pageable);
    }
}

 

3-5. PlayerController (controller)

@Controller
@RequiredArgsConstructor
public class PlayerController {

    private final PlayerService playerService;


    @GetMapping("")
    public String home(@RequestParam(required = false, defaultValue = "1") int page,
                        @ModelAttribute SearchForm searchForm,
                        Model model){

		// 연봉 시작 조건이 빈 경우 : 0으로 설정
        if(searchForm.getStartSal() == null) searchForm.setStartSal(0);
        // 연봉 끝 조건이 빈 경우 : 99999로 설정
        if(searchForm.getEndSal() == null) searchForm.setEndSal(99999);
        
        // 연봉 시작 > 연봉 끝 (검색 조건 위반)
        if(searchForm.getStartSal() > searchForm.getEndSal()){
            model.addAttribute("errorMessage", "검색할 수 없는 범위 입니다.");
            model.addAttribute("nextUrl", "/");
            return "/error";
        }

        // PageRequest.of(페이지 번호, 페이지 당 5개씩 출력, 연봉 기준 내림차순)
        PageRequest pageRequest = PageRequest.of(page-1, 5, Sort.by("salary").descending());

        Page<Player> players;
        if(searchForm.getName() == null){
        	// 이름이 빈 경우 : findAll 호출
            players = playerService.findAll(searchForm.getStartSal(), searchForm.getEndSal(), pageRequest);
        } else {
        	// 이름이 있는 경우 : findPlayersByName 호출
            players = playerService.findPlayersByName(searchForm.getName(), searchForm.getStartSal(), searchForm.getEndSal(), pageRequest);
        }

        model.addAttribute("players", players);
        model.addAttribute("searchForm", searchForm);

        return "/home";
    }

}
  • PlayerRepository 에서 언급한 대로 연봉 시작, 끝 변수가 항상 값이 있게끔 설정함으로써 검색 경우를 반으로 줄임
  • 페이지 당 5명의 선수 출력
  • 연봉을 기준으로 내림차순 정렬

 

3-6. home.html (templates)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>페이징 연습</title>
</head>

<body>
<h2>선수를 검색합니다.</h2>
<hr/>
    <form th:object="${searchForm}" th:method="get" action="">
        <div>
            <input type="text" th:field="*{name}" th:value="*{name}" placeholder="이름" style="margin-right: 20px">
            <input type="number" th:field="*{startSal}" th:value="*{startSal}" placeholder="연봉 시작값 (단위 : 억)" style="margin-right: 20px">
            <input type="number" th:field="*{endSal}" th:value="*{endSal}" placeholder="연봉 끝값 (단위 : 억)" style="margin-right: 20px">
            <button type="submit">검색</button>
        </div>
    </form>
<br><br><br>
<h2>검색한 조건은 다음과 같습니다. (정렬 : 연봉 내림차순)</h2>
<hr/>
<h3>선수 이름 : '[[${searchForm.name}]]' 을 포함하는 이름</h3>
<h3>선수 연봉 : [[${searchForm.startSal}]] 억과 [[${searchForm.endSal}]] 억 사이</h3>
<br>
<table style="text-align: center">
    <thead>
    <tr>
        <th>선수 이름</th>
        <th>소속 팀</th>
        <th>선수 연봉 (단위 : 억)</th>
    </tr>
    </thead>
    <tbody>
    <tr th:each="player : ${players}">
        <td th:text="${player.name}"></td>
        <td th:text="${player.team}"></td>
        <td th:text="${player.salary}"></td>
    </tr>
    </tbody>
</table>
<br><br>
<button th:disabled="${!players.hasPrevious()}"
        th:onclick="|location.href='@{/(page=${players.getNumber()}, name=${searchForm.getName()}, startSal=${searchForm.getStartSal()}, endSal=${searchForm.getEndSal()} )}'|">
    이전</button>
<span>[[${players.getNumber() + 1}]] / [[${players.getTotalPages()}]] 페이지</span>
<button th:disabled="${!players.hasNext()}"
        th:onclick="|location.href='@{/(page=${players.getNumber() + 2}, name=${searchForm.getName()}, startSal=${searchForm.getStartSal()}, endSal=${searchForm.getEndSal()} )}'|">
    다음</button>
<br><br>

</body>
</html>
  • th:disabled 태그 : 조건이 만족하면 (버튼) 클릭 비활성화
  • 페이지 시작을 1로 설정했기 때문에 다음과 같이 설정
    • 이전 페이지 : players.getNumber()
    • 현재 페이지 : players.getNumber() + 1
    • 다음 페이지 : players.getNumber() + 2

 

3-7. error.html (templates)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>페이징 연습</title>
</head>

<body>
    <script th:inline="javascript">
        window.onload = function () {
            alert([[${errorMessage}]])
            window.location.href = [[${nextUrl}]]
        }
    </script>
</body>
</html>
  • 자바 스크립트 문법 사용
    • 컨트롤러에서 Model에 추가한 속성인 errorMessage 출력
    • 컨트롤러에서 Model에 추가한 속성이 nextUrl로 이동하게 설정

 

4. 예제 결과 확인

4-1. 첫 화면

첫 출력 화면

 

4-1-1) 페이징 기능 확인 (한 페이지 당 5개의 데이터 출력, 연봉 기준 내림차순 정렬, 이전/다음 버튼)

 

4-2. 검색 기능 확인

4-2-1) 이름만 검색

'민'을 포함한 선수 이름 검색
연봉 시작, 끝을 입력하지 않아도 자동 초기화 됨

 

4-2-2) 연봉만 검색

연봉이 100 ~ 500억인 선수 검색
연봉 검색 결과

 

4-2-3) 이름과 연봉 모두 검색

이름이 '벨링엄'을 포함하고, 연봉이 100 ~ 500억인 선수 검색
이름 + 연봉 검색 결과

 

4-3. 에러 메시지 확인

연봉 입력 오류
오류 메시지 출력 결과

 

저작자표시 변경금지 (새창열림)

'Spring Boot' 카테고리의 다른 글

[Spring Boot] 로그인 기능 구현 (1) - 쿠키 로그인  (0) 2024.01.07
[Spring Boot] 로그인 기능 구현 (0) - 공통 기능, 코드 구현  (2) 2024.01.07
[SpringBoot] MySQL 연동 (maven, gradle)  (0) 2023.12.12
[Spring Boot] 스프링 구동 시에 특정 코드 자동 실행시키기 (Command Line Runner, Application Runner)  (1) 2023.11.22
[Spring Boot] Gradle 빌드해서 Jar 파일 생성, 실행  (0) 2023.11.22
  1. 1. 페이징 (paging) 기능이란 ?
  2. 2. 페이징 구현 방법 (Spring Boot)
  3. 2-1. 레포지토리 (Repository) 단
  4. 2-2. 서비스 (Service) 단
  5. 2-3. 컨트롤러 (Controller) 단
  6. 2-4. 페이지 내부 정보
  7. 3. 페이징, 정렬, 검색, 에러 메시지 포함 예제
  8. 3-0. build.gradle, application.yml
  9. 3-1. Player (domain)
  10. 3-2. SearchForm (domain)
  11. 3-3. PlayerRepository (repository)
  12. 3-4. PlayerService (service)
  13. 3-5. PlayerController (controller)
  14. 3-6. home.html (templates)
  15. 3-7. error.html (templates)
  16. 4. 예제 결과 확인
  17. 4-1. 첫 화면
  18. 4-2. 검색 기능 확인
  19. 4-3. 에러 메시지 확인
'Spring Boot' 카테고리의 다른 글
  • [Spring Boot] 로그인 기능 구현 (1) - 쿠키 로그인
  • [Spring Boot] 로그인 기능 구현 (0) - 공통 기능, 코드 구현
  • [SpringBoot] MySQL 연동 (maven, gradle)
  • [Spring Boot] 스프링 구동 시에 특정 코드 자동 실행시키기 (Command Line Runner, Application Runner)
공대생안씨
공대생안씨
전자공학과 학부생의 코딩 일기
티스토리
|
로그인
공대생안씨
공대생의 코딩 일기
공대생안씨
글쓰기
|
관리
전체
오늘
어제
  • All Categories (153)
    • Spring Boot (46)
      • JPA (7)
      • Lombok (2)
    • Java (21)
    • DevOps (3)
      • CI,CD (8)
      • Monitoring (2)
    • Database (7)
      • MySQL (5)
      • MongoDB (1)
      • H2 (1)
    • Trouble Shooting (5)
    • FE (4)
    • IntelliJ (3)
    • Git (3)
    • Algorithm (41)

블로그 메뉴

  • 홈
  • 태그
  • Github

공지사항

인기 글

hELLO · Designed By 정상우.v4.2.2
공대생안씨
[Spring Boot] 페이징 기능 구현 ( + 페이징, 정렬, 검색, 에러 메시지 포함 예제)
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.