@Transctional - 스프링 트랜잭션

정승현·2024년 11월 20일

@Transactional
Spring Framework에서 제공하는 선언적 트랜잭션 관리를 위한 어노테이션

  • 클래스 또는 메서드 레벨에 적용 가능
  • 선언적 트랜잭션 관리 방식 제공
  • AOP 기반으로 동작

💡동작방식
1. Spring 컨테이너가 @Transactional이 붙은 빈을 감지
2. 해당 빈에 대한 프록시 객체 생성
3. 메서드 호출 시 트랜잭션 관리자(PlatformTransactionManager)를 통해 트랜잭션 처리
4. 정상 종료 시 커밋, 예외 발생 시 롤백

💡주의사항

  • private 메서드 제약

    • private 메서드에는 @Transactional이 적용 X
    • 프록시 방식의 AOP 특성상 상속이 불가능한 private 메서드는 트랜잭션 처리가 불가능
  • 내부 호출 문제

    • 같은 클래스 내의 메서드 호출 시 트랜잭션이 적용되지 않을 수 있음
    public void test1() {
      test2();
    }
    
    @Transactional
    public void test2() {
      // 상위 메서드인 test1이 타깃 오브젝트가 되어 트랜잭션 적용 안됨
    }
    • 프록시를 통한 호출이 아닌 직접 호출의 경우 트랜잭션이 동작 X
    @Service
    public class UserService {
      
        public void external() {
            // 직접 호출 - 트랜잭션 적용 안됨
            this.internal();
        }
    
        @Transactional
        public void internal() {
            // 트랜잭션이 필요한 로직
        }
    }
    
    // 자기 자신 의존성 주입
    @Service
    public class UserService2 {
        @Autowired
        private UserService userService2;
    
        public void external() {
            // 프록시를 통한 호출
            userService2.internal();
        }
    }
    
    // 클래스 분리 등
  • 롤백 처리

    • 기본적으로 RuntimeException과 Error에 대해서만 롤백
    • 다른 예외에 대해 롤백이 필요한 경우 rollbackFor 속성을 사용하여 지정해야함

✍️ 트랜잭션 전파
기존 트랜잭션이 있을 때 새로운 트랜잭션을 어떻게 처리할지 결정하는 방식

  • 전파(Propagation) 옵션
    • REQUIRED (기본값) : 기존 트랜잭션이 존재하면 참여 없으면 새로 생성
    • REQUIRES_NEW : 항상 새로운 트랜잭션 생성
    • SUPPORTS : 기존 트랜잭션이 있으면 참여, 없으면 트랜잭션 없이 진행
    • MANDATORY : 기존 트랜잭션이 없으면 예외 발생
    • NEVER : 트랜잭션 사용 X, 기존 트랜잭션 있으면 예외 발생
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void someMethod() {
      // 메서드 로직
  }

✍️트랜잭션 격리수준(Transaction Isolation Level)
동시에 여러 트랜잭션이 처리될 때, 트랜잭션끼리 얼마나 서로 고립되어 있는지를 나타냄

  • Read Uncommitted (가장 낮은 격리수준)
    • 다른 트랜잭션에 의해 커밋되지 않은 데이터를 읽을 수 있음
    • Dirty Read, Non-Repeatable Read, Phantom Read 모두 발생 가능
    • 성능은 가장 좋지만, 데이터 일관성이 가장 낮음
  • Read Committed
    • 커밋된 데이터만 읽을 수 있음
    • Dirty Read 방지, 하지만 Non-Repeatable Read와 Phantom Read는 여전히 발생 가능
    • 대부분의 데이터베이스 시스템의 기본 격리수준
  • Repeatable Read
    • 트랜잭션이 시작되기 전에 커밋된 내용에 대해서만 조회 가능
    • Dirty Read, Non-Repeatable Read 방지, 하지만 Phantom Read는 여전히 발생 가능
    • 동일한 쿼리를 실행했을 때 항상 같은 결과를 보장
  • Serializable (가장 높은 격리수준)
    • 가장 엄격한 격리수준으로, 완벽한 읽기 일관성 제공
    • Dirty Read, Non-Repeatable Read, Phantom Read 모두 방지
    • 성능은 가장 낮지만, 데이터 일관성이 가장 높음

  • Dirty Read: 커밋되지 않은 데이터를 읽는 현상
  • Non-Repeatable Read: 같은 쿼리를 여러 번 실행했을 때 결과가 다른 현상
  • Phantom Read: 같은 쿼리를 여러 번 실행했을 때 이전에 없던 레코드가 나타나는 현상
  @Transactional(isolation = Isolation.READ_COMMITTED)
  public void someMethod() {
      // 메서드 로직
  }
  • rollbackFor : 지정된 예외 발생시 롤백
  @Transactional(rollbackFor = Exception.class)
  • noRollbackFor : 지정된 예외 발생 시 롤백하지 않음
    • 해당 Exception을 상속받은 모든 하위 Exception들도 롤백에서 제외
  @Transactional(noRollbackFor = RuntimeException.class)
  • readOnly : 읽기전용
  @Transactional(readOnly = true)
  • timeout : 트랜잭션 수행 시간 제한, 초과시 롤백 발생
  @Transactional(timeout = 10)
  @Transactional(
      propagation = Propagation.REQUIRES_NEW,
      isolation = Isolation.SERIALIZABLE,
      timeout = 10,
      rollbackFor = Exception.class
  )
  public void criticalOperation() {
      // 중요 로직
  }
// 트랜잭션의 수는 ?
@Service
public class OrderService {
    @Transactional
    public void createOrder() {
        saveOrder();
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveOrder() {
        // 주문 저장 로직
    }
}
// 트랜잭션이 롤백 되는가?
@Service
public class PaymentService {
    @Transactional
    public void processPayment() {
        try {
            // 결제 로직
            throw new RuntimeException("결제 오류");
        } catch (RuntimeException e) {
            log.error("에러 발생");
        }
    }
}

@Service
public class FileService {
    @Transactional
    public void processFile() throws IOException {
        throw new IOException("파일 처리 오류");
    }
}
// 트랜잭션이 롤백 되는가?
@Service
public class PaymentService {
    @Transactional
    public void processPayment() {
        try {
            // 결제 로직
            throw new RuntimeException("결제 오류");
        } catch (RuntimeException e) {
            log.error("에러 발생");
            throw CustomException("에러발생", e);
        }
    }
}

public class CustomException extends Exception {
    public PaymentException(String message, Throwable cause) {
        super(message, cause);
    }
}
// 실행결과 ?
@Service
public class UserService {
    @Transactional(readOnly = true)
    public void updateUser(User user) {
        userRepository.save(user);
    }
}

REFERENCE

profile
게시글 업로드중..⌛

0개의 댓글