Tiny Star

IT

[Spring] @Transactional 트랜잭션 어노테이션

흰둥아 2025. 4. 28. 19:43

쿠폰 프로젝트를 진행하면서 사용하게 된 어노테이션이다.

 

[클라이언트 요청]
   ↓
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
top