iBatis 3.0 Beta 10 이 새로 나와서 TDD 방식으로 기능을 익혀본 내용을 아래와 같이 간략하게(?) 정리해 보았다. 여기서는 간단한 PersonBean 클래스를 담당, 관리하는 PersonBeanMapper 클래스 작성을 통해서 iBatis 3.0의 기능을 익혀봤다.


1. bean class 를 만들고 테스트.

초간단 PersonBean 클래스는 property가 4개뿐이다.
나중에 Pooling 기능이 제대로 작동되도록 equals 메소드와 hashCode 메소드 구현 후 테스트를 해본다.

대략 통과

그리 복잡한 부분이 아니므로 금방 통과된다.

기분 좋은


2. PersonBeanMapper 인터페이스 작성

bean class 를 담당할 PersonBeanMapper 인터페이스 작성한다. 2.x에서는 관행처럼 이 구현체를 사용자가 직접 코딩했었는데 ibatis3 에서 프록시 인스턴스를 제공하므로 따로 구현하지 않아도 된다.(대신에 몇가지 제약 조건이 추가되었는데 아래에서 언급함)

3. Mapped Statement XML 파일 작성

PersonBeanMapper 의 각 메소드가 호출될 때 실제 수행될 쿼리를 담고 있는 "PersonMap.xml" 파일을 작성한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN" "http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<mapper namespace="ibatis3.mapper.PersonBeanMapper">
</mapper>
 mapper 노드에서 namespace 값은 반드시 mapper 인터페이스의 full qualified name 을 적어야 한다. 오타가 나거나 제대로 명시하지 않으면 ibatis 기동 후

session.getMapper(PersonBeanMapper.class)

호출 시 주어진 인터페이스 파일에 대응되는 mapper 가 없다고 예외가 던져진다.

4. iBATIS 초기화 테스트

iBATIS를 본격적으로 사용하기에 앞서 xml 파일들이 제대로 로드되어 초기화가 되는지 확인을 한다.

실행해보면 아래와 같이 테스트가 실패한다.
java.io.IOException: Could not find resource ibatis/ibatis3-config.xml
    at org.apache.ibatis.io.Resources.getResourceAsStream(Resources.java:89)
    at org.apache.ibatis.io.Resources.getResourceAsStream(Resources.java:76)
    at org.apache.ibatis.io.Resources.getResourceAsReader(Resources.java:134)
    at ibatis.Test_ibatis.setUp(Test_ibatis.java:16)
ibatis3-config.xml 파일이 없으니 당연히 던져지는 예외. 설정 정보를 저장할 ibatis3-config.xml 파일을 만들고 다시 시도해 본다. 이 때 설정 파일은 자바 소스 파일과 같이 src 폴더 밑에 만드는 것이 일반적인 관행(?)인듯 하다.

설정 파일을 생성한 후 DOCTYPE을 적어준다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//ibatis.apache.org//DTD Config 3.0//EN"
    "http://ibatis.apache.org/dtd/ibatis-3-config.dtd">

DOCTYPE 이 있어야 ibatis 프레임워크에 내장된 xml 파서가 설정 문서를 분석해서  Configuration 인스턴스를 만들 수 있다.  문서를 참조해서 다음과 같이 아무것도 없는 <configuration>..</configuration> 최상위 엘레먼트만 추가해보고 다시 테스트 해본다.
org.apache.ibatis.session.SessionException: Configuration does not include an environment with a DataSource, so session cannot be created unless a connection is passed in.
    at org.apache.ibatis.session.defaults.DefaultSqlSessionFactory.getDataSourceFromEnvironment
(DefaultSqlSessionFactory.java:112)
    at org.apache.ibatis.session.defaults.DefaultSqlSessionFactory.openSessionFromDataSource
(DefaultSqlSessionFactory.java:70)
    at org.apache.ibatis.session.defaults.DefaultSqlSessionFactory.openSession(DefaultSqlSessionFactory.java:32)
    at ibatis3.Test_ibatis.setUp(Test_ibatis.java:20)
