Java

[Spring]Transactional 어노테이션 사용 및 롤백 처리

DevStory 2022. 4. 14.

스프링의 서비스 계층에서 DataBase의 데이터를 변경하다 문제가 발생하는 경우 롤백하기 위해 @Transactional 어노테이션을 사용합니다. 일반적으로 트랜잭션 어노테이션에 대해 "문제가 생기면 알아서 처리해주는구나..." 여기까지만 이해하고 그 이상으로 생각하지 않습니다. 저도 이때까지 트랜잭션 어노테이션에 대해 깊게 생각하지 않았는데 스프링을 다시 공부하면서 몇 가지 의문이 들었습니다.

 

「 트랜잭션 어노테이션이 정말 모든 예외 및 에러에 대해 롤백을 해줘? 」

 왜 Service 계층에서 작성하지?

 내가 트랜잭션 격리 수준에 대해 얼마나 알고 있지?

 예외 및 에러를 어떻게 감지하고 롤백 처리하지?

 

그 외 여러 가지 의문점도 있었지만 저는 트랜잭션 어노테이션이 모든 예외 및 에러에 대해 롤백 처리해주는지 궁금했습니다.

 

이번 포스팅은 트랜잭션 어노테이션 사용 방법과 실수하고 있는 내용들을 제가 알고 있는 범위에서 소개합니다. 트랜잭션 어노테이션 위주로 설명하기 때문에 기본적인 내용은 설명하지 않습니다.


트랜잭션 어노테이션 속성

아래는 트랜잭션 어노테이션 속성들입니다.

 

propagation

: 트랜잭션에서 다른 트랜잭션을 호출할 때 어떠한 방식으로 처리할지 설정하는 것을 '트랜잭션 전파 옵션'이라고 합니다. propagation 어노테이션으로 트랜잭션 전파 옵션을 정의합니다.

 

timeout 및 timeoutString

: 트랜잭션 제한 시간을 정의합니다. 제한 시간을 초과하면 예외가 발생합니다.

 

readOnly

: 현재 트랜잭션이 읽기 전용인지 읽기-쓰기 전용인지 정의합니다.

 

rollbackFor 및 rollbackForClassName

: 지정한 예외가 발생하면 롤백되도록 Throwable 클래스를 정의합니다.

 

noRollbackFor 및 noRollbackForClassName

: 지정한 예외가 발생하지 않더라도 롤백되지 않도록 Throwable 클래스를 정의합니다.


Service 계층에서 작성하는 이유

일반적으로 트랜잭션 어노테이션은 서비스(Service) 계층에 작성합니다. 일반적이라고 말하는 이유는 회사 또는 프로젝트마다 아키텍처가 다를 수 있기 때문입니다.

 

대부분 아시듯이 서비스 계층은 비즈니스 로직을 수행합니다. DataBase의 데이터를 변경하기 위해 DAO를 호출할 수 있으며 한 메서드에 여러 개의 DAO를 호출할 수 있습니다.

@Transactional
public void Save1(Model model) {
  dao.insert(model);
}

@Transactional
public void Save2(Model model) {
  dao.insert(model.getAdded());
  
  dao.update(model.getUpdated());
  
  dao.delete(model.getDeleted());
}

위 예제는 서비스에 한 개의 DAO를 호출하는 Save1() 메서드와 세 개의 DAO를 호출하는 Save2() 메서드가 존재합니다.

 

트랜잭션 어노테이션이 서비스 계층이 아니라 DAO에 있다고 가정해보겠습니다. Save2() 메서드에서 dao.insert() 작업이 실패했는데 dao.update()와 dao.delete()는 작업이 성공적으로 수행될 수 있습니다. 이렇게 되면 일관성 없는 데이터가 존재하므로 트랜잭션 성질 중 하나인 일관성(Consistency)을 위반하게 됩니다.

반응형

모든 예외 및 에러를 롤백하는가?

트랜잭션 어노테이션은 모든 예외 및 에러에 대해 롤백 처리하지 않습니다. Runtime Exception 또는 Error가 발생한 경우 롤백됩니다. 그러나 Checked Exception에 대해서는 롤백되지 않습니다.

Runtime Exception
개발자가 처리하기 어려운 예외로 말 그대로 프로그램 실행 중에 발생하는 예외를 의미합니다.

