하이버네이트의 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장

Reattaching과 Merging

detached 상태의 객체를 persistent 객체로 전이할 때에는 Reattach가 간편하긴 하지만, 여러 개의 Session에 걸쳐 있을 경우에 예상치 못한 문제가 발생할 수 있습니다.

    @Test
    public void reattaching() {
        Session session1 = sessionFactory.openSession();
        Transaction transaction = session1.beginTransaction();
       
        Member member = new Member();
        member.setName(“toby”);
        session1.save(member);
       
        transaction.commit();
        session1.close();
       
       member.setName(“whiteship”);
       
        Session session2 = sessionFactory.openSession();
        Transaction transaction2 = session2.beginTransaction();
       
      Member member2 = (Member) session2.get(Member.class, member.getId());
      session2.update(member);
       
        assertEquals(“whiteship”, member.getName());
       
        transaction2.commit();
        session2.close();
    }

바로 이런 경우인데요. 위의 코드는 NonUniqueObjectException 예외가 발생합니다. 왜 그런지는 비밀입니다.
암튼 위의 코드야 둘이 붙어있으니까 왜 문제가 발생하는지 잘 보이지만, 여러 세션에 걸쳐서 detacked 객체를 붙였다 땟다 한다고 생각하면, 일일히 추적해서 update()를 사용한 줄을 위쪽으로 올려주는 작업을 해야 합니다. 고통이겠죠.

    @Test
    public void merging() {
        Session session1 = sessionFactory.openSession();
        Transaction transaction = session1.beginTransaction();
       
        Member member = new Member();
        member.setName(“toby”);
        session1.save(member);
       
        transaction.commit();
        session1.close();
       
        member.setName(“whiteship”);
       
        Session session2 = sessionFactory.openSession();
        Transaction transaction2 = session2.beginTransaction();
       
      Member member2 = (Member) session2.get(Member.class, member.getId());
      Member member3 = (Member) session2.merge(member);
       
        assertEquals(“whiteship”, member3.getName());
       
        transaction2.commit();
        session2.close();
    }

이럴 때 사용할 수 있는 것이 merge() 입니다. 이 녀석은 재밌게 동작합니다. 저렇게 하면 아무런 문제도 발생하지 않고 원하는 대로 동작합니다.

대신 주의 해야 할 것은 member2 와 member3가 같은 레퍼런스를 가지고 있기 때문에 아래와 같은 코드가 가능합니다.

        Member member2 = (Member) session2.get(Member.class, member.getId());
        Member member3 = (Member) session2.merge(member);
       
        member2.setName(“whiteship2”);
       
        assertEquals(“whiteship2”, member3.getName());

이런 상황이 용납이 안 되는 것은 아닙니다. 하지만 사방에서 저 객체의 값을 변경하면 좀 곤란하겠죠. member2와 member(detached 상태의 객체)는 merge()의 반환값을 가져온 이상. 사장되었다고 생각하고 작업을 하는 것이 좋겠습니다.

merget()와 Persistent Context의 dirty checking이 맞물리면, 정말 SQL이 재미있게 생성되는 모습을 볼 수 있습니다.

위의 예제에서 발생되는 쿼리는 간단합니다.
– member를 넣는 insert 문
– member2를 가져오는 select문
– member3을 가져올 때(merging time)member2가 참조하던 객체를 변경하는 update문

퀴즈 그렇다면 아래의 코드는 어떤 SQL들을 만들어 낼까요?

        Session session1 = sessionFactory.openSession();
        Transaction transaction = session1.beginTransaction();
       
        Member member = new Member();
        member.setName(“toby”);
        session1.save(member);
       
        transaction.commit();
        session1.close();
       
        member.setName(“whiteship”);
       
        Session session2 = sessionFactory.openSession();
        Transaction transaction2 = session2.beginTransaction();
       
        Member member2 = (Member) session2.get(Member.class, member.getId());
        Member member3 = (Member) session2.merge(member);
       
        member2.setName(“toby”);
       
        assertEquals(“toby”, member3.getName());
       
        transaction2.commit();
        session2.close();

위와 같이 간단하게 발생되는 쿼리 종류를 써주세요. 그리고 왜!! 왜 그런 결과가 생기는지도 설명해주세요~