목표
1. JpaRepository를 이용하여 DataBase에 접근한다.
2. Pageable을 이용하여 페이지처리 + 정렬된 데이터를 가져온다.
3. 제네릭 타입을 이용하여 범용 Dto를 생성한다.
4. RestApi를 통해 받아온 데이터를 이용해 게시판 목록과 페이지 마커를 생성한다.
이전 시간에는 Server에 하드코딩된 데이터를 받아와 front view에 뿌려주었다.
이번 시간에는 진짜 로컬 데이터베이스에 접근하여 데이터를 가져오도록 하겠다.
@RestController
@RequestMapping("/api/boards")
@RequiredArgsConstructor
public class BoardApiController {
private final BoardService boardService;
@GetMapping
public ResponseEntity<PageDto<BoardDto>> get(@PageableDefault(page = 1, size = 10, sort = "createdAt", direction = Sort.Direction.DESC )Pageable pageable){
Page<BoardEntity> pageBoardList = boardService.get(pageable);
List<BoardDto> boardDtoList = pageBoardList.stream().map(boardEntity -> BoardDto.of(boardEntity)).collect(Collectors.toList());
return ResponseEntity.ok(PageDto.of(pageBoardList, boardDtoList));
}
}
눈여겨 볼 것은 @PageableDefault
라는 어노테이션이다. 이를 타고 들어가 보면 아래와 같이 구현돼있음을 알 수 있다. page, size, sort, direction을 이용하고 있으며 이는 client에서 해당 값을 파라미터로 명시하지 않는다면 자동으로 설정될 디폴트 값이다. (size
가 원래 디폴트인 10인데 명시하는 것은 코드를 보는 사람들이 쉽게 이해할 수 있기 위함이다. 생략해도 당연히 10
이라는 값으로 지정된다.)
이 때 dispatcher-servlet.xml
에 새로운 설정이 추가된다.
@PageableDefault
가 아규먼트에 포함되어 있으면 Pageable
을 생성하여 리턴해주는 PageableHandlerMethodArgumentResolver
를 빈으로 등록하기위해 다음처럼 <mvc:annotation-driven>
하위에 <mvc:argument-resolvers>
를 추가한다.
<mvc:annotation-driven>
<mvc:path-matching trailing-slash="false"/>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
</mvc:message-converters>
<mvc:argument-resolvers>
<bean class="org.springframework.data.web.PageableHandlerMethodArgumentResolver"/>
</mvc:argument-resolvers>
</mvc:annotation-driven>
@Service
@Transactional
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
public Page<BoardEntity> get(Pageable pageable) {
return boardRepository.findAll(PageUtil.convertToZeroBasePageWithSort(pageable));
}
}
Pageable
을 파라미터로 받는 get()
을 추가한다.
JpaRepository
의 findAll
메소드를 사용할 것인데 파라미터로 Pageable을 넣어 오버로드된 메소드를 호출할 것이다.
Pageable에는 정렬이 포함될 수 있으며, findAll(Pageable, Sort)로 구성된 메소드는 없으니 헷갈리지 않도록한다.
com/freeboard01
하위에 util
패키지를 생성하고, PageUtil
클래스를 추가한다.
public class PageUtil {
public static Pageable convertToZeroBasePage(Pageable pageable){
return pageable.getPageNumber()<=0 ? PageRequest.of(0, pageable.getPageSize()) : PageRequest.of(pageable.getPageNumber()-1, pageable.getPageSize());
}
public static Pageable convertToZeroBasePageWithSort(Pageable pageable){
Pageable zeroBasePageable = convertToZeroBasePage(pageable);
return PageRequest.of(zeroBasePageable.getPageNumber(), zeroBasePageable.getPageSize(), pageable.getSort());
}
}
view에서는 페이지가 1부터 시작하지만 아래 실제로 수행된 쿼리에서 볼 수 있듯이 페이징에 limit
를 이용하기 때문에 인덱스의 시작점
으로 사용되는 페이지 넘버(limit
의 첫번째 파라미터)는 실제 페이지 넘버보다 1작은 값으로 설정되어야한다.
따라서 PageUtil의 ZeroBase 셋팅을 통해 이를 조정해 주는 것이다.
다시 RestController의 코드를 보자. Page<BoardEntity>이 바로 클라이언트 응답으로 보내지지 않고, PageDto가 응답값으로 리턴되는 것을 확인할 수 있다.
PageDto는 다음과 같이 구현하였다.
@NoArgsConstructor
@Getter
public class PageDto<T> {
private final static int VIEWPAGESIZE = 10;
private int startPage;
private int endPage;
private int totalPages;
private List<T> contents;
private PageDto(int startPage, int endPage, int totalPages, List<T> contents) {
this.startPage = startPage;
this.endPage = endPage;
this.totalPages = totalPages;
this.contents = contents;
}
public static <T, G> PageDto of(Page<G> entities, List<T> contents) {
int totalPages = entities.getTotalPages();
int nowPage = entities.getPageable().getPageNumber() + 1;
int startPage = 1 * VIEWPAGESIZE * (nowPage / VIEWPAGESIZE);
int endPage = startPage + VIEWPAGESIZE - 1;
return new PageDto<>(startPage == 0 ? 1 : startPage, endPage > totalPages ? totalPages : endPage, totalPages, contents);
}
}
PageDto를 따로 구성한 이유는 바로 Page 자체에 대한 정보 때문이다. Page<T>로 반환된 오브젝트 리스트에 있는 값을 이용하여 Client에서 필요로 하는 데이터로 변환하는 작업이 여기서 일어난다.(of
메소드를 유심히 보도록 하자.)
이전에 만들어둔 pageMarket.hbs
에서 사용하고 있는 데이터를 다시 보면 PageDto의 멤버 변수가 어떤 용도로 사용될 것인지 알 수 있다. (실제로 pageMarket.hbs
는 이전에 다른 토이프로젝트에 사용했던 것을 복사해 온 것인데, PageDto 클래스는 pageMarket.hbs
를 보면서 만들었다.🤔)
{{#partial "header"}}
<title>Main Page</title>
{{/partial}}
<!--body-->
{{#partial "contents"}}
<h1>이곳은 게시판입니다.</h1>
<div id="tableSpace"></div>
<div id="pageMarkerSpace"></div>
<button onclick='location.href="/freeboard01"' class='btn btn-primary'>메인으로 이동하기</button>
{{/partial}}
<!--body-->
<!--js-->
{{#partial "js"}}
{{> template/table}}
{{> template/pageMarker}}
<script>
window.onload = () => {
apiRequest(attachBoard);
}
var apiRequest = (callback = null, page = 1) => {
const SIZE = 10;
$.ajax({
method : 'GET',
url : 'api/boards?page='+page+"&size="+SIZE
}).done(function (response) {
if(typeof callback != 'undefined'){
callback(response);
}
})
}
var attachBoard = (response) => {
if(typeof response != 'undefined') {
var template = Handlebars.compile($("#tableList").html());
$("#tableSpace").html(template(response));
var template = Handlebars.compile($("#pageMarker").html());
$("#pageMarkerSpace").html(template(response));
}
}
var pageConvert = (page) => {
apiRequest(attachBoard, page);
}
</script>
{{#block "helper"}}{{/block}}
{{/partial}}
<!--js-->
{{> static/helper/helper}}
{{> layout/layout}}
apiRequest를 보면 callback 파라미터 외에 page 파라미터가 추가됐음을 알 수 있다.
바로 위의 window.onload에서 굳이 page 번호를 지정하지 않았고, 디폴트로 설정해둔 1
이 사용됨을 알 수 있다.
attachBoard를 수정하기 전에 톰캣을 실행하고 게시판으로 들어가보자. 당연히 데이터는 하나도 나오지 않겠지만, network
탭에서 우리가 server로부터 넘겨받는 JSON이 어떻게 생겼는지 확인 할 수 있다.
위 노란 박스 내의 데이터를 보기좋게 변환해보았다.
{
"startPage":1,
"endPage":4,
"totalPages":4,
"contents":[
{
"user":"myNameis0",
"password":"1234",
"contents":"test data~0",
"title":"제목입니다.000"
},
... (중략) ... ,
{
"user":"myNameis9",
"password":"1234",
"contents":"test data~9",
"title":"제목입니다.999"
}
]
}
table.hbs
템플릿에서 게시판 목록을 만들기 위해 {{#contents}} ... {{/contents}}
라고 작성한 것을 기억한다면 이 템플릿으로 network에서 회신 받은 데이터를 그대로 전달해도 되겠음을 깨달을 것이다.
따라서 이전에 data
라는 오브젝트를 선언하고 contents : response
라고 JSON 오브젝트를 주입한 것을 지우고 $("#tableSpace").html(template(response))
을 통해 바로 데이터를 전달해주었다.
같은 방식으로 pageMarker.hbs
템플릿 코드를 보며 attachBoard
함수내의 나머지 코드들도 수정해보자.
pageConvert(page)
는 새로 추가된 함수인데, 페이지 마커를 이용하여 만들어진 페이지 버튼을 누를 때 마다 새로운 API요청을 보내는 함수이다.
<script id="pageMarker" type="text/x-handlebars-template">
\{{#if this}}
<div aria-label="Page navigation example">
<ul class="pagination mt-3">
<li class="page-item \{{#if (eqw startPage 1)}}disabled\{{/if}}">
<a class="page-link" id='prevPageBtn' \{{#if (eqw startPage 1)}}disabled="disabled"\{{/if}} data-pagenumber='\{{startPage}}' href="#" onclick="pageConvert(\{{math startPage '-' 1}})">Prev</a>
</li>
\{{#for startPage endPage 1}}
<li class="page-item">
<a class="page-link pageBar" data-pagenumber='\{{this}}' onclick="pageConvert(\{{this}})">\{{this}}</a>
</li>
\{{/for}}
<li class="page-item \{{#if (eqw endPage totalPages)}}disabled\{{/if}}">
<a class="page-link" id='nextPageBtn' \{{#if (eqw endPage totalPages)}}disabled="disabled"\{{/if}} data-pagenumber='\{{endPage}}' href="#" onclick="pageConvert(\{{math endPage '+' 1}})">Next</a>
</li>
</ul>
</div>
\{{/if}}
</script>
바뀐 부분은 <ul>
내부의 두번째 <li>
태그 단락이다.
정확히 말하면 두번째 <li>
내부의 <a>
태그가 변경되었다. onclick을 추가하여 페이지를 클릭하면 board.hbs에 정의된 pageConvert(page)를 호출한다.
데이터 베이스에 충분한 데이터를 임시로 추가한 다음에 톰캣을 띄워보자
페이지 버튼을 클릭하였을 떄 데이터가 잘 바뀐다면 성공!
ref. 현재까지의 코드는 github에서 확인할 수 있다.
페이지 DTO는 만들생각을 안해봤는데 많은 도움이 되었습니다