Whiteship's Note

Isolation 단계 더 높이기

Hibernate/Chapter 10 : 2008.03.30 00:25


Explicit pessimistic locking

  • 격리 수준을 read comitted 보다 높게 설정하는 것은 애플리케이션의 확장성을 고려할 때 좋치 않다.
  • Persistence context cache가 repeatable read를 제공하긴 하지만 이걸로 항상 만족스럽지 않을 수도 있다.
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
Item i = (Item) session.get(Item.class, 123);
String description = (String)
session.createQuery("select i.description from Item i" +
" where i.id = :itemid")
.setParameter("itemid", i.getId() )
.uniqueResult();
tx.commit();
session.close();
  • 위의 코드는 DB에서 같은 데이터를 두 번 읽어온다. 이 때 isolation level이 read committed 였다면, 두 번째에 읽어오는 값이 처음 읽어온 데이터와 다를 수 있다.(둘 사이에 어떤 트랜잭션이 해당하는 값을 바꾸고 커밋했을 수 있다.)
  • 전체 트랜잭션의 isolation level을 높이는 것이 아니라 lock() 메소드를 사용하여 해당하는 부분의 트랜잭젼의 isolation level을 높일 수 있다.
Session session = sessionFactory.openSession(); 
Transaction tx = session.beginTransaction();
Item i = (Item) session.get(Item.class, 123);

session.lock(i, LockMode.UPGRADE);

String description = (String)
session.createQuery("select i.description from Item i" +
" where i.id = :itemid")
.setParameter("itemid", i.getId() )
.uniqueResult();
tx.commit();
session.close();
  • 위의 LockMode.UPGRADE 는 item 객체 대응하는 레코드의 pessimistic lock을 가지고 다니게 된다.
Item i = (Item) session.get(Item.class, 123, LockMode.UPGRADE);
  • 위와같이 코드를 한 줄 줄일 수도 있다.
  • LockMode.UPGRADE는 롹을 가져올 때까지 대기하게 된다. 대기 시간은 사용하는 DB에 따라 다르다.
  • LockMode.NOWAIT는 롹이 없으면 기다리지 않고 쿼리가 바로 fail하도록 한다.

The Hibernate lock modes

  • LockMode.NONE - 락 사용하지 않음. 캐시에 객체가 존재하면 그 객체를 사용.
  • LockMode.READ - 모든 캐시를 무시하고 현재 메모리에 있는 엔티티의 버전과 실제 DB에 있는 버전이 같은지 확인한다.
  • LockMode.UPGRADE - LockMode.READ가 하는 일에 더해서 DB에서 pessimistic upgrade lock을 가져온다. SELECT ... FOR UPDATE 문을 지원하지 않는 DB를 사용할 때는 자동으로 LockMode.READ로 전환된다.
  • LockMode.UPGRADE_NOWAIT - UPDGRADE와 동일한데, SELECT ... FOR UPDATE NOWAIT를 사용한다. 락이 없으면 바로 예외를 던진다. NOWAIT를 지원하지 않으면 자동으로 LockMode.UPGRADE로 전환된다.
  • LockMode.FORCE - 객체에 버전을 DB에서 증가시키도록 강제한다.
  • LockMode.WRITE - 하이버네이트가 현재 트랜잭션에 레코드를 추가했을 때 자동으로 얻어온다.(사용자가 명시적으로 애플리케이션에서 사용할 일 없음.)
  • load()와 get()은 기본으로 LockMode.NONE을 사용한다.
  • Detached 상태의 객체를 reattach 할 때 LockMode.READ 를 유용하게 사용할 수 있다. 자동으로 reattach까지 해주니까.(하이버네이트의 lock()메소드만 reattch까지 해주지, JP의 lock()메소드는 reattch해주지 않느다.)

reattche를 할 때 반드시 lock() 메소드를 사용해야 하는 것은 아니다. 이전에도 살펴봤듯이 Session에 update() 메소드를 사용하면 Transiecnt 상태의 객체가 Persistent 상태가 된다. lock(LockMode.READ)는 Persistent 상태로 사용하려는 객체의 데이터들이 이전에 로딩된 그 상태 그대로 인지, 혹시 다른 트랜잭션에 의해 데이터들이 변경되지는 않았는지 확인하기 위한 용도다. 그렇게 확인을 함과 동시에 덤으로 Persistent 상태로 전환(reattach)시켜 주는 것이다. 즉, Transient 상태의 객체를 lock() 메소드의 인자로 넘겨줄 수 있다는 것인데, 이것은 하이버네이트에서만 할 수 있다. JP에서는 이미 Persistent 상태인 객체한테만 lock()을 호출할 수 있다.

Item item = ... ; 
Bid bid = new Bid();
item.addBid(bid);
...
Transaction tx = session.beginTransaction();
session.lock(item, LockMode.READ);
tx.commit();

Forcing a version increment

  • 하이버네이트가 개발자가 수정한 내용을 버전을 올려야 하는 변경사항인지 모를 수가 있다. 이럴 때 명시적으로 버전을 올리라고 알려줘야 한다.
