하이버네이트를 이용한 DAO 코드에서 int 타입의 리턴값과 SQLException은 무슨 의미가 있을까…

JDBC로만 코딩을 해오신 분에게 하이버네이트를 사용하여 만들 DAO에 필요한 메소드들을 정의해서 커밋해달라고 부탁했습니다. 그리고 다음과 같은 코드를 받았습니다.

public int update(final Task task) throws SQLException {
..
}

public int delete(final Task task) throws SQLException {

}

객체와 레코드가 맵핑되어 있는데 그 수를 셀 필요가 있을까요?

이 코드는 하이버네이트가 그 동안 저에게 얼마나 많은 도움을 줬는지 알게 해주는 코드였습니다. 상대방과 위에서 정의한 리턴값의 용도와 의도를 물어보고, 하이버네이트의 update와 JDBC update 개념이 다르다는 것과 하이버네이트 최신 버전은 모두 RuntimeException을 던진다는 이야기를 해줬습니다. 자세한 내용은 생략하겠습니다. 이미 이전에 하이버네이트의 update에 대해 장문의 글을 쓴적도 있고, RuntimeException에 대해서는 말할 필요도 없을테니까요.

성실한 누군가를 교육한다는건 정말 재밌는 일입니다. 기회가 되면 스크린캐스팅을 해서 올리고 싶은데, 실제 나가는 진도에 비해 공백이 너무 커서 스크랜캐스팅 편집도 해야 되는데 도무지 그럴 짬은 안나네요 ㅋ

하이버네이트의 update() 와 merge()

찬욱군의 블로그를 보다가 merge()를 save(), update() 대용으로 사용하는 코드를 봤습니다. 스프링의 샘플 코드더군요. 해당 코드에 보면 주석으로 모라모라고 달려있는데 그걸 찬욱군이 블로그에 잘 풀어서 설명해두었습니다. (하지만 잘 이해가… @.@;;)

왜 그렇게 코딩을 해야 하는지 모르겠더군요.

1. save() 대용으로 사용한 경우.

해당 코드는 아래와 같습니다.

    public void storeOwner(Owner owner) {
        // Note: Hibernate3’s merge operation does not reassociate the object
        // with the current Hibernate Session. Instead, it will always copy the
        // state over to a registered representation of the entity. In case of a
        // new entity, it will register a copy as well, but will not update the
        // id of the passed-in object. To still update the ids of the original
        // objects too, we need to register Spring’s
        // IdTransferringMergeEventListener on our SessionFactory.
        sessionFactory.getCurrentSession().merge(owner);
    }

    public void storePet(Pet pet) {
        sessionFactory.getCurrentSession().merge(pet);
    }

    public void storeVisit(Visit visit) {
        sessionFactory.getCurrentSession().merge(visit);
    }

코드 출처 : spring 소스/samples/petclinic/src/…/HibernateClinic.java

희한합니다. 전부 저장하는 류의 메소드들인데 merge()를 쓰고 있네요. 이 녀석들을 사용한 코드를 보니 AddXXFrom 류의 클래스들에서 사용하고 있었습니다. 왜 그랬는지 잘 모르겠습니다. 저 메소드들에 넘겨준 객체의 상태를 Persistent로 바꾸기 싫었다고 생각할 수 밖에 없습니다.(merge()의 특성은 조금 뒤에 살펴보겠습니다.) 그런데도 위의 주석을 보면 id값만은 어떻게든 가지고 싶어서 IdTransferringMergeEventListener 이런 녀석을 사용할 수도 있다고 나와있습니다.

그럼 결론은..

this.clinic.storeOwner(owner);

이렇게 넘겨준 owner라는 객체의 상태는 그대로 Transient로 유지하고 Persistent로 바꾸지 않으면서도 id 값은 가지고 있도록.. 하고 싶을 때 저런 방법을 사용할 수 있습니다. 귀찮게 왜 그럴까요? 몰겠습니다. 그냥 save(owner) 하면 넘겨준 owner 객체가 Persistent 상태가 되면서 id도 가지게 될텐데 말이죠.

2. update() 대용으로 사용하는 경우

