SpringBoot #6 SpringBoot+React 기반 간단한 게시판 생성하기 #2 페이징, 검색 처리

2021. 10. 14. 13:53JAVA/Spring

이전글

https://yonghwankim-dev.tistory.com/145

 

SpringBoot #6 SpringBoot+React 기반 간단한 게시판 생성하기 #1 SpringBoot 프로젝트 기본 구조 생성

개요 스타트 스프링 부트라는 도서를 공부하던 중 웹 페이지를 처리하는 View 부분을 Thymeleaft라는 뷰 템플릿을 사용하여 구현하는 것으로 서술되었습니다. 하지만 저는 View 부분을 서버 개발과

yonghwankim-dev.tistory.com

 

본 글은 스타트 스프링 도서의 내용을 복습하기 위해 작성된 글입니다.

 

개요

이전 글에서는 간단한 게시판을 구현하기 위해서 SpringBoot 프로젝트를 생성하고 기본적인 도메인, 컨트롤러 및 데이터베이스 연결 설정 등을 설계하였습니다. 본 글에서는 데이터베이스에 저장된 게시물 샘플 데이터를 활용하여 페이지징 및 검색 처리를 구현하고 테스트하는 것을 목표로 합니다. 아직 웹 페이지를 작성하지 않았기 때문에 이러한 테스트의 결과들은 콘솔 출력을 통하여 확인합니다.

 

페이징, 검색 처리

1. Repository 페이징 테스트

페이징에 대한 테스트는 QuerydslPredicateExecutor의 findAll()을 이용해서 작성하고, Pageable을 사용합니다.

 

org.zerock.persistence.WebBoardRepository.java

package org.zerock.persistence;

import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;
import org.zerock.domain.QWebBoard;
import org.zerock.domain.WebBoard;

import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;

public interface WebBoardRepository extends CrudRepository<WebBoard, Long>,
											QuerydslPredicateExecutor<WebBoard>{
	public default Predicate makePredicate(String type, String keyword) {
		BooleanBuilder builder = new BooleanBuilder();
		
		QWebBoard board = QWebBoard.webBoard;
		
		// type if ~ else
        
		// bno>0
		builder.and(board.bno.gt(0));
		
		return builder;
	}
}

makePredicate()는 검색에 필요한 타입(type) 정보와 키워드(keyword)를 이용해서 적당한 쿼리를 생성합니다. 이 코드에는 아직 다양한 조건에 대한 처리가 없으므로 단지 'where bno>0'이라는 조건만을 생성하도록 합니다. makePredicate 메서드를 실행할 테스트 코드는 아래와 같습니다.

 

org.zerock.WebBoardRepositoryTests.java 일부

package org.zerock;

import java.util.stream.IntStream;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.test.annotation.Commit;
import org.zerock.domain.WebBoard;
import org.zerock.persistence.WebBoardRepository;

import lombok.extern.java.Log;

@SpringBootTest
@Log
@Commit
class WebBoardRepositoryTests {

	@Autowired
	WebBoardRepository repo;
	
    // 한 페이지에 20개, "bno"를 기준으로 내림차순으로 페이징 처리
	@Test
	public void testList1() {
		Pageable pageable = PageRequest.of(0, 20, Direction.DESC, "bno");
		
		Page<WebBoard> result = repo.findAll(repo.makePredicate(null, null), pageable);
		
		log.info("PAGE: " + result.getPageable());
		
		log.info("-----------------");
		result.getContent().forEach(board->log.info(""+board));
	}
}

위의 테스트 결과와 같이 게시물번호(bno)를 기준으로 역순으로 출력된 것을 볼 수 있습니다.

 

1.1 검색 조건 처리

검색 조건이 없을 때 페이지 처리에 이상이 없는 것을 확인하였다면, 검색 조건에 맞게 남은 부분을 구현하도록 합니다.

 

org.zerock.persistence.WebBoardRepository.java

package org.zerock.persistence;

import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;
import org.zerock.domain.QWebBoard;
import org.zerock.domain.WebBoard;

