새로운 테이블 생성 쿼리를 만들고 관련 POJO 클래스를 만들고 테스트를 통해서 기능을 채워나가는 간단한(?) 예제. DB는 오라클을 사용했기 때문에 <selectKey>로 시퀀스 값을 읽어들이는 부분도 포함된다.

블로그에서 글을 쓸 때 붙이는 "태그" 기능을 추가하는 과정을 TDD 방식으로 구현하는 과정을 기술함.

1. 테이블 쿼리 생성

새로 테이블을 정의한 쿼리를 작성한다.

Tags 테이블 생성 쿼리


isqlplus 에 접속해서 쿼리가 제대로 동작하는지 확인 후 쿼리 파일에 추가함.

제대로 동작함을 확인


2. POJO 클래스 작성

위에서 생성한 Tags 테이블에 대응하는 POJO 클래스를 정의한다.
equals와 hashCode는 기본적으로 구현하는게 좋음.

3. POJO를 관리할 Mapper 인터페이스 작성

인터페이스 안에 아무것도 선언하지 않는다.(나중에 테스트 하면서 하나씩 추가한다.)

4. Mapper 인터페이스를 테스트할 테스트 클래스 작성
테스트 코드 역시 아직까지는 아무 내용도 없다.

저 상태에서 테스트를 하면 예외가 발생하는데 "test" 로 시작하는 테스트 메소드가 하나도 없기 때문이다.

5. Mapper인터페이스의 쿼리를 정의한 Mapper.xml 생성

위에서는 DB와 POJO를 만들었고 이번에는 ibatis 관련 파일을 만든다.

적당한 위치에 TagMapper.xml 파일을 새로 만든 후 dtd 노드 구문을 정확히 삽입한다.
<?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">

6. ibatis-config.xml 에 <mapper .../> 추가

<mapper> ... </mapper> 노드 생성 후 namespace이름을 Mapper 인터페이스의 full qualified name으로 설정
<?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="project.mapper.TagMapper">

</mapper>

여기서 미리 쿼리부터 넣으면 예외 지점이 늘어나므로 지금은 위와같이 비워둔다.(마찬가지로 테스트를 하면서 하나씩 채워넣게 된다)

7. 간단한 테스트 메소드 정의

테스트 메소드가 하나도 없으면 테스트 자체가 안되므로 아래와 같이 빈 테스틀 만든 후 테스트 해본다.

테스트를 수행해서 지금까지 설정한 내용에 문제가 없음을 확인한다.

테스트 성공


테스트가 성공했으니 설정 단계에서는 아무 문제가 없다.(확신을 가지고..)

8. Mapper 인터페이스에 기본적인 insert 메소드 선언

설정이 어느정도 성공했으니 본격적으로 기능 구현을 테스트하는 단계.

iBATIS문서를 보면 insert, update, delete 수행 후 변경이 일어난 row의 개수를 반환한다고 나와있다.

따라서 inset, update, delete관련 메소드는 아래와같이 리턴 타입을 int나 Integer로 한다.

9. 테스트 메소드 안에 코드 작성 후 테스트 실행

TagMapper 인터페이스에 inserTag 를 선언했으니 이 메소드를 테스트할 코드를 작성한다.

테스트하면 mapper.xml 에 아무것도 없으니 당연히 에러가 나온다..
테스트코드에서 TagMapper의 insertTag를 호출하면 "project.mapper.TagMapper.insertTag" 와 같이 key 값을 만들어서 이에 대응하는 statement(<mapper>..</mapper>노드의 namespace 와 안에 정의한 쿼리의 id) 를 찾는다. 하지만 <mapper>..</mapper>안에 아무런 쿼리도 없으므로 대응하는 쿼리를 찾지 못했다는 예외가 발생했다.

10. TagMapper.xml 에 insert 쿼리 정의

TagMapper 인터페이스에 선언한 inserTag 메소드에 대응하는 쿼리를 정의할 차례.

TagMapper.xml 의 <mapper> 노드 안에 <insert> 노드를 만들고 parameterType을 Tag로 해준다.("Tag"는 "project.bean.Tag" 의 type alias).

