들어가기 전에
트랜잭션 전파란 무엇인가? 트랜잭션의 범위가 connection 기준인 것부터 이해를 해보자.
The starting point of your database interaction is in making a connection. The details behind what exactly constitutes a connection vary from API to API. Nevertheless, making a connection is basically establishing some sort of link between your code and the database.
출처 : docstore.mik.ua/orelly/weblinux2/mysql/ch08_02.htm
connection은 데이터베이스와의 연결을 맺는 것을 의미한다. 여러 메서드에서 각각의 connection을 구현한다면 같은 트랜잭션 범위에 들어갈 수 없다. 그래서 하나의 커넥션으로 여러 메서드를 한 트랜잭션 범위로 묶고 싶을 때 사용하는 것이 트랜잭션 전파이다. 스프링 프레임워크의 경우 @Transactional 어노테이션을 통해 메서드 간에 connection 객체를 전달하지 않아도 하나의 트랜잭션으로 묶어서 처리해주는데 이 내용을 자세히 알아보려고 한다.
스프링의 트랜잭션 관리
스프링은 다양한 트랜잭션 관리 API를 상위 레벨에서 추상화해서 제공하므로 개발자는 API 내부 구현을 자세히 몰라도 스프링의 트랜잭션 관리 기능을 사용할 수 있다. 스프링에서는 프로그램 방식 혹은 선언적 트랜잭션 관리 기능을 지원한다. 프로그램 방식은 직접 로직 중간에 커밋이나 롤백을 제어하는 것을 말하는데 정교하게 제어할 필요성이 있는 경우에 사용한다. 그리고 스프링에서는 트랜잭션 AOP를 이용해서 선언적 트랜잭션 관리를 지원한다.
참고로 트랜잭션의 속성은 ACID로 설명할 수 있다.
- 원자성(Atomicity) : 트랜잭션은 연속적인 액션들로 이루어진 원자성 작업이다. 트랜잭션의 액션은 전부 다 수행되거나 아무것도 수행되지 않도록 보장한다.
- 일관성(Consistency) : 트랜잭션의 액션이 모두 완료되면 커밋되고 데이터 및 리소스는 비즈니스 규칙에 맞게 일관된 상태를 유지한다.
- 격리성(Isolation) : 동일한 데이터를 여러 트랜잭션이 동시에 처리할 경우 데이터가 변질되지 않게 하려면 각각의 트랜잭션을 격리해야 한다.
- 지속성(Durability) : 트랜잭션 완료 후 그 결과는 설령 시스템이 실패하더라도 살아남아야 한다. 보통 트랜잭션의 결과물은 Persistence 저장소에 저장된다.
@Transactional
스프링에서는 각각 트랜잭션을 적용할 메서드에 @Transactional, 구성 클래스에는 @EnableTransactionManagement을 붙여서 트랜잭션을 선언한다. 메서드나 클래스에 적용이 가능한데 클래스에 적용하면 클래스의 모든 public 메서드에 트랜잭션이 걸린다. 인터페이스에도 사용할 수는 있지만 클래스 기반 프록시에서는 제대로 작동하지 않을 수 있어서 권장하지 않는다.
트랜잭션을 적용하고 싶은 메서드 위에 @Transactional을 사용한다. 대신 주의해야 할 점은 스프링 AOP가 프록시 기반으로 움직이기 때문에 Public 메서드에만 사용할 수 있다. 프록시를 통해서 @Transactional이 적용된 메서드를 가져와서 실행해야 하는데 public이어야만 접근할 수 있기 때문이다.
// 출처 : 스프링 5 레시피
public class JdbcBookShop extends JdbcDaoSupport implements BookShop {
@Transactional
public void purchase(final String isbn, final String username) { ... }
}
자바 구성 클래스에는 @EnableTransactionManagement를 붙인다. 스프링은 IoC 컨테이너에 선언된 빈들을 찾아서 @Transactional을 붙인 메서드나 클래스의 모든 public 메서드를 가져와 어드바이스를 적용한다.
@Configuration
@EnableTransactionManagement
public class BookstoreConfiguration { ... }
Propagation
만약 이렇게 트랜잭션이 걸린 메서드를 다른 메서드가 호출할 경우에는 트랜잭션을 어떻게 전달해야 할까? 호출한 메서드도 기존 트랜잭션 내에서 실행하거나 자신만의 고유한 트랜잭션에서 실행해야 할 것이다.
트랜잭션 전파는 @Transactional의 propagation 속성에 명시하는데 스프링의 org.springframework.transaction.TransactionDefinition 인터페이스에는 총 7가지 방식이 정의되어 있다.
전달 속성 | 설명 |
REQUIRED | 진행 중인 트랜잭션이 있으면 현재 메서드를 그 트랜잭션에서 실행하되, 그렇지 않을 경우 새 트랜잭션을 시작해서 실행한다. |
REQUIRES_NEW | 항상 새 트랜잭션을 시작해 현재 메서드를 실행하고 진행 중인 트랜잭션이 있으면 잠시 중단시킨다. |
SUPPORTS | 진행 중인 트랜잭션이 있으면 현재 메서드를 그 트랜잭션 내에서 실행하되, 그렇지 않을 경우 트랜잭션 없이 실행한다. |
NOT_SUPPORTED | 트랜잭션 없이 현재 메서드를 시행하고 진행 중인 트랜잭션이 있으면 잠시 중단시킨다. |
MANDATORY | 반드시 트랜잭션을 걸고 현재 메서드를 실행하되 진행 중인 트랜잭션이 없으면 예외를 던진다. |
NEVER | 반드시 트랜잭션 없이 현재 메서드를 실행하되 진행 중인 트랜잭션이 있으면 예외를 던진다. |
NESTED | 진행 중인 트랜잭션이 있으면 현재 메서드를 이 트랜잭션의 중첩 트랜잭션 내에서 실행한다. 진행 중인 트랜잭션이 없으면 새 트랜잭션을 시작해서 실행한다. |
출처 : 스프링 5 레시피 |
@Transactional(propagation = Propagation.전파속성)
public void example() { ... }
1. REQUIRED
기본값으로 지정되어 있으며 진행 중인 트랜잭션이 있으면 현재 메서드를 그 트랜잭션에서 실행하되, 그렇지 않을 경우 새 트랜잭션을 시작해서 실행한다. 의사코드는 다음과 같다.
// 출처 : https://www.baeldung.com/spring-transactional-propagation-isolation
if (isExistingTransaction()) {
if (isValidateExistingTransaction()) {
validateExisitingAndThrowExceptionIfNotValid();
}
return existing;
}
return createNewTransaction();
2. REQUIRES_NEW
항상 새 트랜잭션을 시작해 현재 메서드를 실행하고 진행 중인 트랜잭션이 있으면 잠시 중단시킨다. 중단시키기 위해서는 JtaTransactionManager가 필요하다. 트랜잭션 일시 중단은 JtaTransactionManager가 등록된 경우에만 사용할 수 있다.
의사 코드는 다음과 같다.
if (isExistingTransaction()) {
suspend(existing);
try {
return createNewTransaction();
} catch (exception) {
resumeAfterBeginException();
throw exception;
}
}
return createNewTransaction();
3. SUPPORTS
진행 중인 트랜잭션이 있으면 현재 메서드를 그 트랜잭션 내에서 실행하되, 그렇지 않을 경우 트랜잭션 없이 실행한다.
if (isExistingTransaction()) {
if (isValidateExistingTransaction()) {
validateExisitingAndThrowExceptionIfNotValid();
}
return existing;
}
return emptyTransaction;
4. NOT_SUPPORTED
트랜잭션 없이 현재 메서드를 시행하고 진행 중인 트랜잭션이 있으면 잠시 중단시킨다. REQUIRES_NEW와 같이 JTATransactionManager를 이용하여 중단시켜야 한다.
5. MENDATORY
반드시 트랜잭션을 걸고 현재 메서드를 실행하되 진행 중인 트랜잭션이 없으면 예외를 던진다.
if (isExistingTransaction()) {
if (isValidateExistingTransaction()) {
validateExisitingAndThrowExceptionIfNotValid();
}
return existing;
}
throw IllegalTransactionStateException;
6. NEVER
반드시 트랜잭션 없이 현재 메서드를 실행하되 진행 중인 트랜잭션이 있으면 예외를 던진다.
if (isExistingTransaction()) {
throw IllegalTransactionStateException;
}
return emptyTransaction;
7. NESTED
진행 중인 트랜잭션이 있으면 세이브포인트를 체크한다. 만약 비즈니스 로직이 수행되다가 예외가 발생하면 트랜잭션이 이 세이브포인트로 롤백된다. 만약 진행 중인 트랜잭션이 없다면 REQUIRED와 똑같이 새로운 트랜잭션을 생성한다.
참고
- 스프링 5 레시피
- 프로그래밍 초식 : DB 트랜잭션 조금 이해하기 01
- [Spring] 스프링의 트랜잭션 전파 속성(Transaction propagation) 완벽하게 이해하기
- Transaction Propagation and Isolation in Spring @Transactional
'Spring' 카테고리의 다른 글
[Spring] REST Docs 설정 (0) | 2023.04.19 |
---|---|
[Spring Boot] com.mysql.cj.jdbc.driver not found (0) | 2023.03.31 |
@Valid exception handling (with BindException) (1) | 2022.11.29 |
MultipartFile이 있는 DTO 요청하기 (404 Bad Request 해결) (0) | 2022.11.28 |
DTO와 MultipartFile 요청하기 (415 Unsupported Media Type 해결) (0) | 2022.11.28 |