Recording/멋쟁이사자처럼 BE 13기

[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_44일차_"페이징 처리"

LEFT 2025. 2. 10. 17:28

🦁멋쟁이사자처럼 백엔드 부트캠프 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() 사용 등)
- 레이어드 아키텍처의 구조 공부