Spring + mybatis + PageHelper
- -
pageHelper
웹 페이지를 만들면서 페이징 처리는 반드시 있어야 하는 내용이다. 하지만 DB마다 다른 쿼리를 사용해야하는 점이나 전체 페이지, 현재 페이지, 페이지당 데이터 수 등을 계산하기가 쉽지않다.
JPA에는 별도로 Paging 관련 기능이 있는데 MyBatis에서는 관련 기능을 찾지 못하다 최근에 PageHelper라는 녀석이 눈에 띄어서 포스팅해본다.
https://github.com/pagehelper/Mybatis-PageHelper
maven dependency
스프링 부트 기반에서 사용한다면 다음의 dependency를 pom.xml에 추가한다.
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.13</version>
</dependency>
만약 legacy 기반에서 사용한다면 다음의 dependency를 pom.xml에 추가한다.
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.11</version>
</dependency>
환경 설정
스프링 부트 기반에서는 application.properties에서 pagehelper에 대한 설정을 작성한다.
# 사용할 DB 설정
pagehelper.helper-dialect=mysql
# 범위를 넘어가는 pageNum가 들어올 때 가능할 값으로 변경
pagehelper.reasonable=true
helper-dialect는 사용하려는 db를 적어주면 되는데 oracle, mysql, mariadb, sqlite, hsqldb, postgresql, db2, sqlserver, informix, h2, sqlserver2012, derby 등 현존하는 대부분의 DB들이 사용 가능하다.
reasonable은 pageNum을 가능한 값으로 변경해주는데 만약 pageNum이 0이하의 값이 들어오면 1로 변경해주고 최대 페이지를 넘어가는 값이 들어온다면 최대 페이지로 변경해준다.
만약 legacy 환경이라면 SqlSessionFactory를 생성할 때 PageInterceptor를 plugin으로 설정한다.
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource ds) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setConfigLocation(new ClassPathResource("mybatis/mybatis-config.xml"));
PageInterceptor pi = new PageInterceptor();
Properties props = new Properties();
props.put("helperDialect" , "mysql");
props.put("reasonable" , "true"); // 문자열
pi.setProperties(props);
factoryBean.setPlugins(pi);
return factoryBean.getObject();
}
mapper.xml
<select id="selectAll" resultMap="countryBase">
select * from country
</select>
mapper.xml의 sql 문장에는 페이징과 관련된 내용이 전혀 존재하지 않는다. 하지만 나중에 쿼리가 실행되는 시점에 helper-dialect에 의거해서 페이징과 관련된 쿼리가 추가된다.
mapper interface
import com.github.pagehelper.Page;
Page<Country> selectAll();
selectAll의 리턴 타입은 Page 객체인데 이녀석은 List의 자식 클래스이다. 따라서 여러 Country 객체를 받아줄 수 있다.
단위테스트
@Test
public void selectAllPaging() {
int perPage = 10;
// 몇 페이지에 대한 조회인지 설정 후 조회
PageHelper.startPage(1, perPage);
Page<Country> p = cRepo.selectAll();
log.trace("1 page: {}", p);
PageHelper.startPage(2, perPage);
p = cRepo.selectAll();
log.trace("2 page: {}", p);
}
페이지를 지정할 때는 PageHelper의 static 메서드인 startPage를 사용한다 파라미터로는 조회하고 싶은 페이지 번호, 페이지당 보여줄 아이탬의 개수를 넘겨주면 된다.
실제 동작하는 쿼리들을 모아보면 아래와 같다.
# 전체 데이터 개수 조회
Preparing: SELECT count(0) FROM country
# 1페이지에 해당하는 자료 조회 - 0번 부터 10개
Preparing: select * from country LIMIT ?
Parameters: 10(Integer)
# 전체 데이터 개수 조회
Preparing: SELECT count(0) FROM country
#2페이지에 해당하는 자료 조회- 10개부터 10개
Preparing: select * from country LIMIT ?, ?
Parameters: 10(Integer), 10(Integer)
이렇게 매 쿼리 시마다 전체 개수를 조회해서 전체 페이지 정보를 계산하고 필요한 페이지의 내용을 조회하는 방식이다.
Page 객체를 들여다보면 페이징에서 필요한 여러 정보를 확인할 수 있다.
log.trace("전체 데이터: {}", p.getTotal());
log.trace("전체 페이지: {}", p.getPages());
log.trace("현재 페이지: {}", p.getPageNum());
log.trace("페이지당 데이터: {}", p.getPageSize());
List<Country> result = p.getResult();
for (Country country : result) {
log.trace("country: {}", country);
}
이처럼 PageHelper라는 플러그인을 사용하면 기존의 코드를 전혀 건드리지 않고 페이징에 관련된 내용을 처리할 수 있다. 실로 엄청난 유지보수성이 아닐 수 없다.
controller와의 연결
Controller 와의 연결
이제 Controller에서 Service를 통해서 페이징 요청을 처리해보자.
// list로 요청이 오면.. 도서 목록을 request 에 담아 반환하자. 결과 페이지는 list
@GetMapping("/list")
public String list(Model model, @RequestParam(required = false) Integer page,
HttpServletRequest req) {
if(page==null) {
page = 1;
}
PageHelper.startPage(page, 10);
// business logic
Page<Country> countries = service.search();
String path = req.getContextPath()+"/countries/list?page";
PageNavigationForPageHelper helper = new PageNavigationForPageHelper(countries, path);
model.addAttribute("countries", helper);
return "country/list";
}
이제 요청에서는 page를 파라미터로 받아서 해당 페이지에 대한 정보만을 반환한다. 이를 위해 PageHelper의 startPage 메서드를 사용한 후 service를 호출한다.
service가 동작한 후는 조회 결과와 path 정보를 PageNavigationForPageHelper에게 넘겨주는데 이 녀석은 페이징 정보를 저장하는 객체이다. 최종적으로 이 helper를 model에 담아서 view를 연결하면 끝이다.
Interceptor의 활용
그런데 만약 위와 같은 페이징을 처리해야 하는 곳이 여러곳이라면 어떨까? 아마 페이지를 설정하는 부분과 helper를 생성하는 과정은 언제나 동일하고 service를 호출하는 부분만 달라질 것이다.
Controller에서 이런 전/후 처리를 위해서 사용되는 것은? 바로 Interceptor 이다.
이제 service를 호출하기 전인 페이지 설정은 preHandle에서, service가 동작한 후의 처리는 postHandle에서 처리해주면 된다.
@Component
public class PagingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String page = request.getParameter("page");
if(page==null) {
page = "1";
}
PageHelper.startPage(Integer.parseInt(page), 10);
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
Page<?> datas = (Page<?>) modelAndView.getModel().get("datas");
String path = request.getContextPath() + request.getServletPath() + "?page";
PageNavigationForPageHelper pageInfo = new PageNavigationForPageHelper(datas, path);
modelAndView.addObject("pageInfo", pageInfo);
}
}
PagingInterceptor는 /list라는 요청이 발생하면 동작하도록 설정해주자.
@Autowired
PagingInterceptor pageI;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(pageI).addPathPatterns("/**/list");
}
이제 페이징과 관련된 처리는 Interceptor에서 진행하기 때문에 Controller에서는 그런 일을 할 필요가 없어졌다.
public String list(Model model) {
// interceptor의 preahandle에서 page 설정 완성
// business logic 처리
Page<Country> countries = service.search();
// 정보를 model에 담아줌 - 조회된 페이지 정보
model.addAttribute("datas", countries);
return "country/list";
}
'MyBatis' 카테고리의 다른 글
[MyBatis] 05. 기타 (0) | 2023.06.18 |
---|---|
[MyBatis] 04. 동적 쿼리 (0) | 2023.06.18 |
[MyBatis] 03. 조회 결과의 매핑 (0) | 2023.06.18 |
[MyBatis] 02. CRUD (0) | 2023.06.18 |
[MyBatis] 01. 소개 및 환경 설정 (2) | 2023.06.18 |
소중한 공감 감사합니다