역시 실패. 안에 아무것도 없어서 할 게 없다는 뜻이니 뭘 좀 채워넣어야 할 듯..
다음과 같이 envirnments (복수형!!) 노드를 하나 만든다.
<configuration>
    <environments default="ibatis-test">
        <environment id="ibatis-test">
            <transactionManager type="JDBC"/>
            <dataSource type="UNPOOLED">
                <property name="driver" value="org.apache.derby.jdbc.EmbeddedDriver"/>
                <property name="url" value="jdbc:derby:testDB;create=true"/>
                <property name="username" value=""/>
                <property name="password" value=""/>
            </dataSource>
        </environment>
    </environments>
</configuration>
environments 노드 아래에 여러개의 개별적인 environment 노드가 존재하는데 개발 환경, 배포환경, 실제 가동 환경 등 상황에 맞게 <environment/> 들을 만들어두고 ibatis 설정 파일에서는 environments 노드의 default 값만 바꿔서 기민한 개발 환경 전환이 가능하다.

지금은 테스트 중이니 테스트 환경만 하나 만들어 두면 충분하다.

그리고 테스트가 드디어 통과

초기화는 성공.

이것으로 ibatis 가 제대로 가동되는 것을 확인했으니 이제 person mapper 를 가져오는 테스트 코드를 추가하고 테스트 실행해본다.
역시 무참히 실패한다. 예외 메세지는 다음과 같은데...
org.apache.ibatis.binding.BindingException: Type interface ibatis3.mapper.PersonBeanMapper is not known to the MapperRegistry.
    at org.apache.ibatis.binding.MapperRegistry.getMapper(MapperRegistry.java:21)
    at org.apache.ibatis.session.Configuration.getMapper(Configuration.java:358)
    at  org.apache.ibatis.session.defaults.DefaultSqlSession.getMapper(DefaultSqlSession.java:160)
    at ibatis3.Test_ibatis.test_mapper_loading(Test_ibatis.java:31)
session.getMapper(...) 로 호출했을때 mapper 인터페이스 정보를 찾지 못했다는 뜻이다. 따라서 아래와 같이 ibatis3-config.xml 에 mapper 정보를 등록해준다.
<configuration>
    <mappers>
        <mapper resource="ibatis3/mapper/PersonMap.xml"/>
    </mappers>
<configuration>
드디어 테스트가 통과한다. 이렇게 해서 mapper 등록도 제대로 되었음을 확인했다.

5. DB 연결
DB 연결을 테스트에 포함하려면

* DB에 값을 넣고
* 조작하고 테스트
* 다시 DB에 값을 빼내서 이전 상태로 초기화

하는 setup, teardown 메소드를 구현해야한다. 이를 위해서 DBUnit 이라는 별도의 DB 테스트 프레임워크가 있으나 여기서는 PersonBeanMapper 만 테스트 할 것이므로 굳이 쓸 필요는 없는 듯 하다.(배보다 배꼽이 더 커진다.)

아래와 같이 기존의 setup 메소드에 테이블을 초기화하는 코드를 작성해넣는다.

테스트 해서 성공하는 것을 확인한다.

위에서 session 으로부터 connection 인스턴스를 하나 빼냈는데, 이 값은 현재의 session 인스턴스에서 사용하는 것이므로 테스트 코드 중간에서 conn.close(); 를 호출하면 session 자체가 닫히는 효과를 가져온다.

따라서 최종적으로 테스트를 마쳤을 때에만 session.close() 를 호출해서 세션을 정리하도록 한다.

위와같이 테스트 코드 내에 db 코드를 넣는게 지저분해 보인다면 데이터 베이스를 초기화하는 별도의 mapper 를 만들어서 테이블 초기화, 샘플 코드 입력, 테이블삭제를 수행할  mapped statement를 정의해두는 것도 생각해 볼 수 있다.

show >>

