[하이버네이트] detached 객체의 동일성

참조: JPWH 9.2.2 ~ 9.2.3

두 가지 동일성이 있다. 자바 객체 동일성과 DB 동일성이 있다. 자바 동일성은 == 으로 비교를 하고, DB 동일성은 주키 값을 비교한다. 자바 동일성과 DB 동일성이 모두 같을 조건을 Scope of object identity 라고 한다.

그 중에 세 가지 조건은 다음과 같다.
– No identity scope
– Persistence context-scoped identity
– Process-scoped identity

이 중에 하이버네이트는 Persistence context-scoped identity를 구현했다.

Session session1 = sessionFactory.openSession();
Transaction tx1 = session1.beginTransaction();

// “1234” 식별자로 Item 가져오기
Object a = session1.get(Item.class, new Long(1234) );
Object b = session1.get(Item.class, new Long(1234) );

( a==b ) // True, a와 b는 동일하다.
tx1.commit();
session1.close();

// a와 b 레퍼런스는 detached 상태 객체가 된다.

Session session2 = sessionFactory.openSession();
Transaction tx2 = session2.beginTransaction();

Object c = session2.get(Item.class, new Long(1234) );
( a==c ) // False, detached 객체 a와 persistent 객체 c는 동일하지 않다.(session context가 다르기 때문에..)

tx2.commit();
session2.close();

따라서 이렇게 된다. 하지만 여전히 DB id는 가지고 있기 때문에, a, b, c를 id 값으로 비교하면 모두 같은 객체로 인식할 수 있다. equlas()로 비교할 땐 equals()를 어떻게 구현하느냐에 따라 달리질 것이다. equals()를 별도로 구현하지 않으면 Object의 equals()를 사용할테니 == 비교와 다를 바가 없다.

equals() 비교가 중요해지는 시기는 Set 컬렉션을 사용할 때다. 아시다시피 Set은 컬렉션에 요소를 추가하기 전에 기본에 추가되어 있는 것들과 equals() 비교를 해본 다음에 false인 것만 추가한다.

그렇다면, 위의 코드가 모두 깥는 뒤 (detached) 객체 3개(a, b, c)를 Set에 추가하면 어떻게 될까? 과연 Set에는 몇 개의 객체가 들어갈까?

2개다. 일단, equals()를 별도로 구현하지 않아서, == 비교를 할 텐데 이미 위에서 == 비교를 해봤더니 a와 b는 같은 객체를 참조하고 있고, c는 또 다른 객체기 때문이다.

원하던 결과는 몇 개 일까? 한 개일 것이다. 셋 다 같은 DB 레코드를 가리키기 때문에, Set에는 한 개만 들어가는게 맞을 것이다. 그렇게 하려면 어떻게 해야 할까?

DB id를 사용해서 동일성을 비교하는 equals()를 재정의하면 될 것이다. equals()를 재정의하면 hashCode()도 반드시 재정의해서 같은 객체는 같은 해쉬코드를 반환하도록 해야겠다.

결론적으로, detached 객체를 다룰 때는 동일성에 주의하고, 동일성 문제가 발생할 시 equals()와 hashCode()를 적절하게 재정의 할 것.

Hibernate VS JPA

Hibernate JPA
네 가지의 상태(transient, persistent, removed, detached)가 있다. 이것들을 표준화했다.
detached 객체를 merging하거나 reattach할 수 있다. merging만 지원한다.
flush 시점에서 save()와 update()는 모든 연관된 객체들에 영향을 준다. persist()는 호출 시점에 연관되어 있는 객체들에만 영향을 준다. flush 시점에서 persist()는 모든 연관된 객체들에 영향을 준다. save()와 update()는 해당 메소드를 호출하는 시점에 연관되어 있는 객체들에만 영향을 준다.
get()은 DB에 다녀오고, load()는 프록시를 반환한다. find()는 DB를 다녀오고, getReference()는 프록시를 반환한다.
EJB에서 Session을 사용할 때 Depedency Injection은 JBoss Application Server만 지원한다. EntityManager의 DI는 모든 EBJ 3.0 호환 서버에서 지원한다.

하이버네이트 API : Persistence context 관리하기

Persistence Context 캐시 제어하기

  • Persistent 객체들의 스냅샷들을 캐시에 복사해둔다.
  • 이 캐시들을 사용하여 dirty checking을 하여 persistent 객체들의 변경 사항들을 찾아낸다.
  • 수천개의 객체들을 로딩하면, OutOfMemoryException을 내고 죽어버릴 수가 있다.
  • 캐시를 줄이거나 메모리 공간을 제약하려면 다음과 같이 해야 한다.

    1. 필요한 객체만 Persistent 상태로 유지하라. 전체 객체 그래프를 모두 끌어오다가는…
    2. session.evict(object)를 사용해서 persistent 상태의 객체를 명시적으로 detached 시킬 수 있다.
    3. session.clear()를 사용해서 Persistent Context에 있는 모든 객체를 detached로 만들 수 있다. 이 녀석들은 dirty checking하지 않는다.
    4. session.setReadOnly(object, true)를 사용해서 특정 객체에 대해서는 dirty checking을 하지 않도록 설정할 수 있다. false를 주면 다시 한다. 객체의 상태는 바꾸지 않는다.

