[코드로 배우는 스프링부트 웹 프로젝트] - 프로젝트 구조 만들기(4) - 목록 처리 기능

Jongwon·2022년 12월 28일
0

먼저 JPA에 전달해주고, 전달받는 페이지에 대한 정보를 담은 DTO를 생성하겠습니다.

PageRequestDTO

package org.zerock.guestbook.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

@Data
@Builder
@AllArgsConstructor
public class PageRequestDTO {

    private int page;
    private int size;

    public PageRequestDTO() {
        this.page = 1;
        this.size = 10;
    }

    public Pageable getPageable(Sort sort) {
         return PageRequest.of(page - 1, size, sort);
    }
}

PageRequestDTO는 화면에서 전달되는 페이지 번호와 크기를 JPA에게 알려주는 DTO입니다.
PageResultDTO

package org.zerock.guestbook.dto;

import lombok.Data;
import org.springframework.data.domain.Page;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

@Data
public class PageResultDTO<DTO, EN> {

    private List<DTO> dtoList;

    public PageResultDTO(Page<EN> result, Function<EN, DTO> fn) {
        dtoList = result.stream().map(fn).collect(Collectors.toList());
    }
}

PageResultDTO는 JPA의 결과로 나온 엔티티인 result를 받아서 DTO로 변환하고, 페이지에 필요한 정보들을 리스트로 구성해줍니다. 제네릭 타입을 DTO와 EN(Entity)로 정해두었는데, 이를 통해 JPA가 어떤 Page<T>값을 서비스 계층에 전달하더라도 이를 DTO로 변환할 수 있게끔 임의의 제네릭 타입을 지정한 방식입니다.

GuestbookService 인터페이스에 아래와 같이 entity->dto로 변환하는 코드를 작성합니다.

public interface GuestbookService {
    Long register(GuestbookDTO dto);
    
    //추가
    PageResultDTO<GuestbookDTO, Guestbook> getList(PageRequestDTO requestDTO);

    default Guestbook dtoToEntity(GuestbookDTO dto) {
        Guestbook entity = Guestbook.builder()
                .gno(dto.getGno())
                .title(dto.getTitle())
                .content(dto.getContent())
                .writer(dto.getWriter())
                .build();
        return entity;
    }

//추가
    default GuestbookDTO EntityToDto(Guestbook entity) {
        GuestbookDTO dto = GuestbookDTO.builder()
                .gno(entity.getGno())
                .title(entity.getTitle())
                .content(entity.getContent())
                .writer(entity.getWriter())
                .regDate(entity.getRegDate())
                .modDate(entity.getModDate())
                .build();

        return dto;
    }
}

그리고 GuestbookServiceImpl에 아래의 코드를 override합니다.

@Override
    public PageResultDTO<GuestbookDTO, Guestbook> getList(PageRequestDTO requestDTO) {
        Pageable pageable = requestDTO.getPageable(Sort.by("gno").descending());

        Page<Guestbook> result = repository.findAll(pageable);

        Function<Guestbook, GuestbookDTO> fn = (entity -> entityToDto(entity));

        return new PageResultDTO<>(result, fn);
    }

이제 DTO로 변환된 리스트가 정상적으로 출력되는지 확인해보겠습니다. 서비스 테스트 파일에 아래의 코드를 추가합니다.

@Test
    public void testList() {

        PageRequestDTO pageRequestDTO = PageRequestDTO.builder().page(1).size(10).build();

        PageResultDTO<GuestbookDTO, Guestbook> resultDTO = service.getList(pageRequestDTO);

        for(GuestbookDTO guestbookDTO : resultDTO.getDtoList()) {
            System.out.println(guestbookDTO);
        }
    }

10개의 DTO가 정상적으로 출력된 것을 확인할 수 있습니다.







다음으로 할 내용은 페이지 번호 매기기입니다.

네이버 검색화면에서 가장 하단 부분에는 위 사진과 같이 페이지 번호가 있고, 양쪽에 이전 페이지와 다음 페이지를 나타내는 꺽쇠기호가 있습니다. 이 기능을 PageResultDTO에 구현하겠습니다.

PageResultDTO

@Data
public class PageResultDTO<DTO, EN> {

    private List<DTO> dtoList;

    private int page;
    private int size;

    private int totalPage;
    private int start, end;

    private boolean prev, next;

    private List<Integer> pageList;


    public PageResultDTO(Page<EN> result, Function<EN, DTO> fn) {
        dtoList = result.stream().map(fn).collect(Collectors.toList());

        totalPage = result.getTotalPages();
        makePageList(result.getPageable());
    }

    private void makePageList(Pageable pageable) {
        this.page = pageable.getPageNumber() + 1;
        this.size = pageable.getPageSize();

        int tempEnd = (int)(Math.ceil(page/10.0))*10;
        start = tempEnd - 9;
        prev = start > 1;
        next = tempEnd < totalPage;
        end = tempEnd > totalPage ? totalPage : tempEnd;

        pageList = IntStream.rangeClosed(start, end).boxed().collect(Collectors.toList());
    }


}

