Transactional 어노테이션

2023-07-27
ꞏ
유영진
ꞏ
show5116

Transaction

Transaction이란 데이터베이스에서 상태를 변화시키기 위해 수행하는 작업의 단위를 뜻합니다.
크게 4가지 특징이 있고 이를 ACID라 합니다.1

  1. 원자성(Atomicity)

트랜잭션에서 정의된 연산들은 모두 성공적으로 실행되던지 아니면 전혀 실행되지 않아야 합니다.

  1. 일관성(Consistency)

트랜잭션이 실행 되기 전의 데이터베이스 내용이 잘못 되어 있지 않다면 트랜잭션이 실행된 이후에도 데이터베이스의 내용에 잘못이 이쓰면 안됩니다.

  1. 독립성(Isolation)

트랜잭션이 실행되는 도중에 다른 트랜잭션의 영향을 받아 잘못된 결과를 만들어서는 안됩니다.

  1. 지속성(Durability)

트랜잭션이 성공적으로 수행되면 그 트랜잭션이 갱신한 데이터베이스의 내용은 영구적으로 저장됩니다.

Transactional 어노테이션

Spring에서는 @Transactional 어노테이션을 사용해서 선언적 트랜잭션을 사용합니다.
서비스의 한 메서드를 Transaction 단위로 만들어 주는 역할을 합니다.
위에서 설명한 트랜잭션의 4가지 특징을 서비스에서 보장해줍니다.
선언하는 방법은 메서드에 선언하거나, 서비스에 선언을 합니다.
서비스에 선언을 할 경우 서비스 내의 메서드들이 전부 영향을 받게 됩니다.

  • 롤백

트랜잭션의 원자성 특징으로 연산들은 모두 실행되던지 혹은 전혀 실행되지 않아야 합니다.
따라서 서비스에서 Exception이 발생할 경우에 결과가 commit되지 않고 모두 rollback 됩니다.

java
1@Service
2@RequiredArgsConstructor
3public class TransactionService {
4
5 private final UserRepository userRepository;
6
7 @Transactional
8 public void transactionTest(Long id) throws Exception {
9 User user = userRepository.findById(id).orElseThrow();
10 userRepository.delete(user);
11 throw new Exception("익셉션 발생");
12 }
13
14}

위의 예시에서 user 엔티티를 삭제하여서 delete 문을 통해 데이터가 삭제되어야 하지만, Exception이 발생하게 되면 삭제되지 않게 됩니다.

주의할 점

java
1@Service
2@RequiredArgsConstructor
3public class TransactionService {
4
5 private final UserRepository userRepository;
6
7 public void transactionParent(Long id) {
8 transactionChild(id);
9 }
10
11 @Transactional
12 public void transactionChild(Long id) throws Exception {
13 User user = userRepository.findById(id).orElseThrow();
14 userRepository.delete(user);
15 throw new Exception("익셉션 발생");
16 }
17}

위의 코드가 실행되면 어떻게 동작할까요?
@Transactional의 동작에 의해서는 user가 delete되지 않고 rollback되어야 정상입니다.
하지만 위의 코드가 실행된다면 rollback이 되지 않고 user가 삭제됩니다.
그 이유는 @Transactional 어노테이션은 Proxy 방식으로 동작하기 때문입니다.
Spring은 proxy로 접근해서 기존 코드를 가로채 앞 뒤로 transaction을 처리하는 코드를 생성해줍니다.

java
1@Service
2public class TransactionService {
3
4 @Transactional
5 public void test() {
6 ...
7 }
8}

위의 코드가 있다면 실제로 동작하는건 프록시로부터 아래의 코드처럼 동작합니다.

java
1public class TransactionProxy {
2 private final TransactionManager manager = TransactionManager.getInstance();
3
4 public void transactionLogic() {
5 try {
6 manager.begin(); // 트랜잭션 전처리
7 est(); // 코드 동작
8 manager.commit();
9 } catch (Exception e) {
10 manager.rollback();
11 }
12 }
13}

그런데 이 Proxy모드는 오직 외부로부터의 method 호출이 발생한 경우에만 인터셉트 되어서 트랜잭션 관리가 가능해집니다.
그렇기 때문에 내부에서 호출할 경우에는 해당 메서드의 @Transactional이 적용되지 않으니 유의하면서 코드를 작성해야 합니다.

readOnly 옵션

readOnly옵션은 Transaction내에서 조회를 하겠다고 선언하는 옵션입니다.
이 옵션을 사용해야하나? 라 하면 사용하는 것이 좋다. 입니다.
첫번째로는 readOnly옵션이 있을 경우 영속성 컨텍스트가 엔티티를 관리하지 않게 됩니다.
즉 1차캐시에 스냅샷을 만들어 두지 않기 때문에 단순 조회만 사용할 경우에 성능이 올라가게 됩니다.
두번째로는 혹시모를 데이터 변경을 방지할 수 있습니다.
만약 업데이트문을 사용하지 않을것인데 엔티티의 내용이 변경되거나 return이 된다면 데이터 변경이 일어날수도 있는데,(물론 이미 잘못 설계된 코드입니다.) 이를 방지하여 줍니다.
세번째로는 선언만으로 직관적으로 조회 서비스인걸 알 수 있습니다.
CUD 로직이 없음을 알기에 알아보기 쉬워지고 신뢰성이 보장됩니다.
네번째로는 MySql을 사용할 경우에는 읽기 전용으로 master가 아닌 slave를 호출해서 DB 서버의 부하를 줄여줍니다.
이러한 이유들로 단순 조회쿼리를 사용할 경우에도 readOnly 옵션을 사용하는 것이 좋습니다.

java
1@Service
2@RequiredArgsConstructor
3public class TransactionService {
4 private final UserRepository userRepository;
5
6 @Transactional(readOnly = true)
7 public UserDto getUserStatus(Long id) {
8 User user = userRepository.findById(id).orElseThrow();
9 return user.toDto();
10 }
11}