Whiteship's Note

스프링 트랜잭션 주의할 것

Spring/etc : 2009.02.07 20:46


Transaction strategies: Understanding transaction pitfalls

@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
   // 어떤 코드
}

위 코드에 있는 insertTrade를 실행행하면 어떤 결과가 발생할까요?

A. read-only connection 예외가 발생한다.
B. 데이터를 추가하고 커밋한다.
C. read-only가 true라서 아무것도 하지 않는다.

당연히 A일 것 같은데... // 어떤 코드 부분이 JDBC 코드일 경우이고 JPA나 하이버네이트 같은 ORM 코드면 propagation 설정 REQUIRED가 모든 것을 재정의해서 새로운 트랜잭션을 시작하고 read-only 플래그가 없는 것 처럼 동작하게 된다네요... @_@

지금은 넘 졸려서 낼 자세히 읽어봐야겠습니다.

따라서 읽기 전용 매서드의 경우 다음과 같이 SUPPORTS 프로퍼게이션 모드를 사용하는게 타당하다고 합니다. 왜냐면 보통 다음과 같이 읽기 전용 매서드를 설정하는데..

@Transactional(readOnly = true)
public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
}

이 때 기본 프로퍼게이션이 REQUIRED 모드기 때문에 매번 새로운 트랜잭션을 만들어 사용하고 사용하는 DB에 따라서는 불필요한 읽기 롹까지 사용해서 데드락을 발생시킬 수도 있다고 하기 때문입니다. 따라서..

@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
}

이렇게 SUPPORTS로 변경하여 기존 트랜잭션이 있으면 사용하고 없으면 사용하지 않도록 하거나..

public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
}

요렇게 읽기 전용인 경우에는 아예 @Transactional 애노테이션을 아예 사용하지 않는게 나아보입니다. 굳이 원자화 할 것도 없고~ Isolation level만 적당선에서 타협한다면 롹을 걸 일도 없고~


REQUIRES_NEW  사용시 주의 할 것.

@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {...}

@Transactional(propagation=Propagation.REQUIRES_NEW)
public void updateAcct(TradeData trade) throws Exception {...}

이런 매서드와 트랜잭션 설정이 있을 때

@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {
   em.persist(trade);
   updateAcct(trade);
   //exception occurs here! Trade rolled back but account update is not!
   ...
}

이런 식으로 코딩하면... updateAcct() 이후에 에러가 발생하면 updateAcct는 쿼리가 날아가는데 나머지 내용은 롤백 되는 현상이 발생할 수 있죠. 왜냐면 매번 새로운 트랜잭션을 만들기 때문에 insertTrade를 실행하는 트랜잭션과 updateAcct를 실행하는 트랜잭션이 별개기 때문입니다.

뭐~ 이건 쉽네요. 걍 REQUIRED로 쓰거나 MANDATORY를 쓰면 됩니다.

롤백과 관련해서는 checked exception에 대비해야 한다.

@Transactional(propagation=Propagation.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //log the error
      throw up;
   }
}

이런 코드가 과연 안전할까? 아니요. 왜냐면 저 코드에서 만약 updateAcct() 매서드 처리 도중에  checked exception이 발생하면 insertTrade() 매서드는 그대로 실행하고 예외도 그냥 던지고 말기 때문에 데이터가 불안전한 상태가 될 것입니다. 따라서 checked exception에 대비해서..

@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //log the error
      throw up;
   }
}

이런 식으로 하는게 좋겠습니다.



