내용 추가 : resultType을 entity, dto 등의 Value Object으로 지정할 경우 resultMap을 생성한다.

 

 

마이바티스와 롬복을 같이 사용하는 것은 처음이다보니 사소한 실수를 통해 여러 예외를 만나게 되는 것 같다.

User 도메인을 개발하기 시작했기에 엔티티를 작성하고 테스트 데이터베이스 스키마와 데이터를 추가한 뒤 MapperTest를 통해 조회 테스트를 진행하던 중 문제가 발생하였다.

Caused by: org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: java.lang.IndexOutOfBoundsException: Index 9 out of bounds for length 9
### The error may exist in file [C:\Users\serrl\Desktop\Mentoring\Somaeja\out\production\resources\mybatis\mapper\user.xml]
### The error may involve com.somaeja.user.mapper.UserMapper.findByAll
### The error occurred while handling results
### SQL: SELECT *         FROM USER
### Cause: java.lang.IndexOutOfBoundsException: Index 9 out of bounds for length 9
    at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:149)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:426)
    ... 82 more
Caused by: java.lang.IndexOutOfBoundsException: Index 9 out of bounds for length 9
    at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
    at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
    at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:248)
    at java.base/java.util.Objects.checkIndex(Objects.java:372)
    at java.base/java.util.ArrayList.get(ArrayList.java:458)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createUsingConstructor(DefaultResultSetHandler.java:708)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createByConstructorSignature(DefaultResultSetHandler.java:693)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject(DefaultResultSetHandler.java:657)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject(DefaultResultSetHandler.java:630)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:397)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:354)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:328)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:301)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:194)
    at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:65)
    at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)
    at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:63)
    at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:325)
    at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:89)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147)
    ... 88 more

난데없이 IndexOutOfBoundsException가 발생하여 (발생할 꺼리도 없는데????) 약간 헤매긴 하였는데 몇몇 요소를 검증하니 찾을 수 있었다.

 

문제는 바로 Entity 객체였다. (사실은 내 문제다..!)

@Builder
@Getter
@ToString
public class User {

    private final Long id;
    private final Long locationId;
    private final String nickName;
    private final String password;
    private final String email;
    private final String phoneNumber;
    // 계정 권한
    private final String role;
    private final LocalDateTime createdDate;
    private final LocalDateTime modifyDate;

    // Inner Join 을 통해 가져오는 데이터
  // 이것이 누락되었다..!
    private final String cityCountryTown;

}
<select id="findByAll" resultType="com.somaeja.user.entity.User">
    SELECT *
    FROM USER
</select>

Entity를 별도의 설정자 없이 빌더 패턴으로 생성하다보니 모든 필드 값을 final로 설정하였었는데, 테스트를 작성하기 전에 간단하게 작성했던 xml 에서 Join을 통해 가져올 데이터를 누락시키고 있었기에 해당 Entity 생성 로직 자체가 실패한 것이였다.

<select id="findByAll" resultType="com.somaeja.user.entity.User">
    SELECT USER.*, LOCATION.CITY_COUNTRY_TOWN as location
    FROM USER INNER JOIN LOCATION
    ON USER.LOCATION_ID = LOCATION.LOCATION_ID
</select>

이렇게 변경하여 해당 필드 데이터로 가져옴으로써 해결하게 되었다.

내부 로직을 보지않아 정확히 알 수는 없지만 요소가 빠짐으로써 발생하는 Exception이라면 IndexOutOfBoundsException이 아니라 IligalArgumentException을 사용하는 것이 더 좋지 않았을까 라는 생각이 들었다. (물론 내부 로직에 그럴만한 이유가 있겠지...)

 

다른 상황?

다른 문제로도 해당 예외가 발생할 수 있는데, 이는 resultType에서 객체를 사용할 때와 resultMap을 사용할 때 나타나는 경우이다.

 

 

예시 Entity

@Builder
@Getter
public class Account {

        private Long id;
    private String name;
    private String nickname;
    private String phoneNumber;
    private Boolean phoneVerified;

}

위와 비슷하게 빌더 패턴을 이용해서만 Entity를 생성하고 제공한다고 가정한다.

<resultMap id="AccountResultMap" type="....Account">
        <result property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="nickname" column="nickname"/>
    <result property="phoneNumber" column="phone_number"/>
    <result property="phoneVerified" column="phone_verified"/>
</resultMap>

<select id="findById" parameterType="long" resultMap="AccountResultMap">
        SELECT *
        FROM Account
        WHERE id = #{accountId}
</select>

이러한 resultMap을 정의하고 단순하게 id를 통해 조회하는 SELECT 쿼리를 작성하였다.

이 경우에도

Caused by: org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: java.lang.IndexOutOfBoundsException: Index 5 out of bounds for length 5
### The error may exist in file [mybatis\mapper\account.xml]
### The error may involve ...mapper.AccountMapper.findById
### The error occurred while handling results
### SQL: SELECT *         FROM Account         WHERE id = #{accountId}
### Cause: java.lang.IndexOutOfBoundsException: Index 5 out of bounds for length 5

이러한 에러가 발생할 것이다. 이는 resultMap의 특성 때문인데, 해당 기능을 사용할 경우 (+resultType 을 사용할 때 객체를 넘겨줄 경우 동일하다.) Mybatis가 미리 해당 인스턴스를 생성하게 된다. 하지만 위의 엔티티는 모든 인자가 포함된 생성자 (Builder) 만이 존재하기 때문에 인스턴스를 생성할 수 없어 문제가 발생하게 되는 것이다.

이 것을 해결하기 위해서는 인자가 없는 생성자를 추가하면 된다.

@Builder
@Getter
@NoArgsConstructor <-- 이것을 추가해주면 된다.
public class Account {

        private Long id;
    private String name;
    private String nickname;
    private String phoneNumber;
    private Boolean phoneVerified;

}

이렇게 한다면 앞서 언급한 특징에 의해 발생한 문제일 경우 해결될 것이다.

 

 

 

@Builder에 대한 토막글

Finally, applying @Builder to a class is as if you added @AllArgsConstructor(access = AccessLevel.PACKAGE) to the class and applied the @Builder annotation to this all-args-constructor. This only works if you haven't written any explicit constructors yourself.

 

참고 자료

+ Recent posts