GuestBook 프로젝트 (2) (Querydsl Test Code, DTO, Service, ServiceImpl)
GuestBook 프로젝트 (2) (Querydsl Test Code, DTO, Service, ServiceImpl)
GuestBook 프로젝트 (1) (프로젝트 구조, gradle, application, querydsl 설정) GuestBook 프로젝트 (1) (프로젝트 구조, gradle, application, querydsl 설정) 코드로 배우는 스프링 부트 웹 프로젝트 - 남가람북스 코드로
soohykeee.tistory.com
앞서 개발한 내용에서 이제 목록을 처리하는 작업을 진행하려고 한다. 목록을 처리하기 위해서 고려해야할 사항들이 있다.
또한 여기서 미리 개발하는 목록을 처리하는 코드는, 재사용의 가능성이 높으므로 재사용에 유용하도록 코드를 작성해야한다.
- 화면에서 필요한 목록 데이터에 대한 DTO 생성
- DTO를 Pageable 타입으로 전환
- Page<Entity>를 화면에서 사용하기 쉬운 DTO의 리스트 등으로 변환
- 화면에 필요한 페이지 번호 처리
목록 페이지를 요청할 때 사용하는 페이지 요청 처리 PageRequestDTO
서비스 계층이 처리할 수 있도록 페이지 결과 처리를 하는 PageResultDTO
이 두가지를 활용하여 목록처리를 해줄것이다. 지금 작성하는 이 DTO들은 임시로써, 추후에 다시 수정할필요가 있다.
PageRequestDTO
목록 페이지를 요청할 때 사용하는 데이터를 재사용하기 쉽게 만드는 클래스로, 목록 화면에서는 페이지 처리를 하는 경우가 많이 있기 때문에 '페이지번호', '페이지 내 목록의 개수', '검색 조건' 등이 많이 사용된다.
package com.example.guestbookdemo.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;
@Builder
@AllArgsConstructor
@Data
public class PageRequestDTO {
private int page;
private int size;
// 페이지 1개당 10개씩 목록으로 보여주는 기본값을 생성자로 세팅
public PageRequestDTO() {
this.page = 1;
this.size = 10;
}
public Pageable getPageable(Sort sort) {
// page는 항상 0부터 시작해야하므로 page-1이 필요
return PageRequest.of(page - 1, size, sort);
}
}
PageResultDTO
JPA를 이용하는 Repository에서는 페이지 처리 결과를 Page<Entity>타입으로 반환하게 된다. 따라서 서비스 계층에서 이를 처리하기 위해서 별도의 클래스를 만들어서 처리해야 한다. 처리하는 클래스 내용은 아래와 같이 해야한다.
- Page<Entity>의 엔티티 객체들을 DTO 객체로 변환해서 자료구조로 담아 주어야 한다.
- 화면 출력에 필요한 페이지 정보들을 구성해 주어야 한다.
package com.example.guestbookdemo.dto;
import lombok.Data;
import org.springframework.data.domain.Page;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
//DTO와 Entity 타입을 의미한다. 제네릭 타입으로 설정하여 다양한 곳에 이용할 수 있도록 한다.
@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를 제네릭 타입으로 설정하여 작성하면, 나중에 어떤 종류의 Page<Entity> 타입이 생성이 되더라도 해당 Dto 클래스를 이용하여 처리할 수 있다는 장점이 있다.
서비스 계층에서의 목록처리를 위한 수정
앞서 작성한 GuestbookService에 PageRequestDTO를 파라미터로, PageResultDTO를 리턴 타입으로 사용하는 getList() 메소드를 생성하고, default를 사용하여 Entity -> DTO로 변환하는 entityToDto() 메소드를 생성한다.
package com.example.guestbookdemo.service;
import com.example.guestbookdemo.dto.GuestbookDTO;
import com.example.guestbookdemo.dto.PageRequestDTO;
import com.example.guestbookdemo.dto.PageResultDTO;
import com.example.guestbookdemo.entity.Guestbook;
public interface GuestbookService {
Long register(GuestbookDTO guestbookDTO);
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;
}
}
-------------------------------------------------------------
package com.example.guestbookdemo.service;
import com.example.guestbookdemo.dto.GuestbookDTO;
import com.example.guestbookdemo.dto.PageRequestDTO;
import com.example.guestbookdemo.dto.PageResultDTO;
import com.example.guestbookdemo.entity.Guestbook;
import com.example.guestbookdemo.repository.GuestbookRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.function.Function;
@Service
@Log4j2
@RequiredArgsConstructor
public class GuestbookServiceImpl implements GuestbookService {
private final GuestbookRepository repository;
@Override
public Long register(GuestbookDTO guestbookDTO) {
Guestbook entity = dtoToEntity(guestbookDTO);
repository.save(entity);
return entity.getGno();
}
@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);
}
}
여기까지 작성을 했다면, 이제 PageResultDTO 파일에 목록 페이지를 출력할 때 필요한 정보들을 추가해 줘야한다.
- 화면에서 하단의 시작페이지 번호 (start)
- 화면에서 하단의 끝 페이지 번호 (end)
- 이전/다음 이동 링크 (prev / next)
- 현재 페이지 번호 (page)
페이지 계산을 할 때, 끝번호 부터 계산을 하는 것이 코드짜기에 더 수월하다.
만약 페이지 번호를 10개씩 보인다고 가정할 때, 아래와 같이 코드를 짜면 된다.
tempEnd = ( int ) ( Math.ceil ( 페이지번호 / 10.0 ) ) * 10
끝 번호를 tempEnd, 임시로 둔 이유는 확정적인 끝 번호가 아니라서 그렇다. tempEnd는 시작번호(start)를 알기 쉽기 위해서 사용하는 것이라고 봐도 된다. 페이지 번호를 10개씩 보이기로 했음으로 start는 다음과 같이 설정하면 된다.
start = tempEnd - 9
실제 끝 번호(end)는 실제 마지막 페이지와 비교해서 설정해줘야 한다. 이렇게 해주는 이유는 만약, 마지막페이지가 33이여야 하는데, tempEnd는 올림에 의해 위의 계산대로 한다면 40이 될것이다. 따라서 제대로 된 끝 번호를 end에 저장해줘야 한다. 이를 위해서는 Page<Entity>에서의 getTotalPages()를 활용해야 한다.
totalPage = result.getTotalPages() ; -> result 는 Page<Guestbook>
end = totalPage > tempEnd ? tempEnd : totalPage ;
이전(prev), 다음(next) 버튼의 출력 유무는 간단하게 설정이 가능하다. 이전버튼은 시작 번호가 1보다 크다면 항상 존재해야하고, 다음버튼은 위에서의 totalPage가 tempEnd보다 크면 항상 존재해야한다.
prev = start > 1 ;
next = totalPage > tempEnd ;
위의 내용을 pageResultDTO에 적용하면 아래와 같다.
package com.example.guestbookdemo.dto;
import lombok.Data;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
//DTO와 Entity 타입을 의미한다. 제네릭 타입으로 설정하여 다양한 곳에 이용할 수 있도록 한다.
@Data
public class PageResultDTO<DTO, EN> {
private List<DTO> dtoList; //dto리스트
private int totalPages; //총 페이지 번호
private int page, size; //현재 페이지 번호, 목록 사이즈
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());
totalPages = 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;
end = totalPages > tempEnd ? tempEnd : totalPages;
next = totalPages > tempEnd;
pageList = IntStream.rangeClosed(start,end).boxed().collect(Collectors.toList());
}
}
Controller와 화면출력
package com.example.guestbookdemo.controller;
import com.example.guestbookdemo.dto.PageRequestDTO;
import com.example.guestbookdemo.service.GuestbookService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/guestbook")
@Log4j2
@RequiredArgsConstructor
public class GuestbookController {
private final GuestbookService service;
@GetMapping("/")
public String list() {
return "redirect:/guestbook/list";
}
@GetMapping("/list")
@ModelAttribute("pageRequestDTO")
public void list(PageRequestDTO pageRequestDTO, Model model) {
log.info("list..................." + pageRequestDTO);
model.addAttribute("result", service.getList(pageRequestDTO));
}
}
위와 같이 GuestbookController를 수정해주고, 해당 /list에 매핑되는 html 파일 또한 수정해준다.
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>
<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>
<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>
</th:block>
</th:block>