top

  1. 권남 2009.03.09 16:19 PERM. MOD/DEL REPLY

    Spring 2.5, Hibernate 3.3, @Transactional(readOnly = true, propagation=Propagation.REQUIRED) 이 상태에서 insert를 할경우, 왜 난 readOnly니까 쓰기 작업을 할 수 없다는 오류가 날까요?
    원문을 봐도 마찬가지던데.. 내가 뭔가를 잘못이해하고 있나봐요.

    Favicon of https://whiteship.tistory.com BlogIcon 기선 2009.03.09 16:48 신고 PERM MOD/DEL

    그 매서드 안에 코드는 하이버네이트 Session을 사용하신거겠죠?

    음.. 아마도 트랜잭션을 저 매서드를 만날 때 새로 만든게 아니라 기존에 있던 트랜잭션을 사용하는거라 그렇게 잘 동작한게 아닐까 싶네요.

    OSIV 필터 같은 것을 쓰신다면 요청이 들어왔을 때 쓰레드로컬에 미리 만들어 두니까요.

  2. Favicon of http://kwon37xi.egloos.com BlogIcon 권남 2009.03.09 20:14 PERM. MOD/DEL REPLY

    -- 수정: 이 댓글은 오류를 포함하고 있습니다.

    문제점을 확인하였습니다.
    문서를 자세히 보면

    For some vendors, such as Hibernate, the flush mode will be set to MANUAL, and no insert will occur for inserts with non-generated keys

    이 부분이 있는데, 이런 현상은 non-generated key(auto_increment를 사용하지 않고, 키 값이 객체에 직접 지정되는 방식)이고, Flush 모드가 Manual(기본 모드죠?) 이면서, insert/update 등을 할 때 발생합니다.
    내생각엔 generated key 일 경우일지라도, 시퀀스를 사용해서 키를 생성하는 DB에서는 insert가 될 것으로 보입니다. sequence에서 키값 가져오는 것은 read 작업이라서 그냥 해버리고 말 수도 있으니까(이건 단순 가정임).

    그 경우, 트랜잭션 안에서 insert가 일어나지 않고, 스프링의 트랜잭션 영역을 빠져 나간뒤에 한꺼번에 insert가 일어나기 때문에 스프링의 트랜잭션 설정의 영향을 받지 않게 됩니다.

    실제로 트랜잭션 종료 직후 시점에서 로그를 찍어보니, 트랜잭션이 종료된 이후에 insert statement가 실행되었습니다.

    아무때나 이런 현상이 일어나는 것은 아니었던 것이죠.

  3. Favicon of http://kwon37xi.egloos.com BlogIcon 권남 2009.03.09 20:16 PERM. MOD/DEL REPLY

    위 세번째 댓글은 저의 착각이었습니다. 다른 readOnly=false 트랜잭션이 걸린 서비스로 감싼 상태에서 안쪽 서비스 메소드의 readOnly=true 만 보고서 읽기 전용에서 호출했다고 착각했네요.

    제가 찾은 결론은, 위와 같은 readOnly=true 상태에서, insert,update 되는 현상은 나타나지 않는 다는 것입니다.

    스프링의 hibernateTemplate을 통해 save 메소드를 타게 되면 스프링 내부에서 checkWriteOperationAllowed(session) 메소드로 무조건 트랜잭션이 writable한지 여부를 검사합니다.
    따라서 HibernateTemplate을 사용하면 위 문서에서 말한 현상은 발생할 수 없습니다.
    update,delete도 마찬가지더군요.

    하지만 예외적으로, Spring HibernateTemplate을 사용하지 않고, Hibernate Session의 save 메소드를 직접 호출하면 readOnly=true인데 insert/update가 되는 현상이 발생합니다.
    트랜잭션의 writable 여부를 체크하는 것은 스프링이지 하이버네이트가 아니기 때문입니다. 스프링을 거치지 않고 Hibernate의 메소드를 바로 호출하면, 원문에 나온 트랜잭션의 writable여부와 무관하게 save되는 오류가 발생하게 됩니다.(그것도 non-generated id를 가진 엔티티의 경우에 한합니다).

    Favicon of http://whiteship.tistory.com BlogIcon 기선 2009.03.09 20:43 PERM MOD/DEL

    넹 위 본문에서도 스프링의 템플릿 클래스들이 아니라 JPA의 EntityManager나 하이버네이트의 Session을 사용할 때 발생할 수 있는 문제를 말해준 듯 합니다.

    저는 스프링의 템플릿 클래스들을 사용하지 않고 하이버네이트 API를 직접 이용해서 DAO를 구현하는게 더 깔끔해서 그렇게 사용하고 있습니다.

    따라서 결론을 약간 보완하여.. 스프링의 XXXTemplate 클래스를 사용하면 readOnly=true 상태에서 insert, update되는 현상은 나타나지 않는다. 라고 하는게 좋을 듯해 보이네요. ^^

  4. 권남 2009.03.09 23:53 PERM. MOD/DEL REPLY

    내가 HibernateTemplate 쓴다고 남들도 다 그렇겠거니 하는 생각에, EntityManager 를 못봤군요. --;
    암턴 올만에 소스 뒤져봤어요.. ^^

    Favicon of http://whiteship.me BlogIcon 기선 2009.03.10 09:52 PERM MOD/DEL

    캬~~ 멋지세요.
    하이버네이트 번역도 같이 하실래요. ㅋㅋ

Write a comment.




: 1 : ··· : 9 : 10 : 11 : 12 : 13 : 14 : 15 : 16 : 17 : ··· : 26 :