🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [44]일차
🚀44일차에는 실습했던 기존의 친구목록 페이지를 리팩토링하여 페이지를 도입하고 페이징 처리를 통해 친구목록이 페이지마다 특정 개수로 출력될 수 있도록 구현할 수 있었다.
학습 목표 : 페이징 처리에 익숙해질 수 있도록 관련 용어 및 활용예시에 대해 공부
학습 과정 : 회고를 통해 작성
▶️실습 - 페이지 추가
service/FriendService 추가
@Transactional(readOnly = true)
public Page<Friend> findAllFriend(Pageable pageable){
Pageable pageable2 = PageRequest.of(pageable.getPageNumber(),
pageable.getPageSize(),
Sort.by(Sort.Direction.DESC, "id"));
// pageable2로 정보를 얻은 후 friendRepository의 메소드 findAll에 전달
return friendRepository.findAll(pageable2);
}
- PageRequest.of(pageable.getPageNumber(), pageable.getPageSize())
➡️getPageNumber() : 현재 원하는 페이지 번호가 무엇인지 얻어옴
➡️getPageSize() : 한 페이지에 몇개씩 가져올지 controller를 통해서 가져옴
➡️Sort.Direction.DESC : 정렬 기준, ex. id를 기준으로 내림차순 정렬 내림차순 정렬 = Sort.Direction.DESC
service/FriendService 기존 코드
@Transactional(readOnly = true)
public Iterable<Friend> findAllFriend(){
return friendRepository.findAll();
}
- 매개변수로 아무것도 받지 않는 findAllFriend()와 함께
service/FriendService - findAllFriend(Pageable pageable) 메소드 추가
- 현재 원하는 페이지 번호가 무엇인지 얻어오는 것
@Transactional(readOnly = true)
public Page<Friend> findAllFriend(Pageable pageable){
Pageable pageable2 = PageRequest.of(pageable.getPageNumber(),
pageable.getPageSize(),
Sort.by(Sort.Direction.DESC, "id"));
// pageable2로 정보를 얻은 후 friendRepository의 메소드 findAll에 전달
return friendRepository.findAll(pageable2);
}
- @Transactional(readnOnly=true)
➡️조회할때는 읽기만 한다는 읽기전용을 명시해주어야 트랜잭션 수행 시 목적을 명확히 할 수 있음 - findAllFriend(Pageable pageable)
➡️pageable을 매개변수로 받는 findAllFriend()로 오버라이딩한 것
Pageable : Page를 추상화해놓은 객체 - PageRequest.of : Pageable 객체를 생성
➡️페이지에 대한 정보를 가져옴
Service로부터 정보를 가져와 모델에 넣어주며, 현재 페이지 또한 넘겨주게됨
controller/FriendController 기존 코드
// 수정 전 코드
@GetMapping("/list")
public String list(Model model){
// 해야할 일
model.addAttribute("friends", friendService.findAllFriend());
return "friends/list";
}
- list를 보여주는 부분을 페이지로 보여줄 수 있도록 수정해야함
- ➡️해결방법 : Pageable 객체 활용
controller/FriendController 수정 코드
// 수정 : 페이지로 리스트를 보여주도록 처리
@GetMapping("/list")
public String list(Model model, @RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", required = false, defaultValue = "5") int size)){
Pageable pageable = PageRequest.of(page-1, size);
// 해야할 일
model.addAttribute("friends", friendService.findAllFriend(pageable));
model.addAttribute("currentPage", page);
return "friends/list";
}
- @RequestParam(name = "page", defaultValue = "1") int page
➡️name, required, defaultValue 정보를 page 변수에 담기
클라이언트가 보낸 page 값을 받아옴
➡️페이지가 지정되지 않으면 기본값으로 1페이지를 사용 - @RequestParam(name = "size", required = false, defaultValue = "5") int size
➡️다른 name, required, defaultValue 정보를 size 변수에 담기
페이지번호는 0부터 존재하는데, 일반적으론 1번페이지부터 시작하므로 page-1을 해주어서
실제 원하는 페이지를 매핑 (DB의 인덱스가 0부터 시작)
클라이언트가 보낸 size 값을 받아옴
required = false : 클라이언트가 값을 보내지 않아도 동작하도록 설정
➡️기본적으로 5개의 데이터를 가져오도록 설정 - findAllFriend(pageable)
➡️pageable 객체를 인자로 받을 수 있을 것, 기존 매개변수를 받지 않는 findAllFriend가 아닌
pageable객체를 넘길 수 있도록 findAllFriend(Pageable pageable) 메소드에 전달
"페이징된 친구목록을 조회"
➡️결과를 "friends" 이름으로 모델에 추가 후 뷰에 전달 - model.addAttribute("currentPage", page);
➡️현재 사용자가 보고 있는 페이지가 몇 페이지인지 출력할 수 있도록 페이지 정보를 모델에 저장
- 페이지의 사이즈 (=가져올 데이터 개수)는 defaultValue(=기본값)으로 5 지정 후 size 변수에 넣었으므로
친구 데이터 5개가 출력 - Sort.Direction.DESC로 내림차순을 명시해주었기때문에
아이디가 숫자가 큰 친구부터 아이디가 숫자가 작은 친구까지의 순으로 출력이 되고 있음
👀Page 인터페이스
public interface Page<T> extends Slice<T> {
static <T> Page<T> empty() {
return empty(Pageable.unpaged());
}
static <T> Page<T> empty(Pageable pageable) {
return new PageImpl(Collections.emptyList(), pageable, 0L);
}
int getTotalPages();
long getTotalElements();
<U> Page<U> map(Function<? super T, ? extends U> converter);
}
- getTotalPages() 메소드를 가지고 있음
페이지를 나눌때 한 페이지에 데이터를 몇개씩 보여줄 것인지를 알고
전체 데이터가 몇개인지 알려줌 (=getTotalPages()가 자동으로 알아서 해줌)
ex. 총 데이터가 14개, 한페이지에 5개(pagesize = 5)
➡️총 페이지 수는 3인 것을 getTotalPages()가 미리 계산하는 것
❓페이지에서 page-1로 지정한 이유
- JPA의 PageRequest.of()는 0부터 시작하는 인덱스를 사용
- 반면, 사용자는 1페이지부터 시작한다고 인식
- 따라서 사용자가 page=1을 요청하면, 실제로는 PageRequest.of(0, size)를 생성해야 올바른 데이터를 가져옴
- 즉, 사용자의 직관적인 페이지 번호와 JPA의 0-based 인덱스 시스템을 맞추기 위해 page - 1을 수행함
사용자가 요청하는 페이지와 JPA에서 실제 가져오는 페이지를 비교
1페이지 (page=1) | PageRequest.of(0, 5) | 0번 페이지 |
2페이지 (page=2) | PageRequest.of(1, 5) | 1번 페이지 |
3페이지 (page=3) | PageRequest.of(2, 5) | 2번 페이지 |
정리하자면
사용자는 1부터 시작하는 페이지를 요청하지만, JPA의 PageRequest는 0부터 시작하므로 page-1을 해야함
list.html 수정
- 페이지 번호가 친구목록의 밑에 출력이 될 수 있도록 구현
<div th:if="${friends.totalPages > 1}">
<ul>
<li th:each="i:${#numbers.sequence(1, friends.totalPages)}">
<a th:href="@{/friends/list(page=${i})}" th:text="${i}"></a>
</li>
</ul>
</div>
- if문을 사용하여 totalPages가 둘 이상일때만 (=하나 초과일때만) <div>를 만들어내도록 할 것
➡️만약 페이지가 1개면 페이지 목록 번호를 만들어낼 필요가 없기때문 - 2페이지부터는 <div>로 페이지 번호를 만들어냄
- th:each="i:${#numbers.sequence(1, friends.totalPages)}
➡️numbers.sequence 객체(thymeleaf가 제공하는 문법)가 데이터 사이즈만큼 하나씩 (sequence) 증가하면서
1페이지부터 friends.totalPages페이지까지 반복문을 돌면서 페이지 안 정보를 가져옴
여기서의 i는 <a>태그의 i에 값이 전달됨 - <a th:href="@{/friends/list(page=${i})}" th:text="${i}"></a>
➡️page의 번호는 ${i}로 sequence에서 만든 i를 가져옴
이 페이지 숫자를 화면에 보여주기 위해서 text를 지정(${i})
- 3페이지에 커서를 올렸을때 (localhost:8080/friends/list?page=3) 를 확인
➡️페이지 쿼리가 제대로 전달되고 있는지 확인
<table th:if="${!friends.empty}">
<!--테이블 정보들...-->
</table>
- !friends.empty
➡️친구 목록이 없다면 테이블 또한 만들지 않도록 구현
🚀게시판 프로젝트
🚀학습 목표 : 친구 목록 페이지를 만들면서 프로그램의 흐름을 따라가면서 이해할 수 있었다.
혼자 새로운 프로젝트를 개발해낼 수 있을까 생각도 했지만 비슷한 구조로 생각하며, @GetMapping, @PostMapping 등을 활용하여 페이지에서의 요청이 발생했을 경우 매핑시켜주는 과정을 천천히 이해해보았다.
지금까지 배운 내용을 토대로 기능이 원활히 작동할 수 있도록 하나하나 뷰를 구현하는 것이 목표였다.
1. 게시글 목록 구현
2. 게시글 상세 페이지 구현
3. 게시글 등록 폼 구현
4. 게시글 삭제 폼 구현
5. 게시글 수정 폼 구현
6. 이 외 커밋
🚀회고 결과 :
이번 회고에서는 게시판 프로젝트를 직접 만들 수 있었다.
기존의 친구 목록 페이지를 참고하여 게시판 프로젝트의 구조 설계에 적용할 수 있었다.
시간이 오래걸렸지만 뷰를 하나하나 구현해나가는 과정에서 시행착오가 많았다.
- <p>태그와 <span>태그 사용으로 입력 필드 구분하기
- Content 입력필드는 textarea를 사용하는 것
- 비밀번호 검증 로직을 추가하기 위한 방법
- createdAt을 구현하기 위해 "게시글 수정"했을때의 시간으로 적용되도록 구현
- createdAt은 LocalDateTime으로 담아놓고 리스트를 보여주는 페이지에서는 날짜만 출력하도록 format 적용
- 상세페이지에서는 날짜-시간 까지 출력할 수 있도록 format 적용
- 비밀번호는 type="password"로 입력되는 값들이 보이지 않도록 구현
- 기존 친구목록 삭제에서는 데이터가 바로 삭제되었다면 게시판의 삭제는 비밀번호로 유효성 검증을 한 후 삭제하도록 구현
느낀 점 :
확실히 요구사항을 보고 바로 구조와 구현해야할 메소드가 떠오르기까지는 시간이 걸리는 것 같다.
또한 Controller - Service - Repository로 연결되어 메소드가 호출되는 과정이 여전히 익숙치 않았다.
향후 계획 :
- 스프링 프레임워크의 페이징 처리 기술에 대해서 추가 공부 진행
- 타임리프의 유용한 기능들을 추가로 알아보기 (이번 프로젝트의 개발에서 #temporals와 format() 사용 등)
- 레이어드 아키텍처의 구조 공부
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_47일차_"JPA 엔티티 매핑" (0) | 2025.02.13 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_46일차_"JPA" (0) | 2025.02.12 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_43일차_"친구목록 페이지" (1) | 2025.02.07 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_42일차_"SQL 기반 페이징 기법" (0) | 2025.02.06 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_42일차_"Spring Data JDBC" (1) | 2025.02.06 |