스프링을 이용해 트랜잭션을 처리할 때, @Transactional 어노테이션을 이용합니다. 실무에서 이용할 수 있도록 간단한 개념 이해, 사용방법, 동작순서 그리고 사용시 주의해야하는 예시와 대응방안을 소개드리겠습니다.
@Transactional 트랜잭션 처리 방법과 순서, 주의사항과 대응방법 소개
1. 트랜잭션이란 ?
트랜잭션(Transaction)은 데이터베이스에 데이터를 삽입, 갱신, 삭제하는 작업의 단위를 말합니다. 같은 트랜잭션으로 묶이게 되는 작업은 원자성, 일관성, 격리성, 지속성의 특징을 만족시켜야 합니다. 이러한 특징을 만족시키면 데이터베이스의 일관성을 유지할 수 있습니다.
2. 스프링에서 트랜잭션 처리하기 - @Transactional
스프링에서는 이 트랜잭션을 @Transactional 어노테이션을 이용해 처리합니다. 이 어노테이션을 사용하면 메서드가 트랜잭션으로 묶이고 적용한 메서드 호출되고 실행 중에 예외가 발생하면 롤백을 수행합니다. Exception 없이 메서드가 종료되면 커밋을 수행해 작업을 완료시킵니다.
스프링에서 @Transactional은 아래와 같이 메서드 선언부 위에 어노테이션을 달아서 사용하게 됩니다.
@Transactional
public void someTransactionalMethod() {
// 트랜잭션 처리가 필요한 작업
}
스프링에서 제공하는 어노테이션을 적용하는 것 만으로도 메서드에 트랜잭션이 바로 적용되기 때문에 쉽고 편하게 트랜잭션을 수행할 수 있습니다.
3. Exception이 발생했을때 처리하는 순서
실무에서 Transactioanl을 사용하면서 고려해야하는 항목이 있습니다. @Transactional 어노테이션을 이용했을 때 롤백이 가능한 Exception은 RuntimeException과 그 하위 클래스들 입니다. Exception 클래스 관련 개념은 아래 링크를 참고해주세요.
3-1. Runtime Exception이 발생한 경우
@Transactional
public void updateItem(Item item) {
itemRepository.save(item);
if (item.getQuantity() < 0) {
throw new RuntimeException("Quantity는 음수일 수 없습니다.");
}
}
updateItem 메서드가 호출되면 다음과 같은 순서로 처리됩니다.
- updateItem()이 실행되면서, @Transactional 어노테이션에 의해 블록이 트랜잭션으로 묶입니다.
- itemRepository.save() 메서드가 호출되어 Item 데이터가 영속성 컨텍스트에 임시 저장됩니다.
- 조건문으로 저장한 값이 음수인지 확인합니다. 이때 값이 음수라고 가정합니다.
- RuntimeException이 발생하고 스프링이 트랜잭션을 롤백합니다.
- Item 데이터가 아직 데이터베이스에 저장되지 않았으므로 롤백할 내용이 없습니다.
- 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 발생");
}
}
처리 순서는 아래와 같습니다.
- updateItem()이 실행되면서, @Transactional 어노테이션에 의해 블록이 트랜잭션으로 묶입니다.
- itemRepository.save() 메서드가 호출되어 Item 데이터가 영속성 컨텍스트에 임시 저장됩니다.
- IOException이 발생하고 호출부로 Exception이 던져집니다.
- 호출부(예를 들면 컨트롤러)에서 try-catch문등으로 예외처리를 합니다.
- Exception이 예외처리 된 상태로 updateItem() 메서드가 종료됩니다.
- 정상적으로 예외처리 후에 메서드도 종료됐기 때문에 트랜잭션이 커밋되고, 영속성 컨텍스트의 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이 발생한 경우에도 롤백을 수행할 수 있는 두가지 방법에 대해서 알아보았습니다. 이러한 주의사항을 알면 스프링에서 좀 더 안전하고 정확하게 트랜잭션을 처리할 수 있을 것 입니다. 감사합니다.
'기술 블로그' 카테고리의 다른 글
자바 Heapdump 생성하는 방법, MAT 이용해 메모리릭 분석하기 (0) | 2023.03.24 |
---|---|
webSocket 정상연결 후 Pending 원인분석, 웹소켓 정상동작 테스트하는 방법 소개 (0) | 2023.02.28 |
[Java] Try-with-resource 케이스별 동작순서 (0) | 2022.12.25 |
[Java] Try-with-resource을 사용해야하는 이유와 동작순서 (0) | 2022.12.15 |
최근댓글