쿠폰 프로젝트를 진행하면서 사용하게 된 어노테이션이다.
[클라이언트 요청]
↓
POST /api/admin/coupons
↓
[CouponService]
↓
1. couponsRepository.save(coupon) 실행
↓
2. RedisTemplate 재고 등록
Redis에 재고 등록을 하게되는데, 만약 이 때 오류로 Redis 등록에 실패했다면 DB에 저장된 데이터도 삭제하는게 좋다고 판단했다. 왜냐면 실패했을 시 똑같은 쿠폰을 다시 발행할텐데, DB 에는 동일한 이름의 쿠폰이 두 개가 등록되기 때문이다.
1. @Transactional
@Transactional은 메서드 실행을 하나의 트랜잭션 단위로 묶어서,
전체가 성공하면 커밋(commit),
중간에 실패하면 롤백(rollback) 하게 만들어주는 Spring의 기능이다.
즉, @Transactional 로 선언된 메서드에서 발생하는 DB 작업을 하나의 묶음(트랜잭션)으로 처리하게 해준다는 뜻이다.
2. 실제 작업
단계 | 동작 |
메서드 진입 시 | 트랜잭션 시작 (Connection.setAutoCommit(false)) |
메서드 정상 종료 시 | 트랜잭션 커밋 (Connection.commit()) |
메서드에서 예외 발생 시 | 트랜잭션 롤백 (Connection.rollback()) |
메서드가 끝날 때까지 SQL(insert, update 등)은 실제 DB에 커밋되지 않고 '대기'한다.
메서드가 성공하면 한 번에 커밋, 실패하면 한 번에 롤백하는 구조이다.
3. 롤백 규칙
상황 | 롤백 여부 |
RuntimeException 발생 | 자동 롤백 ✅ |
Error 발생 | 자동 롤백 ✅ |
CheckedException 발생 (ex: IOException) | 롤백 ❌ (기본적으로 롤백되지 않는다) |
아무 예외 없음 | 정상 커밋 ✅ |
RuntimeException, Error만 자동으로 롤백한다고 보면 된다.
롤백을 제어하는 방법
제어 방법 | 예시 |
CheckedException에도 롤백하고 싶을 때 | @Transactional(rollbackFor = Exception.class) |
특정 예외만 롤백하고 싶을 때 | @Transactional(rollbackFor = MyCustomException.class) |
특정 예외는 롤백하고 싶지 않을 때 | @Transactional(noRollbackFor = SomeException.class) |
@Transactional에서 rollbackFor 설정은 CheckedException(예: Exception)을 롤백 대상으로 추가하거나, 특정 예외(MyCustomException.class)만 롤백 대상으로 제한하는 데 사용된다. 문법은 같지만 의미가 다르므로 구분이 필요하다.
예제
- 기본 사용: RuntimeExceptiond 을 던지면 자동으로 롤백
@Transactional
public void registerCoupon() {
couponsRepository.save(new Coupon(...));
if (오류) {
throw new RuntimeException("오류 발생");
}
}
- 추가: Exception도 롤백됨
@Transactional(rollbackFor = Exception.class)
public void registerCoupon() throws Exception {
couponsRepository.save(new Coupon(...));
if (문제) {
throw new Exception("체크드 예외 발생");
}
}
4. 내부 호출주의 (self-invocation 문제)
같은 클래스 안에서 @Transactional 메서드를 호출하면 트랜잭션이 적용되지 않는다.
public class CouponService {
@Transactional
public void methodA() {
methodB(); // ❌ methodB의 @Transactional 적용 안됨
}
@Transactional
public void methodB() {
...
}
}
트랜잭션은 프록시(Proxy) 기반이기 때문에 외부에서 메서드를 호출할 때만 동작한다.
5. @Transactional Propagation (전파 옵션)
옵션 | 설명 |
REQUIRED (기본값) | 기존 트랜잭션이 있으면 합쳐지고, 없으면 새로 만든다 |
REQUIRES_NEW | 기존 트랜잭션 무시하고 무조건 새 트랜잭션 만든다 |
NESTED | 기존 트랜잭션 안에서 savepoint를 만들어 부분 롤백 가능 |
REQUIRED
@Transactional
public void methodA() {
methodB();
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
...
}
- 흐름
- methodA() 진입 → 트랜잭션 시작
- methodB() 호출 → 기존 트랜잭션 그대로 이어서 사용
- 둘 다 성공해야 커밋, 하나라도 실패하면 둘 다 롤백
REQUIRES_NEW
@Transactional
public void methodA() {
methodB();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
...
}
- 흐름
- methodA() 진입 → 트랜잭션 시작
- methodB() 호출 → 기존 트랜잭션 잠시 보류하고 새 트랜잭션 시작
- methodB()만 따로 커밋/롤백
- methodB() 끝나면 다시 methodA() 트랜잭션으로 돌아옴
- 각각의 메서드마다 롤백 적용 >> 메일 보내기, 알림 보내기 등 메인 로직과 별도로 실패해도 괜찮은 작업을 하고 싶을 때
NESTED
@Transactional
public void methodA() {
System.out.println("methodA 시작");
saveCoupon();
try {
saveLog(); // 이거 실패한다고 가정
} catch (RuntimeException e) {
System.out.println("saveLog 실패, 하지만 methodA는 계속 진행");
}
System.out.println("methodA 끝");
}
@Transactional(propagation = Propagation.REQUIRED)
public void saveCoupon() {
System.out.println("쿠폰 저장 완료");
// DB insert 쿼리: 쿠폰 저장
}
@Transactional(propagation = Propagation.NESTED)
public void saveLog() {
System.out.println("로그 저장 시도");
throw new RuntimeException("로그 저장 실패!"); // 예외 발생
}
- 흐름
- methodA() 시작 → 트랜잭션 시작
- saveCoupon() 호출 → 같은 트랜잭션(Propagation.REQUIRED) 안에서 진행 → OK
- saveLog() 호출 → 기존 트랜잭션 안에서 savepoint 생성
- saveLog()에서 RuntimeException 발생
- saveLog()에서 생성한 savepoint로 롤백 → saveLog()에서 한 작업만 롤백
- methodA()는 계속 진행 → 최종적으로 methodA() 트랜잭션은 정상 커밋 가능
- 오류 나면 savepoint까지 롤백 (methodA는 계속 진행 가능)
'IT' 카테고리의 다른 글
[데이터] Kaggle (캐글) (0) | 2025.05.05 |
---|---|
Redis란? (1) | 2025.04.28 |
[JAVA] MyBatis VS JPA (0) | 2025.04.23 |