Spring boot에서 어노테이션으로 트랜잭션을 구성하는 방법을 알아보고 여러가지 트랜잭션 전파 옵션(propagation)에 따른 실행결과가 어떻게 달라지는지 확인해보자.
@Transactional 어노테이션
트랜잭션을 구성할 때 빈 클래스 혹은 메소드 레벨에서 @Transactional을 적용할 수 있다.
@Service
@Transactional
public class UserService {
// ...
}
Transaction과 프록시
고수준에서, 스프링은 @Transactional 어노테이션이 있는 모든 클래스 혹은 메소드 레벨에 대해 프록시를 만든다. 프록시는 실행 메소드 전과 후에 트랜잭션 시작과 커밋하는 트랜잭션 로직을 주입한다.
@Transactional 사용하는 방법을 알아보자.
@Transactional을 사용하지 않는 경우와 사용하는 경우 Propagation 옵션에 따라 트랜잭션이 실패하는 경우에 대해 어떻게 동작하는지 알아보자.
1. @Transactional을 사용하지 않고 오류가 발생하는 경우
2. @Transactional을 사용하고 오류가 발생하는 경우
3. @Transactional(Propagation.REQUIRED)를 사용하는 경우
4. @Transactional(Propagation.REQUIRES_NEW)를 사용하는 경우
5. @Transactional(Propagation.NESTED)를 사용하는 경우
6. @Transactional(Propagation.MANDATORY)를 사용하는 경우
7. @Transactional(Propagation.SUPPORT)를 사용하는 경우
8. @Transactional(Propagation.NEVER)를 사용하는 경우
테스트 시나리오
테스트를 위한 사용자를 등록하는 시나리오를 만들어보자.
사용자 등록 시 사용자 기본정보 등록 + 사용자 생성일자 등록 등 2가지 서비스가 실행된다.
- 사용자의 기본정보 등록 (userService)
- userId, name
- 사용자의 생성일자 정보를 등록 (userDateService)
- userId, createdAt
호출하는 rest api 경로는 일반적인 사용방법은 아니지만 여러가지 테스트를 쉽게 이해할 수 있도록 만든 것이니 실제 서비스에서는 아래와 같은 형식으로 사용하면 안된다.
1. @Transactional을 사용하지 않고 오류가 발생하는 경우
@Transactional을 사용하지 않으면 실행 로직 내에서 예외가 발생하더라도 이미 처리된 트랜잭션의 경우 롤백이 되지 않는다.
[UserController]
@PostMapping("withNoTransaction")
public void registerWithNoTransaction() {
User user = new User(123, "홍길동");
userService.registerWithNoTransaction(user);
}
[UserService]
// 트랜잭션을 사용하지 않는 경우
// @Transactional
public void registerWithNoTransaction(User user) {
userRepository.save(new UserJpo(user)); // user 등록
userDateService.registerWithNoTransaction(user.getUserId()); // userDate 등록
throw new IllegalArgumentException(); // 여기서 오류 발생
}
[API 실행]
POST /users/withNoTransaction
위의 케이스를 실행하면 오류가 발생한다.
{
"timestamp": "2023-01-07T11:22:51.952+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/users/withNoTransaction"
}
하지만 user와 userDate를 조회해보면 등록되어 있음을 확인할 수 있다.
[users 조회]
GET /users
[조회 결과]
[
{
"userId": 6864,
"name": "홍길동"
}
]
[userDate 조회]
GET/users/date
[조회 결과]
[
{
"userId": 6864,
"created": 1673090655098
}
]
@Transactional을 추가하지 않았다면 이전에 실행된 작업은 롤백되지 않고 그대로 남아있게 된다.
2. @Transactional을 사용하고 오류가 발생하는 경우
Propagation.REQUIRED
- REQUIRED가 기본 propagation이다.
- 스프링은 트랜잭션이 있는지 체크하고 없다면 새로운 트랜잭션을 만든다.
[UserController]
@PostMapping
public void registerWithTransaction() {
User user = new User(123, "홍길동");
userService.register(user);
}
[UserService]
// 기본 트랜잭션을 사용하는 경우
@Transactional
public void registerWithTransaction(User user) {
userRepository.save(new UserJpo(user)); // user 등록
userDateService.registerWithTransaction(user.getUserId()); // userDate 등록
throw new IllegalArgumentException(); // 여기서 오류 발생
}
[API 실행]
POST /users/withTransaction
위의 케이스를 실행하면 오류가 발생한다.
{
"timestamp": "2023-01-07T11:22:51.952+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/users/withNoTransaction"
}
[users 조회]
GET /users
[조회 결과]
[]
[userDate 조회]
GET/users/date
[조회 결과]
[]
3. @Transactional(Propagation.REQUIRED)를 사용하는 경우
Propagation.REQUIRED는 @Transactional의 기본값이므로 2번의 경우와 동일하게 실행된다.
4. @Transactional(Propagation.REQUIRES_NEW)를 사용하는 경우
Propagation.REQUIRES_NEW
- REQUIRES_NEW일 때에, 트랜잭션이 이미 있다면 현재 트랜잭션을 잠시 대기하고 새로운 트랜잭션을 만든다.
- 새로운 트랜잭션 안에서 예외가 발생해도 호출한 곳에는 롤백이 전파되지 않는다.
- 즉, 2개의 트랜잭션이 완전 독립적으로 동작한다.
[UserController]
@PostMapping("withTransactionRequiresNew")
public void registerWithTransactionRequiresNew() {
User user = new User(123, "홍길동");
userService.registerWithTransactionRequiresNew(user);
}
[UserService]
@Transactional
public void registerWithTransactionRequiresNew(User user) {
userRepository.save(new UserJpo(user)); // user 등록
userDateService.registerWithTransactionRequiresNew(user.getUserId()); // @Transactional(propagation = Propagation.REQUIRES_NEW) 로 등록
throw new IllegalArgumentException(); // 여기서 오류 발생
}
[API 실행]
POST /users/withTransactionRequiresNew
위의 케이스를 실행하면 오류가 발생한다.
{
"timestamp": "2023-01-07T11:22:51.952+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/users/withNoTransaction"
}
user는 @Transactional에 의해 롤백이 되지만 userDate는 REQUIRES_NEW로 새로운 트랜잭션을 실행시켰으므로 데이터가 그대로 들어가는 것을 확인할 수 있다.
[users 조회]
GET /users
[조회 결과]
[]
[userDate 조회]
GET/users/date
[조회 결과]
[
{
"userId": 4231,
"created": 1673091753189
}
]
5. @Transactional(Propagation.NESTED)를 사용하는 경우
Propagation.NESTED
NESTED
일 경우, 스프링은 트랜잭션이 있는지 체크하고, 만일 있다면 새로운 저장 포인트로 표시한다. 비즈니스 로직이 에러를 발생시키면 트랜잭션은 저장 포인트로 롤백된다. 만일 진행중인 트랜잭션이 없다면REQUIRED
처럼 동작한다.
[UserController]
@PostMapping("withTransactionNested")
public void withTransactionNested() {
User user = new User(123, "홍길동");
userService.registerWithTransactionNested(user);
}
[UserService]
@Transactional
public void registerWithTransactionNested(User user) {
try {
userRepository.save(new UserJpo(user)); // user 등록
userDateService.registerWithTransactionNested(user.getUserId()); // @Transactional(propagation = Propagation.NESTED) 로 등록
throw new IllegalArgumentException(); // 여기서 오류 발생
} catch (Exception e) {
e.printStackTrace();
}
}
[API 실행]
POST /users/withTransactionNested
위의 케이스를 실행하면 아래와 같은 오류가 발생한다. JPA에서 savePoint를 지원하지 않는듯 한다.
org.springframework.transaction.NestedTransactionNotSupportedException: JpaDialect does not support savepoints - check your JPA provider's capabilities
그래서 위의 케이스는 일반 로직 에러와 같이 실행이 되며 users만 실행되는 결과로 남게 된다.
[users 조회]
GET /users
[조회 결과]
[
{
"userId": 1775,
"name": "홍길동"
}
]
[userDate 조회]
GET/users/date
[조회 결과]
[]
6. @Transactional(Propagation.MANDATORY)를 사용하는 경우
Propagation.MANDATORY
- 진행중인 트랜잭션이 있다면 해당 트랜잭션이 사용되고, 없다면 예외를 발생시킨다.
[UserController]
@Transactional
public void registerWithTransactionMandatory(User user) {
userRepository.save(new UserJpo(user)); // user 등록
userDateService.registerWithTransactionMandatory(user.getUserId()); // @Transactional(propagation = Propagation.REQUIRES_NEW) 로 등록
}
[UserService]
@Transactional
public void registerWithTransactionMandatory(User user) {
userRepository.save(new UserJpo(user)); // user 등록
userDateService.registerWithTransactionMandatory(user.getUserId()); // @Transactional(propagation = Propagation.MANDATORY) 로 등록
}
[API 실행]
POST /users/withTransactionMandatory
위의 케이스를 실행하면 정상실행이 된다.
[users 조회]
GET /users
[조회 결과]
[
{
"userId": 3343,
"name": "홍길동"
}
]
[userDate 조회]
GET/users/date
[조회 결과]
[
{
"userId": 3343,
"created": 1673176669540
}
]
만일 UserService에서 @Transaction이 없으면 오류를 반환한다.
org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:362) ~[spring-tx-5.3.22.jar:5.3.22]
[users 조회]
GET /users
[조회 결과]
[
{
"userId": 3343,
"name": "홍길동"
}
]
[userDate 조회]
GET/users/date
[조회 결과]
[]
7. @Transactional(Propagation.SUPPORT)를 사용하는 경우
Propagation.SUPPORT
- REQUIRES_SUPPORT일 때에, 트랜잭션이 이미 있다면 현재 트랜잭션을 사용하고 트랜잭션이 없다면 트랜잭션 없이 실행된다.
[UserController]
@PostMapping("withTransactionSupport")
public void withTransactionSupport() {
User user = new User(123, "홍길동");
userService.registerWithTransactionSupport(user);
}
[UserService]
@Transactional
public void registerWithTransactionSupport(User user) {
userRepository.save(new UserJpo(user)); // user 등록
userDateService.registerWithTransactionSupport(user.getUserId()); // @Transactional(propagation = Propagation.SUPPORT) 로 등록
throw new IllegalArgumentException(); // 여기서 오류 발생
}
[API 실행]
POST /users/withTransactionSupport
위의 케이스를 실행하면 오류가 발생한다.
{
"timestamp": "2023-01-07T11:22:51.952+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/users/withNoTransaction"
}
SUPPORT로 실행하면 부모 트랜잭션에 @Transactional이 있으면 해당 트랜잭션을 이용해서 실행이 된다. 그래서 users, users/date 모두 롤백되는 것을 확인할 수 있다.
[users 조회]
GET /users
[조회 결과]
[]
[userDate 조회]
GET/users/date
[조회 결과]
[]
하지만 부모 트랜잭션이 없는 경우는 2개의 로직 모두 롤백되지 않는다.
//@Transactional
public void registerWithTransactionSupport(User user) {
userRepository.save(new UserJpo(user)); // user 등록
userDateService.registerWithTransactionSupport(user.getUserId()); // @Transactional(propagation = Propagation.SUPPORT) 로 등록
throw new IllegalArgumentException(); // 여기서 오류 발생
}
[users 조회]
GET /users
[조회 결과]
[
{
"userId": 3343,
"name": "홍길동"
}
]
[userDate 조회]
GET/users/date
[조회 결과]
[
{
"userId": 3343,
"created": 1673176669540
}
]
8. @Transactional(Propagation.NEVER)를 사용하는 경우
Propagation.NEVER
NEVER
일 경우, 진행중인 트랜잭션이 있을 경우 스프링은 예외를 발생시킨다.
[UserController]
@PostMapping("withTransactionNever")
public void withTransactionNever() {
User user = new User(123, "홍길동");
userService.registerWithTransactionNever(user);
}
[UserService]
public void registerWithTransactionNever(User user) {
userRepository.save(new UserJpo(user)); // user 등록
userDateService.registerWithTransactionNever(user.getUserId()); // @Transactional(propagation = Propagation.NEVER) 로 등록
}
[API 실행]
POST localhost:8080/users/withTransactionNever
위의 케이스를 실행하면 부모에 트랜잭션이 없으므로 정상 실행된다.
[users 조회]
GET /users
[조회 결과]
[
{
"userId": 3343,
"name": "홍길동"
}
]
[userDate 조회]
GET/users/date
[조회 결과]
[
{
"userId": 3343,
"created": 1673176669540
}
]
하지만 부모에 트랜잭션이 있는 경우라면 실패한다.
@Transactional
public void registerWithTransactionNever(User user) {
userRepository.save(new UserJpo(user)); // user 등록
userDateService.registerWithTransactionNever(user.getUserId()); // @Transactional(propagation = Propagation.NEVER) 로 등록
}
[users 조회]
GET /users
[조회 결과]
[]
[userDate 조회]
GET/users/date
[조회 결과]
[]
참고: https://www.baeldung.com/spring-transactional-propagation-isolation