spring / / 2023. 1. 8. 20:52

Spring에서 트랜잭션 사용

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가지 서비스가 실행된다.

  1. 사용자의 기본정보 등록 (userService)
    • userId, name
  2. 사용자의 생성일자 정보를 등록 (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



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