ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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 프로세스

    1. COUNT Query를 날리기 위한 countMappedStatement 생성
    2. countMappedStatement를 이용하여 totalCount 구하기 (returnType은 List여서 Casting 후 0번째 값 get)
    3. listMappedStatement를 이용하여 list 구하기
    4. 구한 값으로 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 메소드 프로세스

    1. totalCount 변수타입인 Long을 반환받도록 ResultMaps 생성
    2. 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"; 
    1. configuration은 동일
    2. id 값은 + COUNT_ID_SUFFIX 만 붙여서 생성
    3. type (Query결과를 return받을 Type) 은 Long.class 설정
    4. resultMappings는 Query결과를 Mapping시킬 Type들을 추가를 위해 필요하나 totalCount를 구할 때는 필요가 없어서 빈 ArrayList만 생성

    결론

    • QueryInterceptor가 하는 일은 Query를 2번 날리도록 하는 역할이다.
      1. totalCount Query ( returnType List )
      2. list Query ( returnType List )

    totalCount를 List형태로 반환받아 get(0)한 이유는 @Mapper에 등록된 @Select메소드의 returnType이 List로 이미 등록되어 버려, Mybatis내부 로직에서 List로 처리하는 selectList 메소드를 타버렸기 때문이다.

    위는 userMapper.selectUserList 가 실행되었을 때 Mybatis가 returnType을 결정하는 로직이다.

    PagableResponse가 List 를 구현하였기 때문에 returnsManytrue로 된다.

    MapperProxy에 의해 MapperMethod가 실행될 때를 보면 returnsManytrue이기 때문에 executeForMany 메소드를 타게 된다.

    실행되고있는 method에 RowBounds를 상속 → PageInfo를 상속 → UserSearch가 있기 때문에 rowBounds와 함께selectList가 실행되어진다.

    위의 이미지에서 볼 수 있듯이 앞으로 sqlSession으로 진행되는 모든 로직은 return 값이 List이기 때문에, 강제로 totalCount를 구하기위해 MappedStatement를 바꿔도 return은 List 이기 때문에 get(0)을 하여 처리하였다.


    PreparetInterceptor는 2편에서 작성하겠다.

    Uploaded by Notion2Tistory v1.1.0

    댓글

Designed by Tistory.