update()에 대한 간략한 설명을 해야겠네요. update()는 그냥 DB의 UPDATE 문이 아닙니다. reattach입니다. reattach가 뭐냐면 “다시 붙이기”입니다. detached 상태의 객체를 Persistence Context에 다시 붙이는 것(해당 객체는 Persistent 상태가 되겠죠.)을 뜻합니다. update(owner); 를 하게되면 owner 객체를 다시 Persistent Context에 붙이고 그럼 owner 객체는 Persistent 상태가 됩니다. 이 때 다음과 같은 문제가 발생할 수 있습니다.

        Member member2 = (Member) session2.get(Member.class, member.getId());
        session2.update(member);

두 줄 모두 하이버의 Unit of work 내에서 실행된다면, NonUniqueObjectException()이 발생합니다. 말 그대로 입니다. Persistent Context 내부에 단일 레코드를 나타내는 둘 이상의 객체가 존재하기 때문에 발생하는 것입니다. 이 현상이 나쁜건가요? 당연한 겁니다. 대체 하이버는 누굴 기준으로 더티 체킹을 해야하죠?? 이 예외를 피해가야 할까요? 아니죠. 소스 코드를 손봐야 하는 겁니다. 어떻게요? 순서를 바꿔주면 됩니다.

        session2.update(member);
        Member member2 = (Member) session2.get(Member.class, member.getId());

만약 왜 위에는 에러가 나고 아래는 에러가 안 나는지 모르시겠다면, 하이버네이트 공부를 하시면 됩니다. 간략하게 설명 드리면, member 객체가 먼저 Persistent Context에 들어가서 Persistent 상태가되고, 그 다음 get()을 하면 DB에서 읽어오는게 아니라 Persistent Context에서 가져오기 때문에 아무런 문제가 없습니다.

그런데 같은 문제를 merge()를 사용해서도 해결할 수 있습니다.

        Member member2 = (Member) session2.get(Member.class, member.getId());
        Member member3 = (Member) session2.merge(member);

이렇게 말이죠. 그런데 여기서 중요한 건 member2와 member3은 Persistent 객체이고 member와 같은 값을 가지고 있지만, member만 여전히 Detached 상태라는 것입니다. 그리고 member2와 member3에 대한 변경(dirty) 사항이 양쪽 모두에 적용이 됩니다. 얼마나 아리까리 합니까? 그래서 하이버 책에서는 merge()해서 돌려받은 객체(여기서는 member3)만 사용하라고 권장하고 있습니다. 그런데 객체가 막 돌아다닐텐데 권장사항대로 잘 되진 않겠죠.

merge()는 넘겨받은 객체의 값들과 콜렉션을 복사합니다. 그리고 그 객체가 가지고 있는 id와 같은 id를 갖고 있는 녀석을 Persistent Context에서 찾아서 가져옵니다.(SELECT 쿼리 안 날아감.) Persistent Context에 없으면 DB에서 가져옵니다.(SELECT 쿼리 날아감.) 그런 다음에 값들을 가져온 객체(Persistent 상태겠죠.)에다가 덮어씌웁니다. 그리고 그녀석을 반환해 줍니다. 따라서 Detached 상태로 넘겨준 객체는 여전히 Detached 상태로 남아있고 그 객체와 같은 값을 가진 새로운 Persistent 객체가 만들어지게 됩니다.

그런데 넘겨준 객체가 Detached 객체가 아니라 Transient 객체라면?? 즉 save() 대용으로 merge()를 사용하는 경우가 이 경우에 해당하겠죠. 그렇다면, id가 없고 그럼 Persistent Context에서 찾을 것도 없고 DB에서 가져올 것도 없습니다. 대신 하나를 새로 만들어야겠죠. 대신 이 녀석도 마찬가지로 넘겨받은 객체의 값들을 복사해서 새로운 객체를 만들고 나중에 Unit of Work가 끝난 뒤 INSERT 문이 날아갈 준비가 됩니다. 다시 한번 주의할 것은 넘겨 받은 객체 자체를 Persistent 상태로 만들지는 않는다는 것입니다. merge()는 넘겨받은 객체의 상태를 바꾸지 않습니다.

결론을 내리자면, save() 대용으로 merge()를 사용하는 건 제 생각으로는 비추입니다. 애초에 merge()는 Detached 객체를 reattach 하기위한 용도이지, Transient 객체를 위한 용도가 아닙니다. update()의 대용으로 사용하는 걸 생각해볼 수는 있지만, merge()와 update()의 특징에 따라 원하는 것을 사용하시는게 좋겠습니다. 단순하게 예외를 피하기 위한 용도로 사용하는 것은 비추입니다.

