Spring/Spring Boot
[Spring Boot] 게시판(Read), pagination 구현
Jenny_yoon
2023. 2. 23. 09:37
728x90
반응형
1. html 파일 생성
-
'처음', '다음', '►►'기능은 html파일에서 제외함
<html layout:decorate="~{layout}">
<div layout:fragment="content">
<table class="table">
<tr>
<td>no</td>
<td>title</td>
<td>date</td>
<td>name</td>
<td>like</td>
<td>read</td>
</tr>
<tr th:each="row : ${list}">
//${list}: controller에서 mv에 list의 key/value 넣어줌
<td th:text="${row.bno}"></td>
<td>
<a th:href="@{/detail(bno=${row.bno})}" th:text="${row.b_title}"></a>
</td>
<td th:text="${#dates.format(row.b_date, 'yyyy-MM-dd HH:mm:ss')}"></td>
<td th:text="${row.member.member_name}"></td> <!-- Member.java에 member클래스 안에 member_name이 있음 -->
<td th:text="${row.b_like}"></td>
<td th:text="${row.b_read}"></td>
</tr>
</table>
<div th:if="${!list.isEmpty()}"> <!-- list가 비어있지 않다면 -->
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${!list.hasPrevious} ? 'disabled'"><!-- disabled: 부트스트랩 들어가면 먹는 기능 -->
<a class="page-link" th:href="@{|?pageNo=${list.number}|}">이전</a>
</li>
<!--1부터 list.totalPages까지 숫자 찍기-->
<li class="page-item" th:each="page : ${#numbers.sequence(1, list.totalPages)}"
th:if="${page >= list.number and page <= list.number+10}"
th:classappend="${page == list.number} ? 'active'"><!-- active: 부트스트랩 들어가면 먹는 기능 -->
<a class="page-link" th:text="${page}" th:href="@{|?pageNo=${page}|}"></a><!-- ${page}: each의 변수명 -->
</li>
<li class="page-item" th:classappend="${!list.hasNext} ? 'disabled'">
<a class="page-link" th:href="@{|?pageNo=${list.number+2}|}">다음</a>
</li>
</ul>
</div>
<button class="btn btn-primary" onclick="location.href='/write'">글쓰기</button>
</div>
</html>
TIP !
* #numbers.sequence()
- Thymeleaf에서 숫자의 시퀀스를 만드는 유틸리티 메서드.
- 지정된 시작 숫자와 끝 숫자 사이의 일련의 숫자를 생성
- 위의 코드에서 1이 시작 숫자이고, list.totalPages가 끝숫자
* ${page == list.number} ? 'active'
- page와 list.number가 같은 경우 active클래스 추가
- active: 그냥 클래스명. 일반적으로 활성화된 상태를 나타냄(현재 보고 있는 메뉴 항목이나 현재 활성화된 탭 등을 표시할때)
- active라는 이름은 많은 개발자들이 흔히 사용하는 클래스명 중 하나
* th:each="page : ${#numbers.sequence(1, list.totalPages)}"와 th:each=page : ${list.totalPages}"의 차이점?
- 전자 : #numbers 객체에서 제공되는 sequence 함수를 사용하여 1부터 list.totalPages 까지의 범위를 생성.
각 숫자를 변page 에 할당하여 반복하면서 <li> 요소를 생성
- 후자: list.totalPages은 하나의 숫자이기 때문에 그 값을 반복. (하나의 숫자인경우 each가 필요없다)
* #numbers
- Thymeleaf에서 제공하는 유틸리티 객체
- 숫자와 관련된 여러 유용한 기능을 수행
- 예시. #numbers.sequence(from, to)는 주어진 범위의 정수 시퀀스 반환
2. Controller 생성
@RequiredArgsConstructor
@Controller
public class IndexController {
//controller -> service -> repository -> DB
@Autowired
private final IndexService indexService;
@GetMapping("/board")
public ModelAndView board(@RequestParam(value="pageNo", defaultValue="1") int pageNo) {
ModelAndView mv = new ModelAndView("board"); //board.html
//페이징 추가하기
Page<Board> list = this.indexService.boardList(pageNo);
//boardList 메서드에 pageNo라는 파라미터를 전달
//boardList 메서드는 페이지네이션을 적용한 게시글 리스트를 반환
//pageNo는 조회할 페이지 번호를 나타냄
//즉,pageNo에 해당하는 페이지의 게시글 리스트를 조회하여 list 변수에 할당함
mv.addObject("list",list);
return mv;
}
TIP !
* (@RequestParam(value="pageNo", defaultValue="1") int pageNo)
- int pageNo: html파일에 <form>태그를 통해 파라미터로 전달.
- 하지만 html파일엔 <form>이 없어 pageNo값은 없다. 값이 없는경우, defaultValue="1"를 통해 기본값이 1로 설정된다.
- 즉, 페이징(숫자) 시작이 1이다.
* Page
- JPA에서 제공하는 인터페이스
- 페이징 처리된 데이터를 다루기 위한 기능을 제공
- (Page인터페이스는) Iterable 인터페이스를 상속받아 페이징 처리된 데이터를 순회할 수 있음
- 다양한 메서드를 제공. 대표적으로 getTotalPages(), getContent(), getNumber() 등
- Pageble을 파라미터로하여 가져온 결과물은 Page<SomeObject> 형태로 반환 됨
- 대부분 다수의 row를 가져오기 때문에 Page<List<SomeObject>>의 형태로 반환
* Page <Board>
- 여기서 <Board>는 조회한 게시글 정보를 담고 있는 엔티티 클래스. 게시글의 제목, 내용, 작성일자 등 정보를 필드로 갖고 있음
- Page<Board>는 한 페이지에 보여줄 게시글들을 담고 있는 객체. 즉, Page<Board> 타입의 list 변수에는 페이징 처리된 게시물 리스트가 저장됨
[Spring Data Jpa의 제공 메서드]
* Pageable
-페이징 정보를 담고 있는 인터페이스 (페이징 처리위해 사용됨)
- Spring JPA에서 DB 쿼리에 쉽고 유연하게 limit 쿼리를 사용할 수 있게 해줌
- JPA를 사용할 때, 자동으로 Pageable 타입의 변수를 넘겨주면 JPA가 DB에 접근해 데이터를 가져올 때 자동으로 limit 조건을 붙여 데이터를 가져옴
- PageRequest 클래스를 이용하여 구현됨
* @PageableDefault
- Pageable 객체의 디폴트 값을 설정
(Controller의 Pageable 객체를 파라미터로 받는 메서드에서 해당 어노테이션을 사용하면,
API 요청 시 Pageable 객체에 대한 파라미터를 넘겨주지 않아도 자동으로 기본값을 가진 Pageable 타입 파라미터를 제공)
- 기본적인 페이지 크기나 정렬 순서 등의 값을 설정 가능
* getTotalElements()
- 쿼리 결과물의 전체 데이터 갯수 반환(전체 요소 갯수)
- Pageable에 의해 limit키워드가 조건으로 들어가지 않는 쿼리 결과의 수 인데, 주의해야 할 점은 쿼리 결과의 갯수만 가져오지 전체 데이터를 가져오지 않는다는 점이다.
- 게시판 기능 사용자에게 전체 데이터 개수를 알려주는 등에 사용하기 좋음
* getTotalPages()
- 전체 페이지의 개수를 반환
- 쿼리를 통해 가져온 요소들을 size크기에 맞춰 페이징하였을 때 나오는 총 페이지의 갯수이다.
- 이를 활용해 쉽게 페이지 버튼의 생성이 가능
* getSize()
- 페이지 크기를 반환
- 쿼리를 수행한 전체 데이터에 대해 일정 수 만큼 나눠 페이지를 구성하는데, 이 일정 수의 크기
* getNumber()
- 현재 페이지 번호(0부터 시작)를 반환
* getNumberOfElements()
- 현재 페이지에서의 요소 개수를 반환
- 최대 size의 수 만큼 나올 수 있다.
3. Service 생성
방법1.
@Service
@RequiredArgsConstructor //생성자기반
public class IndexService {
//IndexService: 데이터베이스와의 상호작용을 위한 메서드를 포함하고,
//해당 메서드를 사용하여 (indexRepository를 통해)게시물 데이터를 가져오는 기능을 수행
private final IndexRepository indexRepository;
public Page<Board> boardList(int pageNo) {
List<Sort.Order> sort = new ArrayList<Sort.Order>();
//Sort 객체는 JPA에서 데이터를 조회할 때, 정렬을 위해 사용됩니다. Order는 정렬 순서를 정하는 클래스
sort.add(Sort.Order.desc("bno")); //JPA는 _를 인식못함 //이 컬럼기준 역순정렬
//1부터 시작하기 위해
Pageable pageable = PageRequest.of(pageNo -1, 10,Sort.by(sort));
//pageNo:controller에서 defaultValue="1"로 설정해놔서 값은1. pageble은 0부터 셀수있도록 pageNo -1로 설정 //10개씩 출력
return this.indexRepository.findAll(pageable);
//select기능 //pageble을 repository로 보내기
//findAll(): 데이터베이스에 저장된 모든 게시물을 가져온 후, 이를 List<BoardDTO> 형태로 반환
//모든 엔티티 레코드를 조회하기 위한 메서드. 리턴 타입은 해당 엔티티의 리스트
}
방법2.
@Service
@RequiredArgsConstructor
public class IndexService {
private final IndexRepository indexRepository;
public Page<Board> boardList(int pageNo) {
Pageable pageable = PageRequest.of(pageNo -1, 10,Sort.by(Sort.Order.desc("bno")));
return this.indexRepository.findAll(pageable);
}
}
TIP !
* @RequiredArgsConstructor와 @Autowired의 차이점
: 둘다 Spring Framework에서 제공하는 의존성 주입(Dependency Injection) 관련 어노테이션
[@RequiredArgsConstructor]
- 생성자를 자동으로 생성해주는 Lombok 어노테이션
- 이 어노테이션을 붙인 클래스의 생성자는 해당 클래스 내부의 final로 선언된 필드들만을 매개변수로 받음.
- 이 때 생성자를 직접 작성하는 것보다 더 간결하게 코드를 작성할 수 있음
[@Autowired]
- 스프링 컨테이너가 관리하는 Bean들 중에서 해당 타입의 Bean을 자동으로 주입해주는 어노테이션
- 이 어노테이션을 붙인 필드나 생성자의 매개변수에 해당하는 Bean이 존재하지 않으면 에러가 발생함
즉, @RequiredArgsConstructor는 생성자 자동 생성을 위한 어노테이션. @Autowired는 의존성 주입을 위한 어노테이션이다.
* List<Sort.Order> sort = new ArrayList<Sort.Order>();
- Sort.Order: 정렬 조건을 List에 담기 위한 객체
- Sort : 정렬 조건을 나타내는 클래스. JPA에서 제공하는 findAll 메서드 등을 호출할 때 데이터를 정렬할 수 있음
* PageRequest.of(pageNo -1, 10,Sort.by(sort));
: pageNo 번째 페이지에서 10개의 게시물을 출력하며, sort 리스트에 지정된 속성을 기준으로 정렬
- Pageable을 구현한 객체를 생성하는 역할
- 첫번째 인자: 조회할 페이지 번호(pageNo)에서 1을 빼주기
- 두번째 인자: 한 페이지당 출력할 게시물의 개수
- 마지막 인자: 정렬 방식을 지정하기 위한 Sort 객체
* Sort.by(속성)
- 주어진 속성을 기준으로 정렬하는 Sort 객체를 생성
4. Repository 생성
public interface IndexRepository extends JpaRepository<Board,Integer> {
//JpaRepository<사용될entity클래스,ID값>
//ID는 repository에서 @ID로 설정
Page<Board> findAll(Pageable pageable); //Pageable : service에서 가져옴
//Board:(@Entity있는 dto역할)
//findTop5() : 5개만 조회
}
TIP !
- JpaRepository상속하기만 해도되기때문에 @Repository 선언 안해도됨
* JpaRepository
- 인터페이스는 CRUD(create, read, update, delete) 작업을 수행할 수 있게함
- PagingAndSortingRepository 인터페이스와 CrudRepository 인터페이스를 상속받고 있음
- findAll(), findById(), save(), delete() 등의 메서드를 자동으로 지원
* findAll()
- 모든 엔티티 레코드를 조회하기 위한 메서드
- return 타입: 해당 엔티티의 리스트
5. Board Entity 생성
(쿼리만들어주는 역할)
@Getter
@Setter
@Entity //@Entity는 JPA에서 Entity 클래스임을 나타내는 어노테이션
@Table(name="board")
public class Board {
@Id
@Column(name="b_no") //컬럼명
//JPA는 _를 인식못해서, 컬럼명의 _빼고 bno로 변수지정(b_no -> bno)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int bno; //b_no의 변수명
}
TIP !
* 클래스 Board
- Board는 table name
- 클래스명이 table name과 다른경우, @Table(name="board")로 맵핑
- board.class안에 int bno가 있는것
* @Id
- 해당 필드가 Primary Key임을 나타냄
- repository에서 JpaRepository<Board,Integer>의 Integer가 바로 ID를 넣는 란(@Id를 통해 불러와짐)
* @GeneratedValue()
- Id값 따로 설정안한경우, ID값(랩퍼클래스:Integer 등)을 자동으로 불러옴
- Primary Key의 값 생성 전략을 지정하는 어노테이션
* (strategy = GenerationType.IDENTITY)
- MySQL에서 자동 증가되는 값을 사용한다는 의미
참조
https://velog.io/@albaneo0724/Spring-Pagination%EA%B3%BC-Page-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Pageable
728x90
반응형