id는 정확히 TagMapper 인터페이스의 메소드 이름인 "insertTag"로 정의해야 한다.
    <insert id="insertTag" parameterType="Tag">
    </insert>
아직 쿼리는 넣지 않는다. 이 상태에서 예상한 예외가 던져지는지 확인한다.(만일 예상한 예외가 아닌 다른 예외가 던져지면 그것부터 먼저 해결하고 다음 단계로 넘어가야 함)

테스트해보니 "Tag"가 가리키는 타입이 뭔지 알 수 없다고 나온다.

11. <typleAlias> 추가

ibatis-config.xml 파일로 가서 POJO 클래스인 Tag 클래스를 가리키는 <typeAlias/> 추가
<configuration>
    ....
    <typeAliases>
        .....
        <typeAlias type="project.bean.Tag" alias="Tag"/>
    </typeAliases>
    ...
</configuration>

다시 테스트해보면 <insert> ..</insert> 안에 아무런 쿼리도 없으므로 실패한다.

12. <insert>..</insert> 안에 쿼리 작성

insert 쿼리를 작성하고 다시 테스트 해본다.
<mapper namespace="project.mapper.TagMapper">
    <insert id="insertTag" parameterType="Tag">
    INSERT INTO Tags (tag_id, tag_name)
    VALUES ( #{id}, #{tagName} )
    </insert>
</mapper>

테스트를 돌리면 이번에도 예외가 발생하는데, #{..} 부분에 값을 설정할 때 null이 입력되었다고 나온다.


13. <selectKey> ..</selectKey> 추가

위에서 발생한 예외는 Tag tag = new Tag("맥주"); 로 인스턴스를 생성했을때 id 프로퍼티는 null인 상태인데, 이 상태에서 insert 쿼리의 #{id} 에 null인 값을 setting 하면서 발생한 것이다.

정확히 말하면 null이어서가 아니라, null의 타입이 무엇인지 모호해서 발생한 예외이다.(테이블 컬럼에 null 값을 넣을수 있다면 반드시 jdbcType을 명시해주어야 한다.)

하지만 여기에서는 Tags 테이블을 생성할 때 NOT NULL로 명시했기 때문에 타입을 명확히 해도 constraint 에 걸려서 예외가 던져질 것이다.

#{id}에 들어갈 값은 오라클의 시퀀스를 통해서 받을 것이기 때문에 mapper.xml 에서 <insert> 노드 안에 <selectKey> ..</selectKey> 노드를 추가해준다.
<mapper namespace="project.mapper.TagMapper">
    <insert id="insertTag" parameterType="Tag">
        <selectKey keyProperty="id" order="BEFORE" resultType="integer">
            SELECT seq_tags_id.nextval FROM dual
        </selectKey>
    INSERT INTO Tags (tag_id, tag_name)
    VALUES ( #{id}, #{tagName} )
    </insert>
</mapper>
order="BEFORE" 로 설정해서 <selectKey> 쿼리가 본문인 <insert> 쿼리보다 먼저 실행되도록 한다. (MSSQL같은 경우는 AFTER로 주면 된다.) 이렇게 얻어낸 시퀀스 값은 keyProperty="id" 로 설정하면 insert 쿼리의 #{id} 에 삽입된다.

selectKey 노드를 작성한 후 다시 테스트 해보면 성공한다.

테스트 성공


이렇게 해서 테이블 생성 쿼리, POJO 클래스, mapper 클래스, 그리고 iBATIS 설정이 마무리 된다.

추가로 tag 인스턴스의 id 프로퍼티가 제대로 설정되었는지 테스트 해보는 것도 좋다.
mapper 클래스에 selectTagByPost(Integer postId); 메소드를 하나 선언하면 위와같이 한단계 한 단계 밟아나가면서 테스트를 하면 알 수 없는 예외의 바다에 빠져서 시간을 낭비하는 일을 줄일 수 있다.

중요한 것은 위와같은 점진적인 테스트코드 작성, 기능 구현을 통해서 프레임워크에 익숙해진다는 것이다. 처음에는 좀 더디겠지만 익숙해지면 중간 과정을 생략하고 곧바로 mapper.xml 에 쿼리를 넣고 최종 테스트만으로 금방 기능을 구현하고 검증할 수 있다.

Posted by yeori
,