Persistent Context flushing 하기

  • 하이버는 Persistent 객체들에 대한 변경 사항을 바로바로 DB에 반영하지 않는다.

    1. DB 요청을 최소화 할 수 있다.
    2. DB 롹킹 기간을 최소화 한다.
    3. JDBC batch API의 장점을 취할 수 있다.
  • flush(): DB와 Persistent Context의 동기화

    1. Transaction(하이버의 API)이 commit() 될 때.
    2. 쿼리를 실행하기 전에..
    3. session.flush()를 호출 할 때.
  • session.setFlushMode()를 사용할 수 있다. 기본값은 FlushMode.AUTO.
  • FlushMode.COMMIT으로 설정하면

    1. 쿼리를 실행하기 전에.. 동기화를 하지 않는다.
    2. 오직 Transaction.commit()과 session.flush()를 할때 만 동기화한다.
    3. 쿼리 -> 수정 -> 쿼리 -> 수정. 이런 경우에 FlushMode를 Commit으로 설정해주면 효율적이다.
  • FlushMode.MONUAL로 설정하면

    1. 오직 session.flush()를 호출 할 때만 동기화한다.
  • FlushMode 제어는 나중에 Persistent Context를 Conversation으로 확장할 때 사용할 것이다.
  • 중요한 것은 flush 처리의 성능은 persistent context의 크기에 따라 달라진다는 것이다.

하이버네이트 API : Detached 객체 다루기

수정된 detached 객체 reattaching 하기

  • update()를 사용하여 새로운 session에 reattach 할 수 있다.
item.setDescription(...); // Loaded in previous Session

Session sessionTwo = sessionFactory.openSession();
Transaction tx = sessionTwo.beginTransaction();

sessionTwo.update(item);

item.setEndDate(...);

tx.commit();
sessionTwo.close();
  • update()에 넘겨주는 객체가 그 전에 수정이 되었는지 안 되었는지는 중요하지 않다.
  • 새로운 Persistent Context에 묶이게 된다는 것이 중요하다.
  • 새로운 Persistent Context에서는 무조건 dirty로 인식하고 나중에 update문을 날리게 된다.
  • DB에서 실제 값과 비교하여 바뀐 부분이 있을 때에만 update문을 날리고 싶으면 select-before-update=”true”를 설정한다.
  • 수정되지 않았다는 것이 활실할 때는 update를 사용하지 않는 reattach 방법을 사용한다.

수정되지 않은 detacked 객체를 reattaching 하기

  • lock()을 사용하면 그 시점 부터 DB에 반영을 합니다. 그 이전의 변경 사항은 반영하지 않습니다.
Session sessionTwo = sessionFactory.openSession();
Transaction tx = sessionTwo.beginTransaction();

sessionTwo.lock(item, LockMode.NONE);

item.setDescription(...);
item.setEndDate(...);

tx.commit();
sessionTwo.close();
  • 위에서 사용한 LockMode.NONE은 새로운 객체를 Session에 붙일 때, 버전 확인이나 DB 레벨의 롹을 가져오지 않겠다는 설정입니다.

detacked 객체를 transient 객체로 만들기

  • update()나 lock()를 호출하열 필요 없이 그냥 delete()를 사용합니다.
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

session.delete(item);

tx.commit();
session.close();
  • 그 안에서 해당 객체를 다시 Session에 붙였다가 tx.commit()이 호출되는 시점에 DB에서 삭제합니다.

detached 객체 mergeing 하기

  • reattachment의 보완제이자 대체제다.
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();
  • NonUniqueObjectException: reattack 할 때, 이미 동일한 레코드를 표현하는 객체가 해당 Persistent Context에 존재할 때 발생하는 예외.
  • Persistent Context에 만약, 같은 DB 레코드를 나타내는 객체가 있다면, detached 객체의 값들을 해당 객체로 복사한다.
  • Persistent Context에 존재하지 않는 객체일 경우에는, DB에서 같은 id를 가진 녀석을 읽어온 다음에 merge() 한다.
  • detached객체나, 기존의 Persisitent Context에 있던 객체의 사용은 지양하고, merge()가 반환하는 객체를 사용하도록 한다.

하이버네이트 API: 저장하고 읽어들이기

