[봄싹] XP 적용 시나리오 3. 개발하기

본격적으로 개발을 해야하는데, 봄싹에서는 오프라인에서 페어로 작업을 해보기도 했지만, 그렇게 자주 충분히 페어 프로그래밍을 했다고 볼 수는 없습니다. 앞으로도 좀 더 꾸준히 시도를 해봐야 그 효용이나 장단점을 파악할 수 있을 것 같습니다. 개인적으로는 뭔가 대화를 나누면서 코딩할 상대가 있어서 안심이 되긴 합니다. DB update 쿼리가 어떻게 되더라? 이거 무슨 리팩토링이지? 이 메서드 이름 맘에 들어? 여기 중복인데 어떻게 제거하면 좋을지 잘 모르겠네.. 같은 식으로 대화를 나눌 수 상대와 함께라면 좋치 않겠어요?


먼저 개발을 진행하기에 앞서 구현하려는 기능을 한 번도 만들어 본적이 없다면, 어느 정도 자신있게 개발을 진행할만큼의 학습이 필요합니다. 그 과정을 파일럿이고 표현했는데, XP 책에서도 파일럿이라고 헀었는지 잘 모르겠습니다. (뭐라고 했는지 찾아보려고 다시 살펴 봤는데 못 찾아서 그냥 썼습니다.)

그다음 과정은 좀 특이하게 바로 개발을 진행하지 않고, 인수 테스트를 만듭니다. 고객이 해당 작업이 완료 됐다는 것을 확인할 수 있는 모종의 장치를 마련하는 것이죠. 고객이 코드를 볼 수 있다면 아주 행복할텐데, 봄싹은 다행히(?) 고객이 전부 개발자 입니다. 굳이 엑셀로 이쁜 포맷을 만들고, 테스트에서 엑셀 로딩해서, 결과를 엑셀에 다시 찍어주고, 고객은 엑셀에서 수식 비교로 해당 테스트가 잘 됐나 안 됐나 확인하는 귀찮은 일은 할 필요가 없습니다. 그렇치만, 인수 테스트 코드가 고객이 원하는 시나리오를 제대로 표현해주지 못하거나, 고객이 개발자인데도 테스트 코드를 읽기가 난해하다면 테스트를 수정해야겠죠.

그다음은 페어 프로그래밍과 TDD로 해당 작업을 구현하는 일입니다. 페어 프로그래밍은 사실 오프라인에서 만났을 때의 얘기지 주중 저녁이나 회사에서 틈틈히 코딩을 하는 봄싹 개발자에데는 다소 난해한 일입니다. 그래도 메신저등을 통해서 의견은 주고 받을 수 있으니 그것도 페어 프로그래밍으로 치도록 하죠.

그렇게헤서 작업이 끝나면, 담당자 두 명은 자신들이 예상했던 난이도와 시간에 비해 실제로는 난이도가 어땠으며 실제로 소요된 시간은 어느정도인지 기록합니다. 고객은 해당 작업 결과를 본 뒤 간략한 피드백을 줍니다. “담부턴 더 빨리 만들어 주세요” 라던지.. “참 잘했어요” 라던지 ㅋ

[GenericDao] 하이버네이트 GenericDao

먼저, GenericDao를 만들어 쓰면 좋은 이유를 생각해보겠습니다.
– 모든 DAO에서 중복되거나 반복되는 코드를 상당량 줄일 수 있습니다.
– 테스트도 그만큼 줄일 수 있습니다.
– 개발이 좀 더 빨라집니다.
– 비슷한 기능을 하는 메서드 이름을 통일할 수 있습니다.

Entity 당 DAO를 쓰면 좋은 점과 타입-안정성을 제공하는 DAO 패턴을 사용하면 좋은 점 등은 이 글에 정리되어 있으니 궁금하신 분들은 참고하세요

TDD로 다 만든 다음, 맨 마지막에 이클립스 리팩터링 기능 중에 extract interface로 뽑아 낸 인터페이스는 다음과 같습니다.

