Whiteship's Note


[회사일] Member 추가. MemberDAO 구현. GenericDAO 구현

프로젝트/SLT : 2010.06.09 14:56


이제 Code 도메인 가지고 아주 기본적인 CRUD를 만들었으니 새로운 도메인을 하나 더 추가해서 중복코드를 잡아가면서 프레임워크를 뽑아내면 됩니다.

첫번쨰 도메인 CRUD를 만들때는 아키텍처 정하고 화면 구상하고 이것저것 플러그인 찾아보고 네비게이션 정하고 버튼 모양 덩덩 정하느라 시간이 많이 걸렸다면 이제부터는 중복코드와 싸움입니다.

먼저 Member 도메인을 추가합니다.

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    int id;

    @Column(length = 50)
    @NotNull(message = "입력해주세요.")
    @Size(min = 1, message = "입력해주세요.")
    String loginId;
    @Column(length = 50)
    @NotNull(message = "입력해주세요.")
    @Size(min = 1, message = "입력해주세요.")
    String password;

    @Column(length = 50)
    @NotNull(message = "입력해주세요.")
    @Size(min = 1, message = "입력해주세요.")
    String name;
    @Column(length = 50)
    @NotNull(message = "입력해주세요.")
    @Size(min = 1, message = "입력해주세요.")
    String email;
    @Column(length = 50)
    String phoneNumber;
    @Column(length = 50)
    String jobTitle;
    @Temporal(TemporalType.DATE)
    Date birthDay;
...
}

여기서도 애노테이션 뭉탱이를 중복제거하고 싶은데.. 일단 조금 뒤로 미루겠습니다.

다음은 DAO를 만듭니다. 인터페이스를 만들고 구현체를 만듭니다.

public interface MemberDao {

    void add(Member member);

    List<Member> list(PageParam pageParam, MemberSearchParam sp);

    int totalSize(MemberSearchParam sp);

    Member getById(int id);

    void deleteBy(int id);

    void update(Member member);

}

구현체는..

@Repository
public class MemberDaoImpl implements MemberDao {

    @Autowired SessionFactory sessionFactory;

    public void add(Member member) {
        getSession().save(member);
    }

    public Member getById(int id) {
        return (Member) getSession().get(Member.class, id);
    }

    public void deleteBy(int id) {
        int result = getSession().createQuery("delete from Member where id = ?").setInteger(0, id).executeUpdate();
        if(result != 1)
            throw new RuntimeException();
    }

    public void update(Member member) {
        getSession().update(member);
    }

    public List<Member> list(PageParam pageParam, MemberSearchParam searchParam) {
        Criteria c = getCriteriaOf(Member.class);
        //searching
        applySearchParam(c, searchParam);
        //paging
        c.setFirstResult(pageParam.getFirstRowNumber());
        c.setMaxResults(pageParam.getRows());
        //ordering
        if(pageParam.getSord().equals("asc"))
            c.addOrder(Order.asc(pageParam.getSidx()));
        else
            c.addOrder(Order.desc(pageParam.getSidx()));

        return c.list();
    }


    public int totalSize(MemberSearchParam searchParam) {
        Criteria c = getCriteriaOf(Member.class);
        applySearchParam(c, searchParam);
        return (Integer)c.setProjection(Projections.count("id"))
            .uniqueResult();
    }

    private void applySearchParam(Criteria c, MemberSearchParam searchParam) {
        
    }

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

    private Criteria getCriteriaOf(Class clazz){
        return getSession().createCriteria(clazz);
    }
        
}

CodeDaoImple 코드와 거의 똑같습니다.

상속을 사용해서 중복을 제거하겠습니다. 먼저 인터페이스 부터..

public interface GenericDao<E, S> {

    void add(E e);

    List<E> list(PageParam pageParam, S s);

    int totalSize(S s);

    E getById(int id);

    void deleteBy(int id);

    void update(E e);

}

그리고 이걸 CodeDao와 MemberDao에 사용합니다.

public interface CodeDao extends GenericDao<Code, CodeSearchParam>{
    
}

public interface MemberDao extends GenericDao<Member, MemberSearchParam>{
   
}

오퀘 인터페이스가 텅 비었습니다. 아주 일반적인 CRUD 이외 추가기능이 생기면 눈에 확 띄겠죠.

다음은 GenericDao 구현체를 만듭니다.

public abstract class GenericDaoImpl<E, S> implements GenericDao<E, S>{

    @Autowired protected SessionFactory sessionFactory;
    
    private Class<E> entityClass;

    public GenericDaoImpl() {
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;
        }
}
    
    public void add(E e) {
        getSession().save(e);
    }

    public E getById(int id) {
         return (E) getSession().get(entityClass, id);
    }

    public void deleteBy(int id) {
        int result = getSession().createQuery("delete from" + entityClass.getSimpleName()+ " where id = ?").setInteger(0, id).executeUpdate();
        if(result != 1)
            throw new RuntimeException();
    }

    public void update(E e) {
        getSession().update(e);
    }

    public List<E> list(PageParam pageParam, S s) {
        Criteria c = getCriteriaOf(entityClass);
        //searching
        applySearchParam(c, s);
        //paging
        c.setFirstResult(pageParam.getFirstRowNumber());
        c.setMaxResults(pageParam.getRows());
        //ordering
        if(pageParam.getSord() != null){
            if (pageParam.getSord().equals("asc"))
                c.addOrder(Order.asc(pageParam.getSidx()));
            else
                c.addOrder(Order.desc(pageParam.getSidx()));
        }
        //noinspection unchecked
        return c.list();

    }

    protected abstract void applySearchParam(Criteria c, S s);

    public int totalSize(S s) {
        Criteria c = getCriteriaOf(entityClass);
        applySearchParam(c, s);
        return (Integer) c.setProjection(Projections.count("id"))
                .uniqueResult();
    }

    protected Session getSession() {
        return sessionFactory.getCurrentSession();
    }

    protected Criteria getCriteriaOf(Class clazz){
        return getSession().createCriteria(clazz);
    }
}

그리고 이녀석을 사용하도록 CodeDaoImpl과 MemberDaoImpl을 수정합니다.

@Repository
public class CodeDaoImpl extends GenericDaoImpl<Code, CodeSearchParam> implements CodeDao {
    
    @Override
    protected void applySearchParam(Criteria c, CodeSearchParam searchParam) {
        CriteriaUtils.addOptionalLike(c, "code", searchParam.getCode());
        CriteriaUtils.addOptionalLike(c, "name", searchParam.getName());
        CriteriaUtils.addOptionalEqual(c, "cate", searchParam.getCateValue());
    }
    
}

@Repository
public class MemberDaoImpl extends GenericDaoImpl<Member, MemberSearchParam> implements MemberDao {

    @Override
    protected void applySearchParam(Criteria c, MemberSearchParam searchParam) {

    }
}

마지막으로 이전에 만들어 둔 CodeDaoImplTest를 한번 돌려주면 됩니다. 굳이 뭐또 GenericDaoImpl 테스트를 만들 필요는 없는것 같네요.

이렇게 과감한 코드 수정을 할 수 있었던 게 다 CodeDaoImplTest 덕분입니다.

자 그럼 잠깐 밥먹고 MemberService도 만들어야지 





top

Write a comment.


[GenericDao] 하이버네이트 GenericDao

모하니?/Coding : 2009.09.04 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.