Checked Exception
프로그램이 제어할 수 없지만 개발자가 충분히 처리 가능한 예외입니다.

Error
Exception이 아닌 경우로 시스템 메모리 부족처럼 예측 및 처리가 어렵습니다.

위에서 말했듯이 트랜잭션 어노테이션은 Runtime Exception 또는 Error가 발생한 경우 롤백 처리합니다. 그렇기 때문에 아래 코드는 동일합니다.

@Transactional

@Transactional(rollbackFro = {RuntimeException.class, Exception.class})

 

다음은 강제로 Checked Exception를 발생시켜 롤백되는지 확인해보겠습니다. 먼저 트랜잭션 어노테이션에 Checked Exception을 설정하지 않은 예제입니다.

@Transactional
public void saveModel(Model model) throws Exception {
  dao.save(model);
 
  ArrayList<Integer> arr = null;
  int size = getArraySize(arr);
}
 
public int getArraySize(ArrayList li) throws Exception {
  if(li == null){
    throw new Exception();
  }
  return li.size();
}

dao.save(model)이 정상적으로 수행되었고 사용자 정의 메서드인 getArraySzie() 메서드에서 예외가 발생하면 롤백이 되지 않습니다. getArraySize() 메서드에서 발생한 예외는 Checked Exception이기 때문입니다.

 

Checked Exception 예외도 롤백되게 하려면 트랜잭션 어노테이션에 Exception.class를 rollbackFor에 추가합니다. 그러면 Runtime Exception과 Checked Exception를 포함한 모든 예외를 롤백시킵니다.

@Transactional(rollbackFor = {Exception.class})
public void saveModel(Model model) throws Exception {
  dao.save(model);
 
  ArrayList<Integer> arr = null;
  int size = getArraySize(arr);
}
 
public int getArraySize(ArrayList li) throws Exception {
  if(li == null){
    throw new Exception();
  }
  return li.size();
}

위 예제는 사용자 정의 메서드인 getArraySize() 메서드에서 예외가 발생하면 정상적으로 롤백됩니다.

Exception.class를 설정하는게 맞을까?

스프링은 왜 Checked Exception을 Default로 설정하지 않았을까? 위에서 언급했듯이 Checked Excpetion은 개발자가 직접 처리할 수 있기 때문입니다.

 

즉, 서비스에서 Checked Excpetion이 발생했는데 롤백되지 않은 경우는 개발자가 예외 처리를 제대로 하지 않아서 발생한 문제로 간주합니다.


롤백 예제

다음은 롤백이 되는 경우와 안 되는 경우를 보여주는 예제입니다.


RuntimeException 발생

@Transactional
public void saveModel(Model model) throws Exception {
  dao.save(model);
  throw new RuntimeException();
}

위 예제 코드는 정상적으로 롤백됩니다. 위에서 언급했지만 트랜잭션 어노테이션은 기본적으로 Runtime Exception와 Error가 발생하면 롤백시킵니다.


SQLException 발생 A.

@Transactional
public void saveModel(Model model) throws Exception {
  dao.save(model);
  throw new SQLException();
}

SQLException은 Checked Exception에 포함되는 예외입니다. 트랜잭션 어노테이션은 기본적으로 Checked Exception이 발생하더라도 롤백되지 않습니다.


SQLException 발생 B.

@Transactional(rollbackFor = SQLException.class)
public void saveModel(Model model) throws Exception {
  dao.save(model);
  throw new SQLException();
}

위 예제 코드는 SQLException 예외가 발생하면 롤백되도록 설정하였습니다. 정상적으로 롤백됩니다.


try-catch 블록

@Transactional
public void saveModel(Model model) throws Exception {
  try {
    dao.save(model);
    throw new RuntimeException();
  } catch (Exception e) {
    System.out.println("예외 발생");
  }
}

catch문에서 예외를 캐치 후 처리했기 때문에 롤백되지 않습니다. 트랜잭션이 정상적으로 실행되고 커밋됩니다.

 

반응형

'Java' 카테고리의 다른 글

[Java]리플렉션(Reflection)  (0) 2022.05.08
[Java]반복자(Iterator)  (0) 2022.05.02
[Java]객체(Object)를 XML로 변환  (0) 2022.04.11
[Java]main 함수(메서드)  (0) 2022.04.10
[Java]현재 날짜 및 시간 가져오기  (0) 2022.04.09

댓글