Unit of work 시작하기

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
  • SessionFactory는 매번 생성하지 말 것. 비싸다. Spring에서는 singleton으로 사용하니까 그럴 걱정은 없겠군. Thread Safe 하겠지?
  • Session은 비싸지 않다. 필요하기 전까지는 JDBC Connection조차 가져오지 않는다.
  • 위에서는 하이버의 Transaction API를 사용해지만, 꼭 저것만 사용해야 하는 건 아니다. 다음 챕터에서 자세히 다룬다.

객체를 persistent 상태로 만들기

Item item = new Item();
item.setName("Playstation3 incl. all accessories");
item.setEndDate( ... );

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

Serializable itemId = session.save(item);

tx.commit();
session.close();
  • save()를 호출하면 persistent context에 묶이게 된다.
  • 언젠가는 이 persistent context와 DB가 동기화 되어야 한다.
  • Transaction의 commit()을 호출할 때 flush가 일어나고, 이것을 flush()로 명시적으로 호출할 수도 있다.
  • session.save()이후의 모든 변경사항들은 추가적인 update문으로 DB에 반영된다.
  • session.beginTransaction()와 tx.commit()사이의 변경 사항들을 DB에 반영할 때, 예외가 발생하면 모든 SQL을 롤백한다.
  • 그러나 Persistent 객체의 메모리 상태는 롤백되지 않는다.

Persistent 객체 가져오기

  • 두 가지 방법이 있다. get(), load()
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

Item item = (Item) session.load(Item.class, new Long(1234));
// Item item = (Item) session.get(Item.class, new Long(1234));

tx.commit();
session.close();
  • session에서 가져오는 순간 persistent 상태이며, session이 끝나는 순간 detached 상태가 된다.
  • id에 대응하는 레코드가 없을 때, get()은 null을 반환한다. load()는 ObjectNotFoundException을 던진다.
  • load() 메소드는 실제로 DB에 다녀오지 않고 Proxy를 반환한다. 단, 현재 Persistent Context에 이미 대응하는 id를 가진 객체가 존재한다면, 초기화까지 되어 있는 객체를 돌려준다.
  • get() 메소드는 절대로 Proxy를 반환하지 않는다. 항상 DB에 다녀온다.
  • DB에 있는 객체 하나를 꺼내서 다른 객체에 세팅해줘야 하는 경우.
    Comment.setForAuction(item). 이 경우 굳이 item은 load()로 가져와도 된다. 굳이 DB에서 전부
    가져올 필요가 없다. Comment를 저장할 때, item의 id를 외례키로 저장하게 되는데, load()로 가져온 Proxy가
    딱 그 id만 가지고 있기 때문이다.

Persistent 객체 수정하기

  • Persistent 상태의 객체는 tx.commit()을 호출할 때 flush()를 통해서 DB에 반영된다.
  • automatic dirty checking

Persistent 객체를 Transient 상태로 만들기

  • delete()를 호출하면 Remonved 상태가 된다.
  • DB와 동기화는 Unit of work가 끝날 때 이루어 지고, 그러면 Transient 상태가 된다.
  • 그런데.. id값을 가지고 있는데, transient 객체라고 할 수 있는건가.. detached 객체랑 차이가 뭘까?
    => Transient 상태가 될 때, 식별자도 제거하려면 hibernate.use_identifier_rollback 이 설정을 해줘야돼.
sessionFactory 설정
<prop key="hibernate.use_identifier_rollback">true</prop>
session = sessionFactory.openSession();
transaction = session.beginTransaction();

session.delete(member);

transaction.commit();
session.close();
assertNull(member.getId());

객체 복사하기

  • 좀 특별한 경우에 한쪽 DB에서 가져온 다음에 다른쪽 DB에 저장하는 경우가 있을 수 있습니다.
  • 서로 다른 SessionFactory에서 얻어온 Session 사이에서 detached 객체를 받아서 persistent 객체로 전환해 줍니다.
Session session = sessionFactory1.openSession();
Transaction tx = session.beginTransaction();
Item item = (Item) session.get(Item.class, new Long(1234));
tx.commit();
session.close();

Session session2 = sessionFactory2.openSession();
Transaction tx2 = session2.beginTransaction();
session2.replicate(item, ReplicationMode.LATEST_VERSION);
tx2.commit();
session2.close();
  • ReplicationMode

    1. ReplicationMode.IGNORE: 같은 id를 가진 녀석이 이미 존재하면, 그 객체를 넣지 않는다.
    2. ReplicationMode.OVERWRITE: 같은 id를 가진 녀석이 존재하면, 덮어쓴다.
    3. ReplicationMode.EXCEPTION: 같은 id를 가진 녀석이 존재하면, 예외를 던진다.
    4. ReplicationMode.LATEST_VERSION:
      같은 id를 가진 녀석의 버전을 확인해서 새것 보다 이전 버전이면 덮어쓰고 아니면 넣지 않는다. 이 걸 사용하려면 하이버의
      optimistic concurrency control을 켜둬야 한다.