ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring 선언적 트랜잭션과 checked exception
    카테고리 없음 2023. 4. 22. 14:39
    오늘은 선언적 트랜잭션과 체크예외에 대한 이야기를 아주 간략히 해보겠다. 결론부터 말하자면 선언적 트랜잭션을 사용할 때 체크드 예외가 발생하면 롤백을 하지 않는다. 솔직하게 별생각 없이 썼다. 그래서 더 안타깝다.. 아직 갈길이 멀다. 조금만 더 관심있게 봤으면 알았을 텐데 말이다. 그런데 왜 굳이 Spring에서는 체크예외 일때 롤백을 하지 않을까? 그 이유는 뭐 간단하게 생각할 수도 있다. Spring의 예외전략은 언체크드 예외이다. 기본 java에서는 데이터베이스에 쿼리를 날리거나 연결을 할 때 에러가 발생하면 체크드 예외를 던진다. 보통 일반적으로 SqlException을 던지고 이 예외는 체크드 예외이다. 하지만 Spring에서 체크드 예외를 언체크드 예외로 포장해서 우리에게 던져준다. 그 이유는 토비의 봄 4장인가? 거기에 보면 Spring의 예외 기본전략이 나오니 참고를 하자. 간단히 설명하자면 데이터베이스에 예외가 났을 경우 거의 99프로가 복구 할 수 없는 예외이다. 개발자의 잘못으로 쿼리가 잘 못 작성 되었거나 서버가 죽어 있다면 복구 불가능한 예외이다. (물론 연결 상태가 안좋은거라면 복구를 할 수 있겠지만..) 그래서 Spring에서는 복구할 수 없는 예외로 판단하여 굳이 체크드 예외를 던지지 않고 우리에게 포장해서 던져준다. 이 뿐만 아니라 Spring에서는 거의 대부분 언체크드 예외를 던지지 체크드 예외를 던지는 경우는 드물다고 판단된다. 토비의 봄 (http://wonwoo.ml/index.php/post/878)
    먼저 생각해볼 사항은 SQLException은 과연 복구가 가능한 예외인가이다. 99%의 SQLException은 코드 레벨에서는 복구할 방법이 없다. 프로그램의 오류 또는 개발자의 부주의 때문에 발생하는 경우이거나 통제할 수 없는 외부상황 떄문에 발생하는 것이다. 예를 들어 SQL 문법이 틀렸거나, 재약조건을 위반, DB 서버가 다운됐다거나 네트워크 불안정, DB 커넥션 풀이 꽉차서 DB 커넥션을 가져올 수 없는 경우 등이다. 시스템의 예외라면 당연히 애플리케이션 레벨에서 복구할 방법이 없다. 관리자나 개발자에게 빨리 예외가 발생했다는 사실이 알려지도록 전달하는 방법밖에는 없다.
    ...생략
    
    Object retVal = null;
        try {
      // This is an around advice: Invoke the next interceptor in the chain.
      // This will normally result in a target object being invoked.
      retVal = invocation.proceedWithInvocation();
    }
        catch (Throwable ex) {
      // target invocation exception
      completeTransactionAfterThrowing(txInfo, ex);
      throw ex;
    }
    
    //생략
    
    위의 소스는 Spring의 트랜잭션 처리를 위한 클래스의 일부분이다. 얼핏 보면 모든(Throwable) 예외를 잡는 듯해 보인다. 뭐 물론 위의 코드는 모든 예외를 잡는 코드가 맞긴하다. completeTransactionAfterThrowing() 메서드를 추적해보자.
    if (txInfo.transactionAttribute.rollbackOn(ex)) {
      try {
        txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
      } catch( ... ) {
        ...
      }
      //..생략
    }
    else {
    
      try {
        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
      }catch( ... ) {
        ...
      }
      //생략
    }
    
    위의 소스는 completeTransactionAfterThrowing() 메서드의 일부분이다. 가만보면 롤백을 하기 전에 무엇가를 체크를 한다. 저게 무언가 추적해보자. 위의 코드를 계속 추적해보면 아래와 같은 소스를 발견 할 수 있다.
    @Override
    public boolean rollbackOn(Throwable ex) {
      return (ex instanceof RuntimeException || ex instanceof Error);
    }
    
    범인은 여기에 있다. 타입이 RuntimeException 이거나 Error 일때 를 확인하는 부분이다. 아까 위의 코드에서 확인하는 것이 바로 RuntimeException, 이거나 Error 에러가 발생 하였을 경우에만 롤백을 실시한다. 그렇다면 만약 체크드 예외도 롤백을 시키고 싶다면 어떻게 할까? @Transactional 속성에 보면 rollbackFor 속성이 존재한다. 거기에 해당하는 exception을 작성해 주면 된다.
    @Transactional(rollbackFor = Exception.class)
    public void save() throws Exception {
      // 블라블라
    }
    
    위와 같이 설정하면 체크드 예외도 롤백을 할 수가 있다. 그런데 여기서 한번 생각을 해보자. 과연 저렇게 쓸 일이 얼마나 있을까? 만약 어떤 메서드에서 체크드예외를 던진다면 우리는 꼭 exception을 잡거나 위와 같이 상위 메서드로 던져야 한다. 그런데 만약 exception을 잡지 않고 상위 메서드로 던진다면 상위 메서드에서도 exception을 잡아야 한다. 그렇다면 과연 위와 같이 쓰는게 올바른 코드일까? 물론 틀렸다고 할 수 는 없겠지만 저렇게 무책임하게 예외를 던지는 것은 바람직하지 못하다고 생각된다. 그래서 우리는 체크드 예외가 발생하였을 경우에는 예외를 잡아 포장하여 던져주는게 좀 더 나은 코드가 되지 않을까 생각된다.
    @Transactional
    public void save()  {
      try{
        filewirte(file);
      } catch (Exception e){
        throw new RuntimeException("file not found");
      }
      //..blabla
    }
    
    대부분의 웹 프로그래밍을 하면 체크드 예외는 사용하지 않겠지만 라이브러리, 프레임워크 등 코어 개발자라면 체크드 예외도 종종 사용한다. 예를들어 꼭 알려야 하는 예외(파일을 쓰지 못한다거나, 서버와 커넥션이 안되었거나 기타 등등)가 발생하였을 경우에는 체크드 예외를 사용해서 상위 메서드에게 알려줘야 한다. 그건 코어 개발자의 몫이지 우리는 예외를 잡아서 포장해서 던지면 된다. 뭐 물론 체크드 예외를 던지지 말라는 건 아니지만 왜 그래야만 하는지 좀 더 생각해 볼 필요가 있을 듯 하다. 과장님 덕분에 한개 더 배웠네요..

    댓글

Designed by Tistory.