* 이 포스트는 전 배달의민족, 현재 인프런에 계시고 유튜브 개발바닥의 크리에이터이신 개발자 이동욱님의 '스프링부트와 AWS로 혼자 구현하는 웹 서비스'를 기반으로 작성된 코드를 기반으로 진행중인 프로젝트에 대한 글임을 알립니다. 포스트 맨 아래에 관련 링크가 있습니다.
* 참고로 @Getter, @Setter 어노테이션은 전부 생략했습니다. 필요할때만 언급하겠습니다.
책 부분이 끝난 후부터는 코드를 커스텀하는 과정을 포스팅합니다.
책 부분이 궁금하시면 ready부터 보시길 추천합니다.
참고 : https://github.com/kth1017/S1
이제 view는 임시로 완성되었으니 게시판에 필요한 페이징, 검색, 검증 기능을 추가로 추가하도록 하겠습니다. 이 중 가장 먼저 페이징을 먼저 소개하는게 맞다고 판단해 해당 포스트에선 페이징 기능 추가를 다루겠습니다. 페이징 기능에 기초가 궁금하시다면 스프링 페이징 기능 개요에 대한 https://fadet-coding.tistory.com/30 글을 먼저 읽고 오시는 것을 추천합니다. 이번 포스트는 해당 글을 읽었다는 전제하에 기초적인 설명은 생략합니다.
Index
1 개요
2 pagination
3 PostsController와 PostsService
# 트랜잭션 스크립트와 도메인 모델 패턴 실습
4 마무리
개요
포스트로 따로 다루겠지만 스프링 프로젝트 설계시 저는 의존성의 방향을 가장 중시합니다. 페이징, 검색, 검증에 대한 클래스들은 당연히 게시판 관련 클래스들을 의존하기 때문에 게시판 관련 기능이 먼저 완성되어야하며 게시판 기능은 앞선 포스트들에서 이미 구축되어 있기때문에 이제 게시판 부가 기능을 추가할 수 있습니다.
그런데 페이징, 검색, 검증에 대한 의존 구조는 한 번 생각해 볼 필요가 있었습니다. 이거야 정답이 없겠지만 이 프로젝트 에 적용할때 검증은 맨 나중에 추가하고, 페이징 관련 클래스를 검색 관련 클래스들이 참조하게 되어 페이징 기능을 먼저 코딩하는게 낫다고 판단되더라구요. 그래서 참조가 없는 페이징 관련 기능을 먼저 포스트합니다.
pagination
일단 가장 먼저 페이징 버튼들의 실제 값들을 담는 pagination을 먼저 만들겠습니다. 일단 필드 선언부입니다. 필드 선언부는 위에 링크된 글 내용과 크게 다를 것이 없습니다.
// Pagination - part 1
public class Pagination {
/** 1. 페이지 당 보여지는 게시글의 최대 개수 **/
private int pageSize = 10;
/** 2. 페이징된 버튼의 블럭당 최대 개수 **/
private int blockSize = 10;
/** 3. 현재 페이지 **/
private int page = 1;
/** 4. 현재 블럭 **/
private int block = 1;
/** 5. 총 게시글 수 **/
private int totalListCnt;
/** 6. 총 페이지 수 **/
private int totalPageCnt;
/** 7. 총 블럭 수 **/
private int totalBlockCnt;
/** 8. 블럭 시작 페이지 **/
private int startPage = 1;
/** 9. 블럭 마지막 페이지 **/
private int endPage = 1;
/** 10. DB 접근 시작 index **/
private int startIndex = 0;
/** 11. 이전 블럭의 마지막 페이지 **/
private int prevBlockPage;
/** 12. 다음 블럭의 시작 페이지 **/
private int nextBlockPage;
다음은 생성자입니다. 생성자 내에 필드들이 set되는 과정을 메서드화하여 따로 뺄까도 생각했지만 의미가 크게 없어서 그냥 진행했습니다. 이 부분도 위 링크 내용과 동일합니다.
// Pagination - part 2
public Pagination(int totalListCnt, int page) {
// 총 게시물 수와 현재 페이지를 Controller로 부터 받아옴
/** 현재 페이지 **/
setPage(page);
/** 총 게시글 수 **/
setTotalListCnt(totalListCnt);
/** 총 페이지 수 **/
// 한 페이지의 최대 개수를 총 게시물 수 * 1.0로 나누어주고 올림 해준다.
// 총 페이지 수 를 구할 수 있다.
setTotalPageCnt((int) Math.ceil(totalListCnt * 1.0 / pageSize));
/** 총 블럭 수 **/
// 한 블럭의 최대 개수를 총 페이지의 수 * 1.0로 나누어주고 올림 해준다.
// 총 블럭 수를 구할 수 있다.
setTotalBlockCnt((int) Math.ceil(totalPageCnt * 1.0 / blockSize));
/** 현재 블럭 **/
// 현재 페이지 * 1.0을 블록의 최대 개수로 나누어주고 올림한다.
// 현재 블록을 구할 수 있다.
setBlock((int) Math.ceil((page * 1.0) / blockSize));
/** 블럭 시작 페이지 **/
setStartPage((block - 1) * blockSize + 1);
/** 블럭 마지막 페이지 **/
setEndPage(startPage + blockSize - 1);
/** 이전 블럭(클릭 시, 이전 블럭 마지막 페이지) **/
setPrevBlockPage((block * blockSize) - blockSize);
/** 다음 블럭(클릭 시, 다음 블럭 첫번째 페이지) **/
setNextBlockPage((block * blockSize) + 1);
/** DB 접근 시작 index **/
setStartIndex((page - 1) * pageSize);
여기서 생성자 부분 마지막에 검증 부분이 빠졌습니다. 이 검증 부분은 제가 따로 메서드화하여 아래로 뺐고 생성자 맨 아래 다음과 같은 validationRun() 메서드를 추가합니다.
// Pagination - part3
public Pagination(int totalListCnt, int page) {
/** 위와 동일 **/
setStartIndex((page - 1) * pageSize);
validationRun();
}
validationRun() 메서드는 다음 각각의 검증 메서드들을 한번에 순회하는 코드입니다.
// Pagination - part4
public void validationRun(){
validationEndPage();
validationNextBlock();
validationPreBlock();
validationPostsNotExist();
}
각 메서드들의 코드입니다. 위 3개의 검증 메서드들은 원래 알고 있었는데 validationPostsNotExist()의 경우 게시글이 하나도 없을때 위 로직이 작동하지 않길래 새로 추가했습니다.
public void validationEndPage() {
if (endPage > totalPageCnt) {
this.endPage = totalPageCnt;
}
}
public void validationPreBlock() {
if (prevBlockPage < 1) {
this.prevBlockPage = 1;
}
}
public void validationNextBlock() {
if (nextBlockPage > totalPageCnt) {
nextBlockPage = totalPageCnt;
}
}
//검색 기능 도입시 게시글이 0일 경우 검증
public void validationPostsNotExist() {
if (totalListCnt == 0) {
setTotalPageCnt(1);
setEndPage(1);
setNextBlockPage(1);
}
}
PostsController와 PostsService
이제 페이징 필드를 작성했으니 본격적으로 게시판에 페이징 기능을 적용하겠습니다. 우선 맨 처음 기능만 추가했을 때의 코드는 이렇습니다. 일단 서비스 코드입니다.
//v1
public class PostsService {
/** 생략 **/
@Transactional
public List<PostsListResponseDto> findListpaging(int startindex, int pagesize) {
return postsRepository.findAllDesc().stream()
.skip(startindex)
.limit(pagesize)
.map(PostsListResponseDto::new)
.collect(Collectors.toList());
}
기본적으로 페이징할땐 skip(), limit()로 개수 제한하는 로직은 거의 다 비슷합니다만 위 코드의 단점은 stream을 써서 코드는 간결한데 로직 상 성능 이슈가 발생할 가능성이 있습니다. 스트림 연산 과정을 보면 postsRepository 글을 전부 가져와서 그 다음에 걸러내는 순서를 가지고 있기에 처음 db에 쿼리를 보낼때 글 전부를 가져오는 비효율을 보여줍니다. 그래서 저는 1 stream과 메서드참조의 실습 2 프로젝트의 규모가 작음 이라는 이유때문에 이 코드를 쓰지만 Repository에 페이징 로직을 가지는 메서드를 추가해서 쿼리를 줄이는 것이 원래는 best practice입니다. 다음은 서비스 코드를 가져다 쓰는 컨트롤러 코드입니다.
// v1
public class IndexController {
/** 생략 **/
@GetMapping("/posts-list")
public String postsList(Model model, @LoginUser SessionUser user, @RequestParam(defaultValue = "1") int page) {
if (user != null) {
model.addAttribute("userName", user.getName());
}
/*
페이징 처리
*/
int totalListCnt = postsService.findAllDesc().size();
Pagination pagination = new Pagination(totalListCnt, page);
int startIndex = pagination.getStartIndex();
int pageSize = pagination.getPageSize();
List<PostsListResponseDto> postsList = postsService.findListpaging(startIndex, pageSize);
model.addAttribute("postsList", postsList);
model.addAttribute("pagination", pagination);
return "posts-list";
}
사실 이렇게만 하면 페이징 기능이 완성은 됩니다만 딱봐도 코드가 엄청 더럽습니다. 초기 코드가 왜 이런지 살펴보자면 우선 컨트롤러가 요청 파라미터인 page를 인자로 넣어야 pagination 객체를 호출할 수 있기때문입니다. 하지만 이처럼 컨트롤러에서 비즈니스 로직을 다 처리하면 너무 비대해지므로 좀 고치고 싶습니다. 그래서 일단 해당 코드의 문제를 먼저 파악해보자면 다음과 같은 문제를 갖고 있습니다.
1. totalListCnt, startIndex 등 @requesParam이 필요 없는 필드들이 컨트롤러에서 선언됨
2. pagination 객체를 컨트롤러에서 호출함
# 트랜잭션 스크립트와 도메인 모델 패턴 실습
따라서 저는 이 두 가지를 service 단에서 처리하게끔 컨트롤러의 코드를 다음과 같이 변경하겠습니다.
// v2
@GetMapping("/posts-list")
public String postsList(Model model, @LoginUser SessionUser user, @RequestParam(defaultValue = "1") int page) {
/** 생략 **/
Pagination pagination = postsService.findPagination(page)
List<PostsListResponseDto> postsList = postsService.findByPagination(pagination);
model.addAttribute("postsList", postsList);
model.addAttribute("pagination", pagination);
return "posts-list";
}
이제 컨트롤러에서는 postsService의 findPagination(), findByPagination() 메서드만 호출하면 되도록 코드가 간략해졌습니다. 쓸데 없는 필드를 입력할 필요가 없게되었습니다. 메서드를 사용해서 코드를 줄였으니 이제 서비스에 추가된 해당 메서드의 코드를 보겠습니다.
//v2
public class PostsService {
/** 생략 **/
@Transactional
public Pagination findPagination( int page) {
int totalListCnt = postsRepository.findAllDesc().size();
PostsSearch caculPaging = new PostsSearch(totalListCnt, page);
return caculPaging.SearchCalcul();
}
@Transactional
public List<PostsListResponseDto> findByPagination(Pagination pagination) {
int startindex = pagination.getStartIndex();
int pageSize = pagination.getPageSize();
return postsRepository.findAllDesc().stream()
.skip(startindex)
.limit(pageSize)
.map(PostsListResponseDto::new)
.collect(Collectors.toList());
}
일단 위 코드에서 PostSearch 클래스는 이후에 소개하겠습니다. 그냥 여기선 pagination을 생성해주는 메서드라고 생각하고 넘어가시면 됩니다. 이제 컨트롤러는 간단해졌습니다만 서비스가 비대해졌습니다. 책을 보셨으면 알겠지만 서비스에 각종 로직이 들어 있는 이런 경우를 트랜잭션 스크립트 패턴 설계라고 하는데 이 경우 서비스 계층이 단순 객체 덩어리가 되기에 최대한 서비스 컴포넌트는 객체의 순서만 보장하도록 설계하는 것이 좋습니다. 전과 같이 해당 코드의 문제를 먼저 파악해보자면 다음과 같은 문제를 갖고 있습니다.
1. 컨트롤러에서 findPagination(), findByPagination() 두 번의 메서드를 호출함
2. findPagination(), findByPagination() 메서드가 비즈니스 로직을 처리함
따라서 다음과 같이 메서드 하나로 개선했습니다.
// v3
@Transactional
public SearchPostResultDto findSearchList(int page) {
PostsSearch postsSearch = new PostsSearch(postsRepository);
SearchPostResultDto dto = postsSearch.findSearchPostsList(page);
return dto;
}
findPagination(), findByPagination()에서 처리했던 비즈니스 로직을 findSearchList()은 직접 처리하지 않고 객체를 호출하기만 해서 객체가 처리하도록 맡깁니다. 해당 메서드는 PostsSearch, SearchPostResultDto 클래스가 필요하고 먼저 SearchPostResultDto를 소개하겠습니다.
@RequiredArgsConstructor
public class SearchPostResultDto {
// 인덱스 컨트롤러 > 뷰
private final List<PostsListResponseDto> PostsListPaging;
private final Pagination pagination;
}
해당 객체는 그냥 postSearch 객체를 서비스 컴포넌트로 데이터만 전달하는 dto로 pagination과 페이징 처리가 끝나고 화면에 보여줄 글 리스트인 PostsListPaging만 필드로 선언됩니다. 다음은 해당 dto를 사용할 PostsSearch입니다.
@RequiredArgsConstructor
public class PostsSearch {
private final PostsRepository postsRepository;
private int page;
private int totalListCnt;
private List<Posts> searchPostsList;
public SearchPostResultDto findSearchPostsList(int page) {
initParam(page);
return ResultDtoCreate();
}
/*
상세 메서드
*/
public void initParam(int page){
this.page = page;
}
public SearchPostResultDto ResultDtoCreate(){
totalListCnt = this.searchPostsList.size();
Pagination pagination = new Pagination(totalListCnt, this.page);
int startindex = pagination.getStartIndex();
int pageSize = pagination.getPageSize();
List<PostsListResponseDto> searchPostsListPaging = searchPostsList.stream()
.sorted(Comparator.comparing(Posts::getId).reversed())
.skip(startindex)
.limit(pageSize)
.map(PostsListResponseDto::new)
.collect(Collectors.toList());
SearchPostResultDto searchResultDto = new SearchPostResultDto(searchPostsListPaging, pagination);
return searchResultDto;
}
PostsSearch의 객체는 postsRepository를 인자로 받아 생성됩니다. PostsSearch의 주요 메서드는 findSearchPostsList()로 해당 메서드는 page 객체를 받는 initParam()을 먼저 수행하고 ResultDtoCreate() 메서드를 호출하여 위에서 소개한 SearchPostResultDto를 return합니다.
ResultDtoCreate() 메서드는 코드 개선 전 컨트롤러에서 진행하던 과정을 대신 수행하고 마지막에 SearchPostResultDto를 생성하며 그 안에 게산이 끝난 pagination과 List를 담습니다.
마무리
위 과정을 전부 마치면 도메인 모델 패턴을 바탕으로 개선해서 도메인들이 비즈니스 로직을 담은 메서드를 가지며 서비스와 컨트롤러 단은 이렇게 깔끔해집니다.
// Controller
@GetMapping("/posts-list")
public String postsList(Model model, @LoginUser SessionUser user, @RequestParam(defaultValue = "1") int page) {
/** 생략 **/
SearchPostResultDto dto = postsService.findSearchList(page);
model.addAttribute("postsList", dto.getSearchPostsListPaging());
model.addAttribute("pagination", dto.getPagination());
return "posts-list";
}
//Service
@Transactional
public SearchPostResultDto findSearchList(int page) {
PostsSearch postsSearch = new PostsSearch(postsRepository);
SearchPostResultDto dto = postsSearch.findSearchPostsList(page);
return dto;
}
View는 타임리프를 사용하여 작성하면 되고 별 어려운 것은 없었기 때문에 생략하겠습니다, 모든 페이징 처리가 완료된 결과 화면은 이렇습니다.
'Project' 카테고리의 다른 글
P2_페이지 내 하이퍼 링크 달아주는 코드_1_단순로직 (0) | 2022.06.08 |
---|---|
P2_페이지 내 하이퍼 링크 달아주는 코드_준비 (0) | 2022.06.03 |
P1_게시판 프로젝트_3_thymeleaf layout (0) | 2022.04.25 |
P1_게시판 프로젝트_2_thymeleaf index (0) | 2022.04.19 |
P1_게시판 프로젝트_1_thymeleaf 시작 (0) | 2022.04.15 |