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


[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