티스토리 뷰

궁금증의 시작

Mybatis를 사용하고 있는 프로젝트 코드 중에 아래와 같은 코드를 보았습니다.

<select id="selectCount" resultType="int">
   ...
</select>
int selectCount();

resultType이 int로 primitiveType으로 지정되어 있습니다. ORM에서는 Result를 primitive type 보다는 객체를 사용하는 것으로 알고 있습니다.
Wrapper Class인 Integer로 반환한다면 NPE가 발생할 수도 있지 않을까 생각되었습니다.

테스트

그래서 먼저 null이 int로 형변환 된다면 어떤 결과가 발생할지 테스트 해 보았습니다.

@Test
public void Null을_int로_형변환_테스트() {
    int count = (Integer) null;
}
java.lang.NullPointerException   
at com.test.java.MapperTest.Null을_int로_형변환_테스트(MapperTest.java:69)   
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)   
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)   
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)   
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)   
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)   
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)   
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)   
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)   
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)   
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)   
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)   
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:254)   
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89)   
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)   
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)   
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)   
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)   
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)   
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)   
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)   
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)   
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:193)   
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)   
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:78)   
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:212)   
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:68)   
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)   
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)   
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)

NullPointException(이하 NPE)이 발생합니다.
그렇다면 Mybatis에서 resultType을 int로 지정할 경우 NPE는 발생하지 않을까요?
임의로 null을 반환하도록 쿼리를 작성 후 테스트 해 보았습니다.

<select id="selectCount" resultType="int">
    SELECT CD
    FROM TEST_TABLE
    WHERE 1=2
</select>

null을 임의로 반환하기 위해 where절 조건을 false가 되도록 작성하였습니다.

@Test
public void PrimitiveType_nullable_테스트() {
    int count = TestMapper.selectCount();
    assertEquals(“테스트로 count값은 0이어야 합니다."0, count);
}
2016-10-04 14:21:55 [DEBUG] [BaseJdbcLogger.java:139] - ==>  Preparing: SELECT CD FROM CD_MSTR WHERE 1=2
2016-10-04 14:21:55 [DEBUG] [BaseJdbcLogger.java:139] - ==> Parameters:
2016-10-04 14:21:55 [DEBUG] [BaseJdbcLogger.java:139] - <==      Total: 0
org.apache.ibatis.binding.BindingException: Mapper method ‘com.test.java.TestMapper.selectCount attempted to return null from a method with a primitive return type (int).   
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:74)   
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:52)   
at com.sun.proxy.$Proxy29.selectCount(Unknown Source)   
at com.nhncorp.umon.monitor.mappercommon.CdMasterMapperTest.PrimitiveType_nullable_테스트(MapperTest.java:74)   
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)   
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)   
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)   
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)   
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)   
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)   
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)   
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)   
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)   
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)   
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)   
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:254)   
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89)   
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)   
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)   
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)   
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)   
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)   
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)   
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)   
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)   
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:193)   
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)   
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:78)   
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:212)   
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:68)   
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)   
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)   
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)

NPE가 발생하지 않고 “Primitive Type(int)를 null로 반환하려고 한다”라고 오류 메시지와 함꼐 BindException이 발생합니다.
MapperMethod 클래스를 열어서 확인해 보았습니다.

public Object execute(SqlSession sqlSession, Object[] args) {
    Object param;
    Object result;
    if(SqlCommandType.INSERT == this.command.getType()) {
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
    } else if(SqlCommandType.UPDATE == this.command.getType()) {
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
    } else if(SqlCommandType.DELETE == this.command.getType()) {
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
    } else {
        if(SqlCommandType.SELECT != this.command.getType()) {
            throw new BindingException("Unknown execution method for: " this.command.getName());
        }

        if(this.method.returnsVoid() && this.method.hasResultHandler()) {
            this.executeWithResultHandler(sqlSession, args);
            result = null;
        } else if(this.method.returnsMany()) {
            result = this.executeForMany(sqlSession, args);
        } else if(this.method.returnsMap()) {
            result = this.executeForMap(sqlSession, args);
        } else {
            param = this.method.convertArgsToSqlCommandParam(args);
            result = sqlSession.selectOne(this.command.getName(), param);
        }
    }

    if(result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
        throw new BindingException("Mapper method \'this.command.getName() + " attempted to return null from a method with a primitive return type (" this.method.getReturnType() + ").");
    } else {
        return result;
    }
}

쿼리 수행시 호출되는 execute() 함수의 구현부입니다. 하단 5줄 코드를 확인해 보면 왜 NPE가 아닌 오류 메시지와 함께 BindException이 발생하는지 알 수 있습니다.
 result가 null이고 primitive type으로 RetureType을 지정했다면 BindException이 발생하도록 되어 있습니다.

그렇다면 resultType을 int로 지정하였는데, result값이 null일 수 있을까요?
이를 확인하기 위해서는 TypeHandler를 확인해야 합니다.

org.apache.ibatis.type 패키지에서 ResultType에 맞는 TypeHandler를 확인할 수 있습니다. 모든 TypeHandler의 부모 클래스인 BaseTypeHandler를 살펴보면 아래와 같습니다.

public T getResult(ResultSet rs, int columnIndex) throws SQLException {
    Object result = this.getNullableResult(rs, columnIndex);
    return rs.wasNull()?null:result;
}

쿼리의 결과(ResultSet)이 null이라면 null로 반환하고 있습니다. 

결론

Mybatis에서 쿼리 결과가 null일 수 있으며, primitive type을 resultType으로 지정할 경우 BindException 이 발생할 수 있습니다.

개인 의견

SQL 결과가 null인 케이스가 없다면 ReturnType을 Primitive를 사용해도 괜찮지만 null일 가능성이 있다면 primitive type보다는 클래스를 사용하는 것이 BindException을 방지할 수 있는 방법입니다. 예를들어 ReturnType이 int라면 Integer를 사용하는 것이 좋을 것 같습니다. 다만 Return 결과가 null일 수 있기 때문에 별도의 null 처리를 해 주어야 합니다.


'Programing > Java' 카테고리의 다른 글

Try~Catch~Finally 사용시 주의사항  (2) 2016.10.06
Java Exceptions Interview Questions, Answers  (0) 2016.10.05
Java Collection Framework  (0) 2016.09.30
논리 연산자(||, &&)와 비트 연산자(|, &)  (0) 2016.09.28
IBATIS CacheModel  (0) 2016.09.26
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함