-
Mybatis Interceptor 자동 Paging처리 만들기(1)개발/Spring Boot 2021. 4. 19. 17:09
Github(https://github.com/rldhks8745/spring-boot-practice)
Notion(https://www.notion.so/Mybatis-Interceptor-Paging-35a3e8b167ad47eca25acb56f1fb9795)
- Notion Base로 작성된 글이기 때문에 Notion으로 보시기를 추천드립니다.
Mybatis Interceptor 자동 Paging처리 만들기(2) 바로가기
♣️미리보기
- H2 Database Table Data
- HTTP GET 통신
- return 값
{ "list": [ { "num": 1, "id": "test1", "pw": "test1", "name": "테스트1" }, { "num": 2, "id": "test2", "pw": "test2", "name": "테스트2" } ], "pageInfo": { "page": 1, "size": 2, "totalCount": 3 } }
♣️개요
위에 미리보기를 확인해보면 page, size를 보내게 되면 list, pageInfo를 담고있는 JSON을 반환 받도록 자동으로 구성해 볼것이다.
⚫Mapper
@Mapper public interface UserMapper { @Select(value = """ <script> SELECT * FROM USER WHERE 1 = 1 <if test='id != null and !id.equals("")'> AND ID LIKE '%${id}%' </if> </script> """) public PagableResponse<User> selectUserList(UserSearch userRequest); }
눈여겨 보아야할 것은 매개변수, 리턴타입 이다.
매개변수
@Data @EqualsAndHashCode(callSuper = false) public class UserSearch extends PageInfo { private String id; private String name; }
PageInfo 를 상속받고 있다.
@Data @EqualsAndHashCode(callSuper = false) @JsonIgnoreProperties({"offset", "limit"}) public class PageInfo extends RowBounds { protected Integer page; @Min(1) protected Integer size; protected Long totalCount = -1L; // set이 되지않았다는 의미로 -1 }
RowBounds를 상속받고 있는데 이는 Mybatis에서 페이징처리시 사용하는 클래스이다.
하지만 나는 PageInfo를 상속받은 클래스를 Mybatis interceptor 안에서 사용하기 위해서 상속받아 놓았다. 실제로 사용하지는 않는다.
리턴타입
@Data @JsonFormat(shape = Shape.OBJECT) public class PagableResponse<T> implements List<T> { private List<T> list; private PageInfo pageInfo; public PagableResponse() { list = new ArrayList<>(); pageInfo = new PageInfo(); } /* * 아래는 List Method Override */ @Override public int size() { return list.size(); } . . . }
List Interface를 구현하여 Mybatis에서 PagableResponse 클래스를 Collection 타입으로 인식하도록 하였다.
모든 List Interface의 메소드는 멤버변수 list 를 사용
또한 @JsonFormat(shape = Shape.OBJECT) 어노테이션으로 return될 때 list만 반환되는 것이 아닌 Object로 멤버변수 전체를 반환시키도록 설정해놓았다.
⚫Interceptors
@Configuration public class MybatisConfig { @Bean public PreparetInterceptor preparetInterceptor() { return new PreparetInterceptor(); } @Bean public QueryInterceptor queryInterceptor() { return new QueryInterceptor(); } }
Paging 자동처리를 위해 직접만든 Interceptor 2개 (PreparetInterceptor , QueryInterceptor) 를 Bean으로 등록
각 클래스에 @Component를 사용해서 등록하니 Interceptor를 구현할 때 필요로하는 @Intercepts 어노테이션을 못찾아서 @Bean으로 등록했다.
QueryInterceptor
@Override public Object intercept(Invocation invocation) throws Throwable { try { PageInfo pageInfo = (PageInfo) invocation.getArgs()[2]; log.debug("■■ QueryInterceptor intercept: Request Parameter가 PageInfo.class를 상속 ■■"); MappedStatement listMappedStatement = (MappedStatement) invocation.getArgs()[0]; MappedStatement countMappedStatement = createCountMappedStatement(listMappedStatement); // COUNT 구하기 invocation.getArgs()[0] = countMappedStatement; List<Long> totalCount = (List<Long>) invocation.proceed(); pageInfo.setTotalCount((Long) totalCount.get(0)); // LIST 구하기 invocation.getArgs()[0] = listMappedStatement; List<Object> list = (List<Object>) invocation.proceed(); return createPagableResponse(list, pageInfo); } catch (ClassCastException e) {} return invocation.proceed(); }
QueryInterceptor 프로세스
- COUNT Query를 날리기 위한 countMappedStatement 생성
- countMappedStatement를 이용하여 totalCount 구하기 (returnType은 List여서 Casting 후 0번째 값 get)
- listMappedStatement를 이용하여 list 구하기
- 구한 값으로 PagableResponse 반환
PageInfo를 상속받은 매개변수가 없으면 try, catch에 걸리기 때문에 기존 Query 그대로 진행
/** * @Method : createCountMappedStatement * @CreateDate : 2021. 4. 19. * @param ms * @return * @Description : COUNT QUERY 결과를 받기위한 MappedStatement 생성 * 속도문제로 개선필요 시 간단히 Map으로 캐싱처리해도 될듯 */ private MappedStatement createCountMappedStatement(MappedStatement ms) { List<ResultMap> countResultMaps = createCountResultMaps(ms); return new MappedStatement.Builder(ms.getConfiguration(), ms.getId() + COUNT_ID_SUFFIX, ms.getSqlSource(), ms.getSqlCommandType()) .resource(ms.getResource()) .parameterMap(ms.getParameterMap()) .resultMaps(countResultMaps) .fetchSize(ms.getFetchSize()) .timeout(ms.getTimeout()) .statementType(ms.getStatementType()) .resultSetType(ms.getResultSetType()) .cache(ms.getCache()) .flushCacheRequired(ms.isFlushCacheRequired()) .useCache(true) .resultOrdered(ms.isResultOrdered()) .keyGenerator(ms.getKeyGenerator()) .keyColumn(ms.getKeyColumns() != null ? String.join(",", ms.getKeyColumns()) : null) .keyProperty(ms.getKeyProperties() != null ? String.join(",", ms.getKeyProperties()): null) .databaseId(ms.getDatabaseId()) .lang(ms.getLang()) .resultSets(ms.getResultSets() != null ? String.join(",", ms.getResultSets()): null) .build(); }
createCountMappedStatement 메소드 프로세스
- totalCount 변수타입인 Long을 반환받도록 ResultMaps 생성
- MappedStatement 생성 ( 다른것은 전부 동일, resultMaps만 변경 )
/** * @Method : createCountResultMaps * @CreateDate : 2021. 4. 19. * @param ms * @return * @Description : COUNT QUERY 결과를 받기위한 ResultMaps 생성 */ private List<ResultMap> createCountResultMaps(MappedStatement ms) { List<ResultMap> countResultMaps = new ArrayList<>(); ResultMap countResultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId() + COUNT_ID_SUFFIX, Long.class, new ArrayList<>()) .build(); countResultMaps.add(countResultMap); return countResultMaps; }
org.apache.ibatis.mapping.ResultMap.Builder.Builder( Configuration configuration, String id, Class<?> type, List<ResultMapping> resultMappings )
private static String COUNT_ID_SUFFIX = "-Long";
- configuration은 동일
- id 값은 + COUNT_ID_SUFFIX 만 붙여서 생성
- type (Query결과를 return받을 Type) 은 Long.class 설정
- resultMappings는 Query결과를 Mapping시킬 Type들을 추가를 위해 필요하나 totalCount를 구할 때는 필요가 없어서 빈 ArrayList만 생성
결론
- QueryInterceptor가 하는 일은 Query를 2번 날리도록 하는 역할이다.
- totalCount Query ( returnType List )
- list Query ( returnType List )
totalCount를 List형태로 반환받아 get(0)한 이유는 @Mapper에 등록된 @Select메소드의 returnType이 List로 이미 등록되어 버려, Mybatis내부 로직에서 List로 처리하는 selectList 메소드를 타버렸기 때문이다.
위는 userMapper.selectUserList 가 실행되었을 때 Mybatis가 returnType을 결정하는 로직이다.
PagableResponse가 List 를 구현하였기 때문에 returnsMany가 true로 된다.
MapperProxy에 의해 MapperMethod가 실행될 때를 보면 returnsMany 가 true이기 때문에 executeForMany 메소드를 타게 된다.
실행되고있는 method에 RowBounds를 상속 → PageInfo를 상속 → UserSearch가 있기 때문에 rowBounds와 함께selectList가 실행되어진다.
위의 이미지에서 볼 수 있듯이 앞으로 sqlSession으로 진행되는 모든 로직은 return 값이 List이기 때문에, 강제로 totalCount를 구하기위해 MappedStatement를 바꿔도 return은 List 이기 때문에 get(0)을 하여 처리하였다.
PreparetInterceptor는 2편에서 작성하겠다.
'개발 > Spring Boot' 카테고리의 다른 글
[QueryDsl] QEntity 연관관계 초기화 2Depth 이상 적용시키기 (0) 2021.10.01 Mybatis Interceptor 자동 Paging처리 사용방법 (2) 2021.04.24 Mybatis Interceptor 자동 Paging처리 만들기(2) (0) 2021.04.20 @ConfigurationProperties, @EnableConfigurationProperties (0) 2020.05.15