하지만 여기서는 PersonBeanMapper 하나 테스트 하자고 저짓거리(?)를 하느니 그냥 고전적인 방식으로 입력하는게 나을 듯 하다. 테스트할 bean과 mapper가 많다면 한 번 고려해볼 만 하다.

그리고 ibatis3 에서는 공식적으로 DDL을 지원하지 않는듯 하다.

MappedMethod 의 구현 내용

select, insert, update, delete 가 아니면 예외를 던지게끔 구현되어있다.

2.x에서 쓰이던 <statement> 노드도 없어졌으니 위와같이 update 나 insert 노드에서 DDL을 입력하면 수행할 수 있기는 하다. (하지만 Derby 에서 테스트 한 코드이고 다른 DB에서도 제대로 작동할지는-작동할 것 같은데 (-_- )a- 확인을 못했다. 해보신 분들 리플이라도 하나씩 달아주시면 매우 감사하겠습니다^^)

6. Mapper 구현 시작

여기서 살짝 리팩토링을 하자면, ibatis가 제대로 초기화되고 PersonBeanMapper를 테스트 하므로 mapper를 프로퍼티로 올려붙여서 테스트 코드를 더 깔끔하게 만들 수 있다.
테스트하니 selectPerson에 대응되는 mapped statement가 없기때문에 실패한다.
java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for

ibatis3.mapper.PersonBeanMapper.selectPerson
    at org.apache.ibatis.session.Configuration$StrictMap.get(Configuration.java:412)
    at org.apache.ibatis.session.Configuration.getMappedStatement(Configuration.java:346)
    at org.apache.ibatis.binding.MapperMethod.setupCommandType(MapperMethod.java:137)
    at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:46)
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:34)
    at $Proxy1.selectPerson(Unknown Source)
    at ibatis3.Test_ibatis.test_mapper_loading(Test_ibatis.java:71)
다시 PersonMap.xml 로 가서 id로 사용자를 읽어오는 statement 를 작성한다.
    <select id="selectPerson" parameterType="string" resultType="ibatis3.bean.PersonBean">
    select
        personId ,
        personFName as "firstName",
        personLName as "lastName",
        personAge as "age"
    from Persons
    where personId = #{value}
    </select>
ibatis3 문서 13페이지에 보면 기본적인 데이터 타입에 대한 키값과 맵핑값을 구현해 놓았다. PersonMapper.selectPerson 이 String 값을 파라미터로 받기 때문에 PersonMap.xml 에서는 parameterType을 "string" 으로 지정하면 된다. 결과값은 PersonBean 이므로 fqn 을 지정해준다.

마찬가지로 id 값인 "selectPerson" 도 정확하게 PersonBeanMapper의 selectPerson 메소드 이름과 일치해야 한다.

iBatis 2.x 에서 사용자가 직접 구현해야했던 Mapper 클래스는 이렇게 mapper 인터페이스의 타입과 여기에 대응하는 mapped statement 의 이름을 가지고 자동으로 연결되기 때문에 설정 값을 정확히 하지 않으면 아래와 같이 에러가 발생한다.
java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for
ibatis3.mapper.PersonBeanMapper.selectPerson
    at org.apache.ibatis.session.Configuration$StrictMap.get(Configuration.java:412)
    at org.apache.ibatis.session.Configuration.getMappedStatement(Configuration.java:346)
    at org.apache.ibatis.binding.MapperMethod.setupCommandType(MapperMethod.java:137)
    at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:46)
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:34)
    at $Proxy1.selectPerson(Unknown Source)
    at ibatis3.Test_ibatis.test_mapper_loading(Test_ibatis.java:72)
호출한 mapper 인터페이스의 selectPerson 메소드 이름을 키값으로 ( ibatis3.mapper.PersonBeanMapper.selectPerson ) 등록된 statement 를 찾았지만 없다는 예외 메세지이다.

7. 기능 추가