public interface GneericDao<E> {

    void add(E entity);

    List<E> getAll();

    E getById(Serializable id);

    void delete(E entity);

    void update(E entity);

    void flush();

    E merge(E entity);

}

이것을 구현한 실제 DAO 구현체는 이렇게 생겼습니다.

public class HibernateGenericDao<E> implements GneericDao<E> {

    protected Class<E> entityClass;

    @SuppressWarnings(“unchecked”)
    public HibernateGenericDao() {
        ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
        Type type = genericSuperclass.getActualTypeArguments()[0];
       if (type instanceof ParameterizedType) {
         this.entityClass = (Class) ((ParameterizedType) type).getRawType();
       } else {
         this.entityClass = (Class) type;
       }
    }

    @Autowired
    protected SessionFactory sessionFactory;

    public void add(E entity) {
        getCurrentSession().save(entity);
    }

    private Session getCurrentSession() {
        return sessionFactory.getCurrentSession();
    }

    @SuppressWarnings(“unchecked”)
    public List<E> getAll() {
        return getCurrentSession().createCriteria(entityClass)
                .list();
    }
   
    @SuppressWarnings(“unchecked”)
    public E getById(Serializable id){
        return (E) getCurrentSession().get(entityClass, id);
    }
   
    public void delete(E entity){
        getCurrentSession().delete(entity);
    }
   
    public void update(E entity){
        getCurrentSession().update(entity);
    }
   
    public void flush(){
        getCurrentSession().flush();
    }
   
    @SuppressWarnings(“unchecked”)
    public E merge(E entity){
        return (E) getCurrentSession().merge(entity);
    }

}

특징이라고 할 수 있는 걸 꼽자면..
– 하이버네이트 SessionFactory를 사용하는 GenericDAO 라는 것.
– 별도로 엔티티 타입을 인자로 넘겨줄 필요가 없다는 것.
– 타입-안전성을 보장하기 때문에 별도의 캐스팅 등이 필요없고, 컴파일 시점에 체크 가능하다는 것.

이 클래스는 다음과 같은 테스트 클래스를 이용해서 TDD로 만들었습니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations=”/testContext.xml”)
@Transactional
public class HibernateGenericDaoTest extends DBUnitSupport{

    @Autowired TestDao dao;
   
    @Test
    public void add() throws Exception {
        TestDomain entity = new TestDomain();
        dao.add(entity);
        assertThat(dao.getAll().size(), is(1));
    }
   
    @Test
    public void getAll() throws Exception {
        insertXmlData(“testData.xml”);
        assertThat(dao.getAll().size(), is(2));
    }
   
    @Test
    public void getById() throws Exception {
        insertXmlData(“testData.xml”);
        assertThat(dao.getById(1).getName(), is(“keesun”));
    }
   
    @Test
    public void delete() throws Exception {
        insertXmlData(“testData.xml”);
        TestDomain entity = dao.getById(1);
        dao.delete(entity);
        assertThat(dao.getAll().size(), is(1));
    }
   
    @Test
    public void update() throws Exception {
        insertXmlData(“testData.xml”);
        // entity is (similar)detached object
        TestDomain entity = new TestDomain();
        entity.setId(1);
        entity.setName(“whiteship”);
       
        dao.update(entity);
        // now, entity has been persistent object
        entity.setName(“helols”);
        dao.flush();
        assertThat(dao.getById(1).getName(), is(“helols”));
    }
   
    @Test
    public void merge() throws Exception {
        insertXmlData(“testData.xml”);
        // entity is detached object
        TestDomain entity = new TestDomain();
        entity.setId(1);
        entity.setName(“whiteship”);
       
        TestDomain newEntity = dao.merge(entity);
        // newEntity is persistent object, but entity is still detached object
        newEntity.setName(“helols”);
        entity.setName(“nije”);
        dao.flush();
        assertThat(dao.getById(1).getName(), is(“helols”));
    }
   
}

