[F-Lab 챌린지 42일차 TIL]

성수데브리·2023년 8월 8일
0

f-lab_java

목록 보기
33/73

MyBatis 3.x 쿼리 결과 맵핑하기

    <select id="findTicketGrades" parameterType="java.lang.Long"
            resultType="com.heeverse.ticket.domain.entity.TicketGrade">
        SELECT
            ticket_grade_id,
            grade,
            grade_name,
            seat_count,
            concert_id,
            create_datetime
        FROM
            ticket_grade
        WHERE
            concert_id = #{concertId}
    </select>

문제 상황

쿼리 결과를 List<TicketGrade> 로 맵핑하려고 했다. 그러나 결과는 리스트에 널이 담겨졌다.

[null, null, null, null, ....]

디버깅할 때 삽질을 하여 애먼 클래스를 뒤지느라 시간을 많이 소요했다.
그러던중 무심코 추가한 옵션이 생각이 났다.

mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true
    arg-name-based-constructor-auto-mapping: true // 이 부분!

이 옵션이 무엇이냐면

argNameBasedConstructorAutoMapping
When applying constructor auto-mapping, argument name is used to search the column to map instead of relying on the column order. (Since 3.5.10)
default : false

쿼리 결과를 객체로 맵핑할때 디폴트 동작 방식은 클래스 생성자의 매개변수 순서를 참고하여 컬럼과 맵핑한다. 즉, 매개변수가 선언된 순서와 동일한 순서의 컬럼과 매핑을 한다.

그래서 생성자 자동 매핑을 적용할 때 칼럼 순서가 아닌 칼럼 이름과 생성자 인수들의 이름을 기반으로 매핑하도록 저 옵션을 활성화했다.

당연히 생성자 파라미터 변수명과 컬럼명을 맵핑할 줄 알았는데 디버깅 결과 예상과 전혀 달랐다.

디버깅

쿼리 결과인 ResultSet 을 처리하는 DefaultResultSetHandler 을 확인해야한다.

public class DefaultResultSetHandler implements ResultSetHandler 

위에서 말한 옵션을 활성화 하면 아래 메서드를 통해 맵핑을 한다.

private boolean applyArgNameBasedConstructorAutoMapping(ResultSetWrapper rsw, ResultMap resultMap,
      String columnPrefix, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor,
      boolean foundValues) throws SQLException {
    List<String> missingArgs = null;
 // 원인 코드 시작
    Parameter[] params = constructor.getParameters();
    for (Parameter param : params) {
      boolean columnNotFound = true;
      Param paramAnno = param.getAnnotation(Param.class);
      String paramName = paramAnno == null ? param.getName() : paramAnno.value();
      for (String columnName : rsw.getColumnNames()) {
        if (columnMatchesParam(columnName, paramName, columnPrefix)) {
 // 원인 코드 끝
          Class<?> paramType = param.getType();
          TypeHandler<?> typeHandler = rsw.getTypeHandler(paramType, columnName);
          Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
          constructorArgTypes.add(paramType);
          constructorArgs.add(value);
          final String mapKey = resultMap.getId() + ":" + columnPrefix;
          if (!autoMappingsCache.containsKey(mapKey)) {
            MapUtil.computeIfAbsent(constructorAutoMappingColumns, mapKey, k -> new ArrayList<>()).add(columnName);
          }
          columnNotFound = false;
          foundValues = value != null || foundValues;
        }
      }
      if (columnNotFound) {
        if (missingArgs == null) {
          missingArgs = new ArrayList<>();
        }
        missingArgs.add(paramName);
      }
    }
    if (foundValues && constructorArgs.size() < params.length) {
      throw new ExecutorException(MessageFormat.format(
          "Constructor auto-mapping of ''{1}'' failed " + "because ''{0}'' were not found in the result set; "
              + "Available columns are ''{2}'' and mapUnderscoreToCamelCase is ''{3}''.",
          missingArgs, constructor, rsw.getColumnNames(), configuration.isMapUnderscoreToCamelCase()));
    }
    return foundValues;
  }

원인을 찾아낸 코드만 발췌해보겠다.

    Parameter[] params = constructor.getParameters();
    for (Parameter param : params) {
      boolean columnNotFound = true;
      Param paramAnno = param.getAnnotation(Param.class);
      String paramName = paramAnno == null ? param.getName() : paramAnno.value();
      for (String columnName : rsw.getColumnNames()) {
        if (columnMatchesParam(columnName, paramName, columnPrefix)) {
  • 클래스 생성자 객체에서 파라미터 배열을 가져온다.
  • 이 배열을 순회하면서 파라미터에 Param.class 어노테이션을 조회한다.
  • paramAnno 가 null 이 아니면 어노테이션 value에 선언한 이름을 가져오고, null 이면 Parameter 객체에서 변수명을 조회한다.

나는 생성자 파라미터에 @Param 을 선언 안했으니 Parameter 객체에 생성자 매개변수 이름이 어떻게 들어있는지 확인해봤다.

Parameter.getName() 메서드 내부 코드를 까보자

    /**
     * Returns the name of the parameter.  If the parameter's name is
     * {@linkplain #isNamePresent() present}, then this method returns
     * the name provided by the class file. Otherwise, this method
     * synthesizes a name of the form argN, where N is the index of
     * the parameter in the descriptor of the method which declares
     * the parameter.
     *
     * @return The name of the parameter, either provided by the class
     *         file or synthesized if the class file does not provide
     *         a name.
     */
    public String getName() {
        // Note: empty strings as parameter names are now outlawed.
        // The .isEmpty() is for compatibility with current JVM
        // behavior.  It may be removed at some point.
        if(name == null || name.isEmpty())
            return "arg" + index;
        else
            return name;
    }

분기문에서 첫번째 if 절을 충족해서 "arg" + index 로 이름이 생성되었다.
그 결과 arg0, arg1 과 같은 변수명과 테이블의 컬럼명을 비교하니 일치하는 이름을 찾지 못해 객체 생성자를 호출하지 못한것이고 그 결과 리스트에 null 만 잔뜩 담긴 것이다.

해결

  • 맵핑할 객체의 생성자 파라미터에 @Param 어노테이션으로 이름을 명시해줬다.

보완

  • 이 해결 방법은 모든 객체에 어노테이션을 선언해줘야 하는 문제가 남아있다.
    내일은 왜 Parameter 객체에 name 이 null 로 되었는지 찾아보고 일괄적으로 auto-mapping 할 수 있는 방안을 찾아봐야겠다.

0개의 댓글