트랜잭션이란?
하나의 작업 단위를 논리적으로 묶어주는 역할을 해준다!
이를 통해서 데이터 베이스의 ACID 속성을 지킬 수 있도록 한다!
- 원자성 (Atomicity)
예: 송금 시, 돈이 A 계좌에서 빠졌지만 B 계좌로 들어가지 않으면 롤백.
작업이 모두 성공하거나, 하나라도 실패하면 전체를 롤백. - 일관성 (Consistency)
작업 전후의 데이터 상태가 항상 유효하도록 보장. - 격리성 (Isolation)
트랜잭션 간 서로 간섭하지 않도록 격리 수준을 설정할 수 있다. - 지속성 (Durability)
트랜잭션이 성공하면 결과가 영구적으로 반영된다.
또 다른 구체적인 예시
상품을 구매 했을 때
1. 재고 감소
2. 결제 처리
3. 주문 기록 생성
결국 트랜잭션은 1, 2, 3 과 같은 각각의 작업을 하나의 작업 단위로 묶어주는 역할을 한다!!
@Service
@RequiredArgsConstructor
public class OrderService {
private final StockService stockService;
private final PaymentService paymentService;
private final OrderRecordService orderRecordService;
private final TransactionLogService transactionLogService;
@Transactional
public void processOrder(Long productId, int quantity, BigDecimal amount) {
// 1. 재고 감소
stockService.decreaseStock(productId, quantity);
// 2. 결제 처리
paymentService.processPayment(productId, amount);
// 3. 주문 기록 생성
orderRecordService.createOrderRecord(productId, quantity);
}
}
트랜잭션은 언제 동작할까?
트랜잭션은 여러 작업을 묶어서 하나의 작업으로 처리하는 것이니 예외가 발생했을 때 이를 핸들링 하는 것이다.
중간에 에러가 나면 기존에 작업 했던 것을 없던 것으로 처음으로 돌려야 한다!
왜 롤백을 해야할까?
> 우리가 예상하지 못한 에러가 발생했고 이를 처리하지 못했으니 없던 것으로 만들어야 하기 때문이다.
예외 처리 시 동작
우리의 서비스 Transactional 내부에 예외가 발생했다고 롤백하고 아 몰라~ 하면 끝나는 걸까? 이러면 안된다 ㅠㅠ
만약 우리 서비스의 상품을 구매했을 때 결제 처리 과정 중
- 재고 감소
- 결제 처리 => "잔액이 부족하다"라는 예외가 발생!
- 주문 기록 생성
try - catch 문 !!! 이럴 때 사용하는 것이다.
물론 모든 부분에 try-catch로 감싸면 코드가 더러워지겠지만... 자주 에러가 발생하는 부분에서는
로그든 어떠한 추가 작업이든 처리해주는 것이 좋다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final StockService stockService;
private final PaymentService paymentService;
private final OrderRecordService orderRecordService;
private final TransactionLogService transactionLogService;
@Transactional
public void processOrder(Long productId, int quantity, BigDecimal amount) {
try {
// 1. 재고 감소
stockService.decreaseStock(productId, quantity);
// 2. 결제 처리
paymentService.processPayment(productId, amount); -> 돈 없어요.
// 3. 주문 기록 생성
orderRecordService.createOrderRecord(productId, quantity);
} catch ( Exception e ) {
log.info("결제 도중 에러 발생");
혹은 어떤 에러가 발생했는지 디비에 저장한다.(로그 생성)
throw e; -------> 예외를 던져야지만 롤백이 된다!!!!
}
}
}
중요) 마지막에 throw e를 꼭 던져야지 롤백이 된다!
=> 그 이유는 Spring에서 트랜잭션 관리는 기본적으로 RuntimeException 또는 Error가 발생해야 트랜잭션을 롤백 한다. 만약 catch 블록에서 예외를 처리한 후 예외를 다시 던지지 않으면 Spring은 해당 메서드가 정상적으로 종료되었다고 간주하고 트랜잭션을 커밋할 수 있다!!
만약 트랜잭션 안에 있는 메서드가 또 트랜잭션이 있으면 어떻게 될까?
- 재고 관리
- 금액 차감
- 마일리지
- 쿠폰
- 현금
- 주문기록
위 처럼 마일리지, 쿠폰, 현금에도 트랜잭션이 붙어있다면? 우리는 전파라는 속성을 써야한다..!
그럼 전파는 언제 쓰는데?
@Service
@RequiredArgsConstructor
public class OrderService {
private final StockService stockService;
private final PaymentService paymentService;
private final OrderRecordService orderRecordService;
private final TransactionLogService transactionLogService;
@Transactional
public void processOrder(Long productId, int quantity, BigDecimal amount) {
// 1. 재고 감소
stockService.decreaseStock(productId, quantity);
// 2. 결제 처리
paymentService.processPayment(productId, amount);
// 3. 주문 기록 생성
orderRecordService.createOrderRecord(productId, quantity);
// 4. 성공 여부 기록
transactionLogService.logTransactionResult(productId, true, "Order successful");
}
}
4번을 보면 중요 로직은 솔직히 1, 2, 3번이다 근데 4번 성공 여부 기록도 같은 트랜잭션으로 돌리는게 맞을까? 이럴 때는 그냥 NEW를 쓰자..!
트랜잭션 전파 속성 요약
1. REQUIRED(기본값) => 거의 99%는 이거 씀
- 기존 트랜잭션이 있으면 해당 트랜잭션에 참여, 없으면 새 트랜잭션을 생성한다.
2. REQUIRES_NEW => 진짜 가끔가다 씀.
- 항상 새로운 트랜잭션을 생성하며 기존 트랜잭션은 일시 중단된다.
3. SUPPORTS
- 기존 트랜재션이 있으면 해당 트랜재션에 참여한다 없으면 트랜잭션 없이 실행된다
4. NOT_SuPPORTED
- 트랜잭션 없이 실행하며 기존 트랜잭션은 일시 중단된다.
5. MANDATORY
- 반드시 기존 트랜잭션이 있어야 한다. 트랜잭션이 없다면 예외가 발생.
6. NEVER
- 트랜잭션이 활성화된 상태에서 호출되면 예외를 던진다.
- 트랜잭션이 없는 경우에만 실행
7. NESTED
- 기존 트랜잭션 내부에 중첩된 트랜잭션을 생성한다.
- 중첩 트랜잭션은 독립적으로 롤백 가능하지만 부모 트랜잭션의 경계 내에 속한다.
코드로 보는 트랜잭션 전파 동작 예제
InnerService 클래스
InnerService는 여러 전파 속성을 테스트하기 위한 메서드들을 제공
@Transactional(propagation = Propagation.REQUIRED)
public void innerRequired() { logTransaction("innerRequired"); }
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void innerRequiresNew() { logTransaction("innerRequiresNew"); }
OuterService 클래스
OuterService는 InnerService를 호출하며, 상튀 트랜잭션에서 하위 메서드 호출 시 전파 속성이 어떻게 동작하는지 테스트
@Transactional(propagation = Propagation.REQUIRED)
public void outerRequired() {
logTransaction("outerRequired");
innerService.innerRequired();
innerService.innerRequiresNew();
}
실행 시 예상 로그
1. REQUIRED
outerRequired가 트랜잭션을 생성하며, innerRequired는 동일한 트랜잭션에 참여
[outerRequired] 트랜잭션 활성화됨. 이름: outerRequiredTransaction
[innerRequired] 트랜잭션 활성화됨. 이름: outerRequiredTransaction
2. REQUIRES_NEW
새로운 트랜잭션을 생성하며 기존 트랜잭션은 일시 중단
[innerRequiresNew] 트랜잭션 활성화됨. 이름: innerRequiresNewTransaction
Try-Catch 전략
- outerService가 Propagation.REQUIRED 이고 innerService도 Propagation.REQUIRED인 경우
> outerService에서 try-catch 전략을 쓰자! 어차피 같은 트랜잭션이라 inner에서 예외가 터져도 outer에서 잡으면 되니깐! - OuterService가 Propagation.REQUIRED이고 InnerService가 Propagation.REQUIRES_NEW인 경우
> 두 트랜재션은 서로 독립적으로 동작한다.
우리는 outerService가 롤백이 되는걸 원하지 않고 inner만 롤백되길 원하는거이기 때문에 InnerService에서 예외를 던지고 outer에서 try-catch로 잡으면 된다!
'Back-End > Spring' 카테고리의 다른 글
| SpringSecurity + JWT를 활용한 로그인 (0) | 2025.01.23 |
|---|---|
| Spring Security + OAuth2.0 + Jwt 활용 소셜 로그인 기능 만들기 (0) | 2025.01.21 |
| Spring Valid (1) | 2024.12.08 |
| Spring Bean 등록 (0) | 2024.12.08 |
| IOC / DI (0) | 2024.12.06 |