import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;

public interface WebBoardRepository extends CrudRepository<WebBoard, Long>,
											QuerydslPredicateExecutor<WebBoard>{
	public default Predicate makePredicate(String type, String keyword) {
		BooleanBuilder builder = new BooleanBuilder();
		
		QWebBoard board = QWebBoard.webBoard;
		
		// type if ~ else
		// bno>0
		builder.and(board.bno.gt(0));
		
		if(type==null)
		{
			return builder;
		}
		
		switch(type)
		{
		case "t":
			builder.and(board.title.like("%"+keyword+"%"));
			break;
		case "c":
			builder.and(board.content.like("%"+keyword+"%"));
			break;
		case "w":
			builder.and(board.writer.like("%"+keyword+"%"));
			break;
		}
		
		return builder;
	}
}

org.zerock.WebBoardRepositoryTests.java

@SpringBootTest
@Log
@Commit
class WebBoardRepositoryTests {

	@Autowired
	WebBoardRepository repo;
	
	// 제목에 "10"이 포함되는 게시물을 검색
	@Test
	public void testList2() {
		Pageable pageable = PageRequest.of(0, 20, Direction.DESC, "bno");
		
		Page<WebBoard> result = repo.findAll(repo.makePredicate("t", "10"), pageable);
		
		log.info("PAGE: " + result.getPageable());
		
		log.info("-----------------");
		result.getContent().forEach(board->log.info(""+board));
	}

}

 

2. 컨트롤러의 페이징 처리

Repository 쪽에서 페이징과 검색에 대한 처리가 완료되었으므로, 이제 컨트롤러에서 파라미터를 전달하고, 연동해서 결과를 처리하도록 해야 합니다.

 

웹 화면에서 전달되는 데이터는 크게 다음과 같습니다.

  • 페이지 관련
    • 페이지 번호(page - 0, 1, 2, 3...)
    • 페이지당 사이즈(size - PageRequest의 기본 size는 20)
  • 검색 관련
    • 검색 종류(type) : t(title), c(content), w(writer)
    • 검색 키워드(keyword)

2.1 @PageableDefault를 이용한 처리

WebBoardController의 list()에 페이지 관련 처리를 위한 파라미터를 추가하면 다음과 같습니다.

 

org.zerock.controller.WebBoardController.java

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/boards/")
@Log
public class WebBoardController {
	
	@GetMapping("/list")
	public void list(@PageableDefault(direction=Sort.Direction.DESC,
                                        sort="bno",
                                        size=10,
                                        page=0) Pageable page) {
    
    	log.info("list() called... " + page);
	}
}

페이징 처리에는 Pageable 타입을 이용하는 것이 간단하지만, 매번 페이지 처리를 할때마다 size나 direction 등을 지정해야 하는 경우에 매번 여러 개의 파라미터를 전달해서 불편할 수 있습니다. Spring Data 모듈에서는 @PageableDefault 어노테이션을 이용하면 간단하게 Pageable 타입의 객체를 지정할 수 있습니다.

 

테스트 실행 결과 확인

프로젝트 실행->브라우저 검색창에 http://localhost:8080/boards/list 입력-> IDE 콘솔창에서 결과 확인

위의 결과는 'page'나 'size' 파라미터를 추가하지 않고 기본값을 이용한 경우이고 아래와 같이 브라우저에 입력하면 'page'와 'size' 파라미터에 값을 넣을 수 있습니다.

 

2.2 PageVO를 생성하는 방식

@PageableDefault 방식 단점

  • 페이지 번호가 0부터 시작하기 때문에 직관적이지 않음
  • 파라미터를 이용해서 size를 지정 가능하기 때문에 고의적으로 size 값을 크게 주는 것을 막을 수 없음
  • 기타 정렬방향이나 속성 역시 모두 브라우저에서 전달되는 값을 통해 조절할 수 있기 때문에 고의적인 공격에 취약함