pagesize는 각가 현재 페이지 번호와 페이지의 크기, totalPage는 총 페이지 수, startend는 해당 페이지의 시작과 끝번호(5페이지 같은 경우에는 1~10의 리스트를 나타내야 하므로 start=1, end=10), prevnext는 이전 페이지나 다음 페이지가 있는지를 나타내는 boolean값입니다. 마지막으로 pageList에는 페이지 번호 목록(1,2,3...10)이 Integer형으로 담깁니다.

정상적으로 동작하는지 확인하기 위해 테스트코드도 수정하겠습니다.

GuestbookServiceTests

@Test
    public void testList() {

        PageRequestDTO pageRequestDTO = PageRequestDTO.builder().page(1).size(10).build();

        PageResultDTO<GuestbookDTO, Guestbook> resultDTO = service.getList(pageRequestDTO);

        System.out.println(resultDTO.isPrev());
        System.out.println(resultDTO.isNext());
        System.out.println(resultDTO.getTotalPage());
        System.out.println("-------------------------");
        for(GuestbookDTO guestbookDTO : resultDTO.getDtoList()) {
            System.out.println(guestbookDTO);
        }
        System.out.println("-------------------------");
        resultDTO.getPageList().forEach(i -> System.out.println(i));

    }


이러한 결과가 나오면 성공입니다.


이제 서비스 레이어는 완성이 되었으므로 Controller 레벨로 넘어가겠습니다. 앞서 간단하게 만들었던 GuestbookController를 아래와 같이 수정합니다.

@Controller
@Log4j2
@RequestMapping("/guestbook")
@RequiredArgsConstructor    //GuestbookService를 DI하기 위해 추가
public class GuestbookController {

    private final GuestbookService service;

    @GetMapping("/")
    public String index() {
        return "redirect:/guestbook/list";
    }

    @GetMapping("/list")
    public void list(PageRequestDTO pageRequestDTO, Model model) {

        log.info("list..........." + pageRequestDTO);
        model.addAttribute("result", service.getList(pageRequestDTO));
    }
}

/list로 pageRequestDTO의 내용(page, size)과 일치하는 데이터를 보내면 스프링 MVC는 자동적으로 이를 pageRequestDTO에 담아줍니다. 이를 pageResultDTO로 서비스 레이어에서 변환한 후, result라는 이름으로 화면에 전달합니다.

리스트를 보여주는 화면도 아래와 같이 수정합니다. 타임리프 페이지이기 때문에 자세한 설명은 생략하겠습니다.

list.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<th:block th:replace="~{/layout/basic::setContent(~{this::content})}">
    <th:block th:fragment="content">
        <h1 class="mt-4">GuestBook List Page</h1>

        <table class="table table-striped">
            <thead>
            <tr>
                <th scope="col">#</th>
                <th scope="col">Gno</th>
                <th scope="col">Title</th>
                <th scope="col">Regdate</th>
            </tr>
            </thead>

            <tbody>
            <tr th:each="dto : ${result.dtoList}">
                <th scope="row">[[${dto.gno}]]</th>
                <td>[[${dto.title}]]</td>
                <td>[[${dto.writer}]]</td>
                <td>[[${#temporals.format(dto.regDate, 'yyyy/MM/dd')}]]</td>
            </tr>
            </tbody>
        </table>
    </th:block>
</th:block>

여기까지 진행 후 계속 SpringBoot 서버가 실행되지 않는 문제가 있었습니다.
build.gradle에서 providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' 를 지운 후에 다시 gradle build를 하면 정상적으로 실행됩니다.

다음으로 페이지 선택 바를 html에 작성하겠습니다. 위의 table에 이어 아래의 코드를 작성하시면 됩니다.

...생략
    </tbody>
        </table>

        <ul class="pagination h-100 justify-content-center align-items-center">
            <li class="page-item" th:if="${result.prev}">
                <a class="page-link" th:href="@{/guestbook/list(page=${result.start - 1})}" tabindex="-1">Previous</a>
            </li>

          //띄어쓰기를 해야 현재 페이지번호의 class명이 'page-item active'가 됩니다.
            <li th:class=" 'page-item ' + ${result.page == page ? 'active' : ''} " th:each="page: ${result.pageList}">
                <a class="page-link" th:href="@{/guestbook/list(page=${page})}">
                    [[${page}]]
                </a>
            </li>

            <li class="page-item" th:if="${result.next}">
                <a class="page-link" th:href="@{/guestbook/list(page=${result.end + 1})}">Next</a>
            </li>
        </ul>
...생략


버튼이 생성되고 정상적으로 동작하는 모습을 확인할 수 있습니다.

profile
Backend Engineer

0개의 댓글