스프링을 이용해 트랜잭션을 처리할 때, @Transactional 어노테이션을 이용합니다. 실무에서 이용할 수 있도록 간단한 개념 이해, 사용방법, 동작순서 그리고 사용시 주의해야하는 예시와 대응방안을 소개드리겠습니다.

 

@Transactional 트랜잭션 처리 방법과 순서, 주의사항과 대응방법 소개

 

 

 1. 트랜잭션이란 ? 

 트랜잭션(Transaction)은 데이터베이스에 데이터를 삽입, 갱신, 삭제하는 작업의 단위를 말합니다. 같은 트랜잭션으로 묶이게 되는 작업은 원자성, 일관성, 격리성, 지속성의 특징을 만족시켜야 합니다. 이러한 특징을 만족시키면 데이터베이스의 일관성을 유지할 수 있습니다.

 

 

 2. 스프링에서 트랜잭션 처리하기 - @Transactional 

 스프링에서는 이 트랜잭션을 @Transactional 어노테이션을 이용해 처리합니다. 이 어노테이션을 사용하면 메서드가 트랜잭션으로 묶이고 적용한 메서드 호출되고 실행 중에 예외가 발생하면 롤백을 수행합니다. Exception 없이 메서드가 종료되면 커밋을 수행해 작업을 완료시킵니다.

 

스프링에서 @Transactional은 아래와 같이 메서드 선언부 위에 어노테이션을 달아서 사용하게 됩니다.

@Transactional
public void someTransactionalMethod() {
    // 트랜잭션 처리가 필요한 작업
}

스프링에서 제공하는 어노테이션을 적용하는 것 만으로도 메서드에 트랜잭션이 바로 적용되기 때문에 쉽고 편하게 트랜잭션을 수행할 수 있습니다.

 

 

 3. Exception이 발생했을때 처리하는 순서 

 실무에서 Transactioanl을 사용하면서 고려해야하는 항목이 있습니다. @Transactional 어노테이션을 이용했을 때 롤백이 가능한 Exception은 RuntimeException과 그 하위 클래스들 입니다. Exception 클래스 관련 개념은 아래 링크를 참고해주세요.

 

 

[Java] Checked Exception vs Unchecked Exception 차이점

자바의 Exception의 분류와 Checked Exception과 UnChecked Exception의 차이점에 대해서 알아보겠습니다. Checked Exception vs Unchecked Exception 개념과 차이점 >> Error 클래스와 Exception 클래스 먼저 자바의 Exception 클

timotimo.tistory.com

 

3-1. Runtime Exception이 발생한 경우

@Transactional
public void updateItem(Item item) {
    itemRepository.save(item);
    if (item.getQuantity() < 0) {
        throw new RuntimeException("Quantity는 음수일 수 없습니다.");
    }
}

updateItem 메서드가 호출되면 다음과 같은 순서로 처리됩니다.

 

  1. updateItem()이 실행되면서, @Transactional 어노테이션에 의해 블록이 트랜잭션으로 묶입니다.
  2. itemRepository.save() 메서드가 호출되어 Item 데이터가 영속성 컨텍스트에 임시 저장됩니다. 
  3. 조건문으로 저장한 값이 음수인지 확인합니다. 이때 값이 음수라고 가정합니다.
  4. RuntimeException이 발생하고 스프링이 트랜잭션을 롤백합니다.
  5. Item 데이터가 아직 데이터베이스에 저장되지 않았으므로 롤백할 내용이 없습니다.
  6. RuntimeException이 호출자에게 전달됩니다.

 

 위 예시에서는 데이터베이스에 데이터가 실제로 저장되지 않았기 때문에 롤백할 내용은 없습니다. 실제로 롤백되는 예시를 생각해본다면, updateItem 메서드의 어떤 호출부가 @Transactional가 적용되어있다고 가정합니다. 데이터를 저장 후 메서드 호출이 끝나 커밋되어 데이터가 저장된 후 updateItem 메서드가 불렸다면 호출부에서 저장한 데이터는 데이터베이스에서 롤백됩니다.

 

 

 

