트랜잭션을 다루면서 Propagation 설정을 사용하는 일이 있었다.
특히나 REQUIRES_NEW를 사용했는데 무슨 말인지 이해가 되지 않았다.
무슨 부모 spring에서 부모와 트랜잭션을 동일한 범위로 묶지 않기 위해서 사용한다는 말을 했는데..
아래서 한번 알아보자.
Propagation
일단 Propagation의 옵션들을 정리하면 아래와 같다.
옵션 | 설명 |
REQUIRED | [기본 옵션] 부모 트랜잭션이 존재한다면 부모 트랜잭션에 합류, 그렇지 않다면 새로운 트랜잭션을 만든다. 중간에 자식/부모에서 rollback이 발생된다면 자식과 부모 모두 rollback 한다. |
REQUIRES_NEW | 무조건 새로운 트랜잭션을 만든다. nested한 방식으로 메소드 호출이 이루어지더라도 rollback은 각각 이루어 진다. |
MANDATORY | 무조건 부모 트랜잭션에 합류시킨다. 부모 트랜잭션이 존재하지 않는다면 예외를 발생시킨다. |
SUPPORTS | 메소드가 트랜잭션을 필요로 하지는 않지만, 진행 중인 트랜잭션이 존재하면 트랜잭션을 사용한다는 것을 의미한다. 진행 중인 트랜잭션이 존재하지 않더라도 메소드는 정상적으로 동작한다. |
NESTED | 부모 트랜잭션이 존재하면 부모 트랜잭션에 중첩시키고, 부모 트랜잭션이 존재하지 않는다면 새로운 트랜잭션을 생성한다. 부모 트랜잭션에 예외가 발생하면 자식 트랜잭션도 rollback한다. 자식 트랜잭션에 예외가 발생하더라도 부모 트랜잭션은 rollback하지 않는다. 이때 롤백은 부모 트랜잭션에서 자식 트랜잭션을 호출하는 지점까지만 롤백된다. 이후 부모 트랜잭션에서 문제가 없으면 부모 트랜잭션은 끝까지 commit 된다. 현재 트랜잭션이 있으면 중첩 트랜잭션 내에서 실행하고, 그렇지 않으면 REQUIRED 처럼 동작합니다. |
NEVER | 메소드가 트랜잭션을 필요로 하지 않는다. 만약 진행 중인 트랜잭션이 존재하면 익셉션이 발생한다. |
REQUIRED
우리가 기본으로 사용하는 @Transaction의 기본 값이 REQUIRED로 설정됩니다.
위의 예제코드처럼 자식 트랜잭션에서 예외가 발생하면 부모, 자식 모두 롤백되어 어떤 유저도 저장되지 않습니다. (부모에서 예외가 발생해도 마찬가지입니다.)
자식에서 예외 발생한 것을 부모에서 예외 처리
그러면 위와 같이 자식에서 발생한 예외를 부모 트랜잭션에서 try-catch로 예외 처리를 하는 경우에는 어떻게 될까요?
try-catch를 하였더라도 예외가 발생하면서 전부 RollBack 되는 것을 볼 수 있습니다.
REQUIRES_NEW
자식에서 예외 발생
이번에는 자식에게 REQUIRES_NEW 옵션을 준 후에 위의 코드와 같이 테스트해보았습니다. 위에서 적었던 설명으로 보면 부모, 자식 트랜잭션이 각각 열리기 때문에 자식에서 예외가 발생해도 부모에서 save 한 것은 저장이 되는 것을 예상했습니다.
그런데 부모에서 저장한 유저도 INSERT 되지 않은 것을 볼 수 있습니다. REQUIRES_NEW는 자식 트랜잭션에서 발생한 것이 부모 트랜잭션까지 전파되지 않는다의 말은 틀린 말이 아닌가 생각이 들었습니다.
"트랜잭션이 전파"되는 것과 "예외가 전파" 되는 것은 다르다는 특징이 있습니다. 자식 쪽에 예외가 발생할 경우 자식 쪽은 트랜잭션이 롤백이 되는 것이 맞습니다. 그런데 이때 자식 쪽에서 발생한 예외가 부모 쪽으로 전파되기 때문에 부모 쪽에서도 예외가 발생하여 롤백이 일어난 것으로 이해해야 맞을 것 같습니다.
자식에서 발생한 예외 부모에서 예외 처리
그래서 이번에는 부모에서 예외 처리를 했을 때는 어떻게 처리되는지 보기 위해서 예외 처리하고 실행해 보겠습니다. 이번에는 자식에서 발생한 예외를 부모에서 try-catch로 묶어주었는데요. 그랬더니 자식 트랜잭션을 호출하기 전까지의 쿼리만 커밋된 것을 확인할 수 있습니다. 예외 처리를 해주어야 부모 트랜잭션도 자식 트랜잭션에 영향을 받지 않고 커밋을 하는 것 같습니다.
부모에서 예외 발생
이번에는 부모에서 예외가 발생한 경우에는 어떻게 되는지 테스트해보겠습니다.
REQUIRES_NEW는 부모 트랜잭션에서 예외가 발생해도 자식 트랜잭션에서는 꼭 커밋되어야 하는 상황에서 사용하면 좋을 것 같습니다.
아무튼 궁금한 내용은 위와 같았다.
나는 아래와 같이 동시성 문제를 학습 중이었다. 아래의 내용에서 Propagation을 적용하는 이유는 다음과 같았다.
재고 서비스
@Service
@RequiredArgsConstructor
public class StockService {
private final StockRepository stockRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)//spring에서 트랜잭션을 부모와 따로 분리하고 싶을때 사용
public void decrease(Long id, Long quantity) { //synchronized : 해당 메서드는 한개의 쓰레드만 접근 가능
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
}
Mysql NamedLock 사용
@Component
@RequiredArgsConstructor
public class NamedLockStockFacade {
private final LockRepository lockRepository;
private final StockService stockService;
@Transactional
public void decrease(Long id, Long quantity) {
try {
lockRepository.getLock(id.toString());
stockService.decrease(id, quantity);
} finally {
lockRepository.releaseLock(id.toString());
}
}
}
부모의 트랜잭션과 동일한 범위로 묶인다면 Synchronized와 같은 문제가 발생합니다.
Database에 commit 되기 전에 락이 풀리는 현상이 발생합니다.
그렇기 때문에 별도의 트랜잭션으로 분리를 해주어 Database에 정상적으로 commit 이 된 이후에 락을 해제하는 것을 의도하였습니다.
핵심은 lock을 해제하기 전에 Database에 commit 이 되도록 하는 것입니다.
Q) 그렇다면 부모 트랜잭션이 어떤 걸 말씀하시는 걸까요?
Spring의 @Transactional 어노테이션은 시작할 때 Transaction을 시작합니다.
기본옵션으로 실행 시 부모의 트랜잭션을 따라가게 됩니다.
예를 들어보자면 A Class에 a 메서드가 있고 B Class 에 b 메소드가 있다고 가정해 보겠습니다.
public class A {
private B b;
@Transactional
public void a() {
b.b();
}
}
public class B {
@Transactional
public void B() {}
}
이럴 경우 A Class의 a에서 새로운 트랜잭션이 생성되고 B.b 메서드를 호출하게 됩니다.
b 메서드에도 Transactional 이 있지만 새로 생성하지 않고 부모(B.b 메서드를 호출한 곳 즉 A.a)에서 이미 트랜잭션이 생성된 트랜잭션을 그대로 사용하게 됩니다.
참고
https://devlog-wjdrbs96.tistory.com/424
'Framework > Spring' 카테고리의 다른 글
[Spring] Spring REST Docs API 문서 만들기 (0) | 2023.08.07 |
---|