Session session = getSessionFactory().openSession(); 
Transaction tx = session.beginTransaction();

User u = (User) session.get(User.class, 123);
u.getDefaultBillingDetails().setOwner("John Doe");

tx.commit();
session.close();
  • 하이버네이트는 객체와 직접적으로 닿아있는 값의 변화만을 알지 한 단계 걸친 변화는 해당 객체의 수정사항으로 인식하지 않는다.
  • 위의 코드에서 BillingDetail 객체만 버전을 올리게 된다. 하지만 개발자는 정보를 수정한 BillingDetail(aggregate)을 가지고 있는 User(root object) 역시 버전을 올리고 싶어 할 수 있다.
Session session = getSessionFactory().openSession(); 
Transaction tx = session.beginTransaction();

User u = (User) session.get(User.class, 123);
session.lock(u, LockMode.FORCE);
u.getDefaultBillingDetails().setOwner("John Doe");

tx.commit();
session.close();
  • 이렇게 하면 현재 User 객체에 대응하는 레코드를 가지고 작업하고 있는 모든 Unit of work들이 해당 객체의 버전이 올라갔다고 인식한다.
  • 심지어 아무런 변경을 가하지 않았더라도 해당 Root 객체의 버전은 올라간다.

공부할 것

  • 사용하는 DB에 따라 다른 결과가 나올 수 있다. Select ... FOR UPDATE NOWAIT 문을 지원하느냐 안 하느냐에 따라 LockMode.UPGRADE 와 LockMode.UPGRADE_NOWAIT의 결과가 LockMode.READ 와 같게 나올 수도 있다.
  • 결국 원하는 Isolation level을 정하는 것이 중요하고, repeatable read를 보장하려면 LockMode.UPGRADE 또는 LockMode.UPGRADE_NOWAIT를 사용하여 pessimisitc locking하면된다.
  • LockMode.READ는 DB에서 데이터를 읽어와서 버전을 확인한다. isolation level을 미리 올려두는 것이 아니라, optimistic 한 방법으로 DB에 쓰기 직전에 확인하기 위한 용도라고 생각된다.
  • 자동 버전 증가는 오직 엔티티가 직접적으로 물고 있는 속성, 콜렉션 자체의 변화만 인식한다. 객체 맵의 루트를 올려야 한다면, 해당 루트 객체를 가져올 때 LockMode.FORCE를 사용하며 이 녀석은 isolation level과 별 상관이 없어 보이지만, 해당 엔티티를 사용하는 트랜잭션들의 isolation level을 repeatable read로 보장해야 하는 경우에 유용하게 사용할 수 있을 것 같다.
  • 결국 테스트 코드를 많이 만들어서 테스트해봐야겠다.
top

  1. Favicon of https://jjaeko.tistory.com BlogIcon 째코 2008.03.30 20:47 신고 PERM. MOD/DEL REPLY

    잘 봤습니다.
    락모드에 대해서는 공부하지 않아서 좀 이해하지 못한 부분이 있는데..
    UPGRADE와 UPGRADE_NOWAIT 는 객체를 가져오는 시점에서 락을 걸어버리는 pessimitic-lock 이고 그 외의 모드들은 optimistic-lock 과 관련된 부분인가요?

    Favicon of http://whiteship.tistory.com BlogIcon 기선 2008.03.30 22:29 PERM MOD/DEL

    네. 그렇습니다.

    해당 row에 접근할 때 롹을 하는 pessimistic lock을 사용하는 것이 UPGRADE와 UPGRADE_NOWAIT이구요.

    나머지 중에서 NONE은 optimistic한 동시성 처리를 하지 않겠다는 것입니다. 이 말은 다시 말하자면, first-win이 아니라 last-win 전략을 사용하겠다는 것이고, 사용자에게 불편함을 끼칠 우려가 많은 전략입니다.

    그래서 READ를 사용해서 데이터의 변경사항을 DB에 반영하기 전에 혹시 그 변경사항과 관련된 객체들에 대응하는 레코드가 이미 다른 트랜잭션에 의해 변경되지 않았는지 확인해서 first-win 전략을 사용할 수 있습니다.(automatic optimistic locking)

    그런데, READ와 같이 데이터를 먼저 쓰는 시점에 우선권을 주는것이 아니라 데이터에 먼저 접근하는 트랜잭션에게 우선권을 주는 pessimistic locking을 하려면 UPGRADE와 UPGRADE_NOWIAT을 사용할 수 있는 것 입니다. 그런데 이 두 개의 롹 모드는 사용하는 DB에서 SELECT ... FOR UPDATE를 지원할 때만 그렇게 동작이 되고, pessimistic locking을 지원하지 않는 DB에다가 대고 위와 같은 락모드를 설정하면 결국 READ와 동일하게 동작합니다.

    따라서 락모드를 설정하는 두 가지 방법이 있는데, optimistic locking을 할 때는 session.lock()을 사용하고, pessimistic locking을 사용할 때는 session.load()나 get()을 할 때 롹모드를 주는 것이 직관적이고 이해하기 쉬운 코드가 될 것으로 생각합니다.

Write a comment.