내배캠/TIL

[스프링] 트랜잭션 활용과 주의사항

모닝펄슨 2024. 10. 1. 23:50

ACID

  • Atomicity, 원자성
    • 트랜잭션 내 실행 작업들은 모두 하나의 작업처럼 성공하던가 실패해야 함. 만약 하나의 작업이 실패한다면 전체 rollback, 전체가 성공하면 commit 이 발생.
  • Consistency, 일관성
    • 트랜잭션의 수행 전후의 데이터가 데이터베이스의 규칙을 따르는지
    • 트랜잭션 완료되면 데이터는 항상 db에서 정한 제약 조건 만족해야 함.
  • Isolation, 격리성
    • 동시에 실행되는 트랜잭션들이 서로에게 어떤 영향을 미칠 수 있는지 격리 수준 정하는 것.
    • 트랜잭션과 동시성
  • Durability, 지속성
    • 트랜잭션이 성공적으로 끝나면 그 결과가 항상 기록되어야 한다는 의미
    • 시스템에 문제가 발생하더라도 로그를 활용해 성공한 트랜잭션의 내용을 복구해야 함.
    • WAL (Write Ahead Logging)

 

스프링에서 트랜잭션 관리 방법

롤백 정책 (rollbackFor)

  • 기본 롤백 정책은 Runtime Exception
  • 체크 예외는 기본적으로 롤백 발생하지 않음!!
  • rollbackFor, noRollbackFor 를 사용해서 특정 예외를 rollback 정책 조건에 추가하거나 제거하고 싶을 경우 사용
/**
* 기본 rollback 정책
*/
@Transactional
public void updateCourseWithDefault() {
	// ...
}

/**
* rollbackFor 활용
* 기존 rollback 정책 조건에 특정 예외를 추가시킨다.
*/
@Transactional(rollbackFor = Exception.class)
public void updateCourseWithRollbackFor() {
	// ...
}

/**
* noRollbackFor 활용
* 기존 rollback 정책 조건에 특정 예외를 제거시킨다.
*/
@Transactional(noRollbackFor = RuntimeException.class)
public void updateCourseWithNoRollbackFor() {
	// ...
}

 

 

전파 (Propagation)

트랜잭션이 메서드간 겹칠 때 어떻게 동작할지 설정.

  • 기본 : Requried
  • @Transactional 달려있는 메서드 호출할 때 그 메서드 안에서 또 @Transactional 달려있음.

 

  • REEQUIRED
    • 기본 전파 속성
    • 있으면 참여, 없으면 생성
  • REQUIRES_NEW
    • 항상 새로운 트랜잭션 시작
    • 기존 진행은 일단 보류
    • 자식 트랜잭션이 커밋되면 이어서 진행
  • SUPPORTS
    • 기존 트랜잭션 있다면 참여
    • 없으면 없이 진행
  • NESTED
    • 이미 진행중인 트랜잭션 있다면 중첩 트랜잭션 시작
    • 부모 트랜잭션이 ROLLBACK되면 자신도 ROLLBACK
    • DBMS마다 지원 여부 다름.
  • NEVER
    • 사용 불가
    • 트랜잭션 존재하면 예외 발생
  • MANDATORY, NOT_SUPPORTED …

 

트랜잭션 사용시 주의사항

수강 신청 시나리오를 가정했을 때, 로그에서 예외가 발생한다고 모든 프로세스가 실패하는 것은 바람직하지 않다. 그래서 로그 부분의 트랜잭션은 REQUIRES_NEW를 이용해 새로운 트랜잭션을 시작하도록 했다. 그러나 결과가 의도한 대로 나오지 않았다. 왜 그럴까?

    @Transactional
    public void processEnrollV1() {
        printActiveTransaction("::: EnrollmentService.processEnrollV1()");

        // 1. 수강 처리: Course 엔티티 생성
        processCourse();

        // 2. 결제 처리: Payment 엔티티 생성
        processPayment();

        // 3. 로그 처리: PaymentLog 로그 생성
        try {
            processLog();
        } catch (Exception e) {
            log.info("로그 예외 발생");
        }

    }

    @Transactional
    public void processCourse() {
        printActiveTransaction("processCourse()");

        // 1. 저장 - Course 저장
        Course newCourse = Course.createNewCourse("newSpring");
        courseRepository.save(newCourse);
    }

    @Transactional
    public void processPayment() {
        printActiveTransaction("processPayment()");

        // 1. 저장 - Payment 저장
        Payment newPayment = new Payment();
        paymentRepository.save(newPayment);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processLog() {
        printActiveTransaction("processLog()");

        // 1. 저장 - PaymentLog 저장
        PaymentLog newPaymentLog = new PaymentLog();
        logRepository.save(newPaymentLog);
        throw new RuntimeException("로그 예외 발생");
    }

 

[결과]

EnrollmentService.processEnrollV1() - isActive: true, isNew: true
processCourse() - isActive: true, isNew: true
processPayment() - isActive: true, isNew: true
processLog() - isActive: true, isNew: true

 

최상단의 processEnrollV1를 제외한 나머지 메서드들에는 @Transactional 어노테이션이 적용되지 않는다!

 

 

트랜잭션 동작 원리

 

트랜잭션은 스프링에서 가장 대표적으로 사용하는 AOP 이다. 그리고 AOP 를 적용한 객체는 프록시 객체로 등록이 된다.

 

Self-invocation 문제

 

processCourse(), processPayment(), processLog() 는 Target(EnrollmentService) 본인 스스로가 본인의 메서드를 호출한 것이기 때문에 proxy 가 적용이 되지 않는다. 그렇기에 트랜잭션을 적용하고 싶다면 자기 자신의 메서드를 호출하는 것이 아니라, 따로 클래스를 만들어서 호출해줘야 한다.

 

 

 

+ REQUIRES_NEW의 오해

REQUIRES_NEW 라고 하면 완전히 분리된 전파 속성으로 받아드리는 경우가 많은데, 별도의 트랜잭션이라고 하더라도 하위 메서드에서 발생한 예외는 항상 상위 메서드로 전파되기 때문에 예외처리를 적절하게 해줘야 한다. 아니라면 REQUIRES_NEW로 새로운 트랜잭션을 생성했어도 자식에서 예외 발생하면 다 같이 롤백되는 수가 있다.