위와 같은 문제때문에 @PageableDefault를 이용하는 방식보다는 별도로 파라미터를 수집해서 처리하는 Value Object를 생성하는 방식이 이러한 문제를 조금은 줄어줄 수 있습니다. 

 

org.zerock.vo.PageVO 생성 및 작성

package org.zerock.vo;

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

public class PageVO {
	private static final int DEFAULT_SIZE = 10;
	private static final int DEFAULT_MAX_SIZE = 50;
	
	private int page;
	private int size;
	
	public PageVO() {
		this.page = 1;
		this.size = DEFAULT_SIZE;
	}
	
	public int getPage() {
		return page;
	}
	
	public void setPage(int page) {
		this.page = page < 0 ? 1 : page;
	}
	
	public int getSize() {
		return size;
	}
	
	public void setSize(int size) {
		this.size = size < DEFAULT_SIZE || size > DEFAULT_MAX_SIZE ? DEFAULT_SIZE : size;
	}
	
	public Pageable makePageable(int direction, String... props) {
		Sort.Direction dir = direction==0 ? Sort.Direction.DESC : Sort.Direction.ASC;
		
		return PageRequest.of(this.page-1, this.size, dir, props);
	}
}

PageVO 클래스는 브라우저에서 전달되는 값은 페이지 번호(page)와 게시물의 수(size)만을 받도록 설계하고, 이때에도 일정 이상의 값이 들어올 수 없도록 제약을 둡니다. 이후 정렬방향이나 정렬 기준이 되는 속성은 컨트롤러에서 지정합니다.

 

PageVO 클래스에서 가장 주목해야 하는 부분은 makePageable() 메서드입니다. 전달되는 파라미터를 이용해서 최종적으로 PageRequest로 Pageable 객체를 생성합니다. 이때 브라우저에서 전달되는 page 값을 1 줄여서 Pageable 타입의 객체를 생성합니다.

 

org.zerock.controller.WebBoardController.java의 list() 메서드 내용 수정

	@GetMapping("/list")
	public void list(PageVO vo) {
	
		Pageable page = vo.makePageable(0, "bno");
		
		log.info(""+page);
		
	}

수정된 내용을 저장하고 프로젝트를 실행합니다.(http://localhost:8080/boards/list?page=2&size=15)

브라우저에서 전달되는 파라미터들은 자동으로 PageVO로 처리됩니다. 이때 페이지 번호(page)와 페이지당 개수(size)가 처리되고, 정렬 방향과 정렬 대상 칼럼은 컨트롤러에서 처리됩니다.

 

위의 테스트 결과와 같이 브라우저에서 'http://localhost:8080/boards/list?page=2&size=15'와 같은 형태로 호출하게 되면 서버의 내부에서는 페이지 번호가 자동으로 1이 감소된 형태로 Pageable 타입의 객체를 사용할 수 있게 됩니다.

 

2.3 Repository와의 연동 처리

컨트롤러에서 Pageable 타입의 객체에 대한 처리가 끝났다면 WebBoardRepository와의 연동작업을 처리하도록 합니다.

 

org.zerock.controller.WebBoardController.java

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/boards/")
@Log
public class WebBoardController {
	
	@Autowired
	private WebBoardRepository repo;
	
	@GetMapping("/list")
	public void list(PageVO vo) {
	
		Pageable page = vo.makePageable(0, "bno");
		
		Page<WebBoard> result = repo.findAll(repo.makePredicate(null, null), page);
		
		log.info(""+page);
		log.info(""+result);
		
		result.getContent().forEach(board->System.out.println(board));
	}
}

 

다음 글에서는 콘솔로 출력하던 게시물 및 페이지 번호를 리액트로 구현하겠습니다. SpringBoot의 페이징 처리는 이후부터는 웹 페이지로 출력해야지 진행할 수 있기 때문에 우선 리액트로 웹 페이지를 구현하는 것을 목표로 합니다.

 

References

source code : https://github.com/yonghwankim-dev/SpringBoot-Study
스타트 스프링 부트, 구멍가게코딩단 지음