Whiteship's Note

[GenericDao] 하이버네이트 GenericDao

모하니?/Coding : 2009. 9. 4. 15:23


먼저, 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 내부에 포함 시키는 것이 좋치 않을까?
- 어떤 기능을 더 추가할 수 있을까?


top

  1. 대한민국토리 2009.09.04 16:29 PERM. MOD/DEL REPLY

    저도 최근에 비슷하게 기본 CRUD에 대해서 제네릭으로 인터페이스를 만들었는데요. 너무나 비슷해서 깜짝 놀랐어요. 사람들 생각하는게 비슷하구나 하구요^^

    게다가 자세한 설명에 TDD까지 덧붙여주시니 그야말로 감동입니다.

    Favicon of http://whiteship.me BlogIcon 기선 2009.09.04 17:18 PERM MOD/DEL

    네ㅎㅎ GenericXXX 인터페이스 내용은 많이들 비슷할 것 같아요.

  2. hoyeol 2009.09.04 17:33 PERM. MOD/DEL REPLY

    오.. 비슷합니다.
    제경우는 update() 를 사용할때 문제가 있어서 note에 적어두었었는데.. 내용인즉슨
    findXXX() 로 instance를 가져온후, 동일한 ID를 가진 새로운 instance를 생성해서 update()를 호출하는 경우에 NonUniqueObjectException이 발생했었습니다.
    hibernate에 대한 무지로 인해 비롯된 문제였었죠 -.-; 그래서 이후에는 반드시 update()에 "원래 가져온 instance를 다시 저장해야한다" 를 떠올리게됩니다;

    Favicon of https://whiteship.tistory.com BlogIcon 기선 2009.09.04 22:05 신고 PERM MOD/DEL

    응~ LazyInitializing뭐시기랑 더불어 익숙해져야 하는 에러 중 하나로군..

  3. Favicon of http://blog.lckymn.com BlogIcon Kevin 2009.09.05 03:09 PERM. MOD/DEL REPLY

    저도 제가 쓰는 GenericRepository 를 예로 포스팅 하나 해야 하는데,
    여러가지 일이 겹쳐서 바빠지니, 엄두가 나질 않습니다... @_@;
    아무래도 게을러서겠죠? ㅠ_ㅠ

    그러고 보면, 쓰리잡 시작하신 기선님께서는 정말 부지런 하신듯...
    그나저나 E getById(Serializable id); 요거 id를 그냥 Serializable 로 잡으면,
    Serializable를 implements 한 타입은 다 적용되서
    실제로 쓰려는 id 타입이 아닌걸 넣어도 compile-time error가
    발생하지 않는 문제가 있을것 같은데요.

    실제 ID는 Integer인데 Long이 들어갈수도 있겠고 아니면 Double이라던가요.
    심한경우 Serializable implements한 일반 JavaBean까지 id로 들어갈수가...
    물론 id로 JavaBean을 넘기는건 심각한 실수겠지만요.
    add method같은거랑 착각해서 그냥 넣어도 컴파일 타임 에러 없음이니... 덜덜덜...
    제생각엔 역시 ID도 generic으로 잡아주는게 좋지 않을까 합니다만...

    Favicon of http://whiteship.tistory.com BlogIcon 기선 2009.09.05 07:16 PERM MOD/DEL

    아... 그런 문제가 있군요. 키 타입이 안전하지 않았네요.
    흠.. 그래서 키 타입을 따로 주는 코드들이 있었던 거군요.

    감사합니다~ㅎㅎ

Write a comment.




: 1 : ··· : 92 : 93 : 94 : 95 : 96 : 97 : 98 : 99 : 100 : ··· : 299 :