이 테스트의 특징은 다음과 같습니다.
– 하이버네이트의 update()와 merge()의 특징과 그 차이점을 이해할 수 있도록 작성했습니다.
– 스프링 테스트 컨텍스트를 사용했습니다.
– DBUnit과 그것을 확장한 클래스를 이용했습니다.


생각해볼 것
– GenericDao에 있는 update(), merge(), flush()는 Generic하지 않다는 생각이 듭니다.
– (위에는 보여드리지 않았지만)테스트에 사용된 TestDomain 클래스와 TestDao를 GenericDaoTest 내부에 포함 시키는 것이 좋치 않을까?
– 어떤 기능을 더 추가할 수 있을까?

JUnit에서 setUp() 또는 @Before를 사용하는 이유?

참조: http://stackoverflow.com/questions/512184/best-practice-initialize-junit-class-fields-in-setup-or-at-declaration

JUnit은 각각의 테스트를 독립적으로 테스트하기 위해 테스트 마다 개별적인 객체를 생성합니다. 따라서..

public class SomeTest extends TestCase
{
   private final List list = new ArrayList();

    public void testPopulateList()
   {
       // Add stuff to the list
       // Assert the list contains what I expect
   }
}

이런식으로 테스트를 작성해도

public class SomeTest extends TestCase
{
   private List list;

    @Override
   protected void setUp() throws Exception
   {
       super.setUp();
       this.list = new ArrayList();
   }

    public void testPopulateList()
   {
       // Add stuff to the list
       // Assert the list contains what I expect
   }
}

이렇게 작성한 것과 동일하게 각각의 테스트를 실행하기 전에 초기화 하는 코드를 실행합니다.

그럼 대체 setUp이나 @Before는 왜 사용할까요? 그냥 생성자를 사용하거나 저렇게 필드에 직접 객체를 생성하게 해도 비슷한데 말이죠.

윗 글을 찾아보기 전에는 ‘그냥 tearDown이랑 균형을 맞출려고 만들었나?’, ‘생성자를 쓰면 안 좋은 뭔가가 있나?’ 등등 막연하게 생각하고 있었습니다. 그러다가 도무지 궁금해서 구글신에게 문의했더니 윗 글을 찾을 수 있었습니다.

댓글들을 조사해 본 결과.

1. Exception 발생: setUp()에서 예외를 던지면 JUnit이 유용한 스택 트레이스 정보를 돌려주지만, 생성자에서 예외가 발생하면 그냥 테스트 객체를 못 만드는 것이기 때문에 유용한 정보를 얻을 수 없음.

2. 베스트 프랙티스: 테스트 하려는 클래스의 인스턴스는 테스트 또는 setUp()에서 생성하고, 테스트 대상이 아닌 속성들은 필드에서 직접 new로 생성해도 상관없다.

이 두 가지가 유력해 보이네요.

봄싹 3기 TDD 스터디 장소 시간 확정 됐습니다.

봄싹 3기 첫 번째 모임 장소, 시간, 참가자 확정

봄싹 블로그도 꼭 구독해주세요. 윗 글에 댓글을 다신 다음에 참석하시면 됩니다. 정말 간단하죠. 비용은 5천원입니다. 장소는 신촌. 시간은 오후 4~6시 두 시간. 뭘하냐면.. 처음이니만큼.. 참가자 분들 소개와 봄싹 소개 및 이번 TDD 스터디 개요와 진행 방법을 정하고 공유하겠습니다.

최대 25인까지 참석할 수 있으니까요. 많이들 신청하고 참석해주세요. (17인보다 적게 오시면 오시는 분들 비용이 늘어날 수 있습니다. 주변에 친한 동료를 한 분씩 데리고 오세요.ㅋㅋ)

5천원이면;; 웬만한 커피숖에서 젤 싼 커피값 아닌가요. 오셔서 다른 개발자 분들과 담소도 나누고 스터디도 하고 음료수도 마시고~. 좋자나요.