먼저 아래와 같이 테스트 코드를 추가하고...
테스트를 돌려보면 당연히 에러가 나온다. insertPerson에 대응되는 statement를 정의하지 않았기 때문이다. 여기서 중요한 것은 내가 예상한 예외가 던져졌는지 확인함으로써 그 다음 단계로 나아갈 수 있다는 점이다.

예외를 확인하지 않고 곧바로 PersonMap.xml로 가서 관련된 쿼리를 짜 넣고 테스트하면 예외 발생 지점이 두군데로 늘어나게 된다.(작성한 쿼리에 문제가 있을 경우 또는 쿼리 수행 전에 문제가 있었을 경우)

이렇게 예외 지점이 넓어지면 여기저기 들쑤시고 다니느라 시간을 낭비하게 되므로 메소드 한 줄 추가하고 테스트해서 예상했던 예외가 나오는지 확인하고 그 다음 단계로 넘어가는게 더 확실하다.(예외의 범위를 좁혀나간다)

다음과 같이 쿼리를 입력하고 다시 테스트 하면 성공한다.
show >>


8. 리팩토링
xml 파일을 보면 "ibatis3.bean.PersonBean" 이 빈번하게 사용된다.
    ...
    <insert id="insertPerson" parameterType="ibatis3.bean.PersonBean" > ... </insert>
    <select id="selectPerson" parameterType="string" resultType="ibatis3.bean.PersonBean">
        ...
    </select>
    ...
이걸 그냥 둘게 아니라 ibatis 에서 제공하는 typeAlias 로 등록해서 좀 더 간단하게 만들 수가 있다.
<configuration>
    ....
    <!-- 별칭을 추가해주자 -->
    <typeAliases>
        <typeAlias type="ibatis3.bean.PersonBean" alias="person"/>
    </typeAliases>
    <environments> ...</eonvironments>
</configuration>

그러면 PersonMapper.xml는 아래와 같이 간단해진다.
    ...
    <insert id="insertPerson" parameterType="person" > ... </insert>
    <select id="selectPerson" parameterType="string" resultType="person">
        ...
    </select>
    ...
나중에 PersonBean의 이름을 바꾸거나 패키지 경로를 바꾸더라도 type 애트리뷰트 값만 바꿔주면 된다.

그리고 현재의 테스트 환경에서 DB 접속 정보를 아래와 같이 치환하면 편리하다.
(( derby_test.properties 파일을 생성))
driver=org.apache.derby.jdbc.EmbeddedDriver
url=jdbc:derby:testDB;create=true
username=
password=

(( ibatis3-config.xml 파일에 properties 노드 추가 ))
<configuration>
    <properties resource="ibatis3/ibatis.properties"></properties><!-- 이부분 추가-->
    <typeAliases> ......... </typeAliases>
    <environments>
        <environment id="test">
            <transactionManager type="JDBC"/>
            <dataSource type="UNPOOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
        </dataSource>
    <environments>
<configuration>
등록된 properties에서 파일을 읽어서 값을 저장해놓고 치환 구문 (${..})이 등장하면 값일 바꿔치기 한다. 실행 환경을 변경할 때 properties의 resource 값만 바꿔주면 된다.


이렇게 delete, selectAll, selectByFirstName, selectByAge(int start, int end) 등의 메소드를 작성하고 테스트하고, 실패하면 수정하고 또 테스트하고... 이렇게 하나하나 기능을 구현해나가다보면 어느새(?) PersonBeanMapper 가 완성된 것을 확인할 수 있다.(자기가 해놓고도 신기함.)

TDD 방식은 iBatis3 처럼 새로 나온 프레임워크를 공부할때도 매우 유용하다.

설정부터 초기화 단계를 테스트 하면서 많은 정보를 습득할 수 있기 때문에 나중에 시간이 흘러서 기억이 잘 나지 않을 때에는 문서를 따로 찾아보지 않더라도 테스트 코드만 보고도 대번에 활용 방식을 익힐 수 있다.
Posted by yeori
,