참조 : Java Persistence With Hibernate 9장

하이버네이트를 쓰려면 게터 세터가 꼭 필요하다.

뻥입니다. 보통 게터 세터를 많이 만들어서 사용하기때문에 이렇게 생각할 수도 있겠습니다. 하지만 미신입니다.

필드에 직접 접근할 수 있습니다.

코딩으로 검증작업은 찬욱군이 해주었습니다.

저는 구글링으로 검증을 해보았습니다.
바로 hibernate-mapping default-access=”field” 이런 설정이 검색되었습니다. 하이버네이트 레퍼런스 5.1.2. hibernate-mapping 부분의 설정에 보면 위의 설정이 있습니다.

찬욱군 블로그의 Hibernate 기초 학습[6] – AccessType / fomula 이 글을 보시면 다양한 설정 방법을 참조하실 수 있습니다.

잘못 된 openSession() 사용 예제ㅋ

이전 글에서 DAO 구현을 다음과 같이 했었습니다.

public class MemberDaoImpleWIthSpringTransaction implements MemberDao{

    private SessionFactory sessionFactory;

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public void add(Member member) {
        Session session = sessionFactory.getCurrentSession();
        session.save(member);
    }
}

여기서 빨간 색 부분은 찬욱군이 알려줬기 때문이고 원래는 아래처럼 코딩했었습니다.

    public void add(Member member) {
        Session session = sessionFactory.openSession(); //(1)
        session.save(member);
        session.flush(); //(2)
        session.close(); //(3)
    }

위에 표시한 빨간 부분이 모두 잘못된 부분이였습니다.

(1) 오픈 세션은 새로운 세션을 만들게 되고 그럼 새로운 트랜잭션에서 save를 실행하게 됩니다. 그런데 지금 저 코드는 새로운 트랜잭션이 아니라 Service Layer에서 사용하던 트랜잭션을 사용해야 합니다. 그러려면 새로운 세션이 아닌 getCurrentSession을 사용하면 기존에 존재하는 트랜잭션을 사용하게 됩니다.

(2) flush()는 무조건 DB에 저장하게 된다고 합니다. 따라서 삭제해야합니다. 지금 이 코드는 트랜잭션 내에 있고 예외가 발생하면 롤백해야 되니까 무조건 DB에 넣어버리는 flush()를 사용하면 안되겠습니다.

(3) getCurrentSession은 자기가 알아서 세션을 닫기 때문에 명시적으로 session을 닫아버리면 안됩니다.(에러가 나더군요.)

찬욱군 베리베리 땡큐 감사!!

Convention over Configuration

RoR을 공부해 본적은 없지만 조만간 운좋게 베타리더로 Rails for Java Developer를 접하게 될 것 같아서 공부하게 될 것 같습니다.

짧게나마 이해한 바로는 ‘설정보다 규약이 편하니까 규약으로 할 수 있는 건 규약으로 하자.’는 것 같습니다. 숙제3 에 있는 링크 중에 하나에 다음과 같은 그림이 있습니다.

사용자 삽입 이미지

장점 : 1. 새로운 개발자가 시스템을 빨리 이해할 수 있다. 2. 일관성을 높일 수 있다. 3. 보다 유연하다.

단점 : 1. 잘 알고 있어야 한다. 2. 프레임웤이 커진다. 3. 프레임웤이 새로운 규약을 도입했을 때 리팩토링이 힘들다.

단점 중에 3번이 가장 치면적인 것 같습니다. EJB였나.. 3.0으로 올리면서 어떤 속성의 default값을 바꿔서 온라인에서 개발자와 된통 싸웠다는 이야기를 들은 것 같은데 3번에 딱 맞는 예가 될 것 같습니다.[footnote]자세한 내용은 찬욱이한테 들은 것 같긴한데 기억이 잘 안납니다.[/footnote]

Spring에 CoC가 적용된 예로는 InternalResourceViewResolver, MethodNameResolver, ControllerClassNameHandlerMapping. 이런 것들이 있다고 합니다. 참조