오늘은 선언적 트랜잭션과 체크예외에 대한 이야기를 아주 간략히 해보겠다.
결론부터 말하자면 선언적 트랜잭션을 사용할 때 체크드 예외가 발생하면 롤백을 하지 않는다.
솔직하게 별생각 없이 썼다. 그래서 더 안타깝다.. 아직 갈길이 멀다. 조금만 더 관심있게 봤으면 알았을 텐데 말이다.
그런데 왜 굳이 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
}
대부분의 웹 프로그래밍을 하면 체크드 예외는 사용하지 않겠지만 라이브러리, 프레임워크 등 코어 개발자라면 체크드 예외도 종종 사용한다. 예를들어 꼭 알려야 하는 예외(파일을 쓰지 못한다거나, 서버와 커넥션이 안되었거나 기타 등등)가 발생하였을 경우에는 체크드 예외를 사용해서 상위 메서드에게 알려줘야 한다. 그건 코어 개발자의 몫이지 우리는 예외를 잡아서 포장해서 던지면 된다. 뭐 물론 체크드 예외를 던지지 말라는 건 아니지만 왜 그래야만 하는지 좀 더 생각해 볼 필요가 있을 듯 하다.
과장님 덕분에 한개 더 배웠네요..