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

1-7. 레코드 update 하기

PizzaApp의 내용을 다음과 같이 수정합니다.

Pizza pizza = new Pizza();
        pizza.setName(“yuonghoe’s pizza”);
        pizza.setPrice(10000);
        pizza.setSize(“Large”);
        pizza.setToping(“Pepperoni”);

        s.save(pizza);

        pizza.setPrice(15000);
        s.update(pizza);

처음에 가격을 만원으로 했다가 너무 낮아서 만오천원으로 올리고 s.update(pizza); 메소드를 사용하여 update를 합니다.

실행시키면 콘솔 창에 다음과 같이 출력됩니다.

Hibernate: select nextval (‘Pizza_PizzaId_Seq’)
Hibernate: insert into O_Pizza (toping, price, name, size, pizzaId) values (?, ?, ?, ?, ?)
Hibernate: update O_Pizza set toping=?, price=?, name=?, size=? where pizzaId=?

bk87.bmp
DB를 확인해 본 결과 15000원으로 update된 것을 확인 할 수 있습니다.

PizzaApp에서 작성한 코드에서 s.update(pizza);를 삭제하고 다음과 같이 입력해 보겠습니다.

        Pizza pizza = new Pizza();
        pizza.setName(“chanwook’s pizza”);
        pizza.setPrice(10000);
        pizza.setSize(“Large”);
        pizza.setToping(“Kimchi”);

        s.save(pizza);

        pizza.setPrice(15000);

s.update(pizza); 가 빠진것을 제외하면 다른 부분은 거의 동일합니다. 이 것을 실행시키면 콘솔창에 다음과 같이 출력이 되고 DB에서 확인을 해도 역시 update 된 것을 확인할 수 있습니다.

Hibernate: select nextval (‘Pizza_PizzaId_Seq’)
Hibernate: insert into O_Pizza (toping, price, name, size, pizzaId) values (?, ?, ?, ?, ?)
Hibernate: update O_Pizza set toping=?, price=?, name=?, size=? where pizzaId=?

bk88.bmp
왜 이럴지는 HIA 4장에 나오는 그림을 보며 생각해 보겠습니다.