3-2. Checked Exception이 발생한 경우

 먼저 간단하게 Checked Exception에 대해서 설명하면, CheckedException은 예외처리를 하지 않으면 컴파일 시점에 에러가 발생합니다. 따라서 개발자가 직접 예외처리를 하고 메서드 호출이 종료되기 때문에 @Transactional을 통한 롤백이 수행되지 않게 됩니다.

 

아래 예시는 @Transactional 어노테이션을 적용한 메서드에서 Checked Exception이 발생하는 예시이면서, 동시에 문제가 발생할 수 있는 코드입니다. 

@Service
public class ItemService {
    @Autowired
    private ItemRepository itemRepository;

    @Transactional
    public void saveItem(Item item) throws IOException {
        itemRepository.save(item);
        // Checked Exception 발생
        throw new IOException("IOException 발생");
    }
}

 처리 순서는 아래와 같습니다.

 

  1. updateItem()이 실행되면서, @Transactional 어노테이션에 의해 블록이 트랜잭션으로 묶입니다.
  2. itemRepository.save() 메서드가 호출되어 Item 데이터가 영속성 컨텍스트에 임시 저장됩니다. 
  3. IOException이 발생하고 호출부로 Exception이 던져집니다.
  4. 호출부(예를 들면 컨트롤러)에서 try-catch문등으로 예외처리를 합니다.
  5. Exception이 예외처리 된 상태로 updateItem() 메서드가 종료됩니다.
  6. 정상적으로 예외처리 후에 메서드도 종료됐기 때문에 트랜잭션이 커밋되고, 영속성 컨텍스트의 item 데이터가 데이터베이스에 실제로 반영됩니다.

 

  분명히 롤백을 하기위해서 @Transactional 어노테이션을 메서드에 적용했는데, CheckedException이 내부에서 발생하면 롤백이 수행이 안되는 상황이 발생했습니다. 운영환경에서 이런 상황이 벌어졌다면 데이터베이스에 일관성이 깨지는 상황이 벌어진 것 입니다. 아래에서 두가지로 대응방안을 알아보겠습니다.

 

 

 

 4. Checked Exception이 포함된 @Transactional 롤백시키는 방법 

 

4-1. 예외처리 후에 RuntimeException 직접 발생시키기

@Service
public class ItemService {
    @Autowired
    private ItemRepository itemRepository;

    @Transactional
    public void saveItem(Item item) {
        try {
            itemRepository.save(item);
        } catch (DataAccessException e) {
            throw new RuntimeException("데이터 저장중 에러가 발생했습니다.", e);
        }
    }
}

 CheckedException을 예외처리하고 RuntimeException을 의도적으로 다시 발생시킵니다. RuntimeException은 UncheckedException이기 때문에 롤백이 수행됩니다.

 

 하지만 이 방법은 RuntimeException을 본래 의도대로 사용하지 않았으며 가독성이 좋지않기 때문에 추천되는 방법이 아닙니다.

 

4-2. rollbackFor 옵션을 사용하기

@Service
public class ItemService {
    @Autowired
    private ItemRepository itemRepository;

    @Transactional(rollbackFor = IOException.class)
    public void saveItem(Item item) throws IOException {
        itemRepository.save(item);
        throw new IOException("IOException 발생");
    }
}

위와 같이 rollbackFor 코드에 롤백하고 싶은 클래스를 지정하면, 해당 Exception이 발생했을 때 롤백을 수행하게 됩니다. (위 코드도 앞선 예시와 마찬가지로 영속성 컨텍스트에만 데이터가 임시 저장된 상태이기 때문에 롤백은 수행하지만, 실제 롤백할 데이터는 없습니다.)

 

 

 5. 요약 

스프링에서 @Transactional을 이용해 쉽게 트랜잭션을 적용할 수 있습니다. 다만 내부에서 RuntimeException이 발생하는 경우에는 자동으로 롤백을 수행해주지만, CheckedException의 경우에는 예외처리가 강제되기 때문에 롤백이 수행되지 않습니다. 이는 운영상황에서 문제가 될 수 있으며, CheckedException이 발생한 경우에도 롤백을 수행할 수 있는 두가지 방법에 대해서 알아보았습니다. 이러한 주의사항을 알면 스프링에서 좀 더 안전하고 정확하게 트랜잭션을 처리할 수 있을 것 입니다. 감사합니다.

 

반응형
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기