Java

[JAVA] @Transactional 알아보기 Part.2 #전파(Propagation)

TaeHuiLee 2024. 12. 24. 08:00
반응형

📌 트랜잭션 전파(Propagation)란?

트랜잭션 전파(Propagation)는 트랜잭션의 존재 여부와 관계를 정의합니다.

  • 현재 트랜잭션이 있는 경우 그 트랜잭션에 참여할지,
  • 새로 트랜잭션을 생성할지,
  • 트랜잭션 없이 실행할지를 결정합니다.

Spring에서 제공하는 전파 속성@Transactional 어노테이션의 propagation 속성으로 설정할 수 있습니다.


📌 트랜잭션 전파와 격리 수준의 차이

전파(Propagation)

  • 트랜잭션의 관계를 설정합니다.
  • 예: 새로운 트랜잭션 생성, 기존 트랜잭션 참여 등.

격리 수준(Isolation Level)

  • 동시에 실행되는 트랜잭션 간 데이터 접근 규칙을 설정합니다.
  • 예: Dirty Read 방지, Repeatable Read 보장 등.

간단히: 전파는 트랜잭션의 관계, 격리 수준은 데이터 무결성을 관리.


📌 트랜잭션 전파 속성의 종류와 동작 방식

Spring은 7가지 전파 속성을 제공합니다. 각 속성의 동작 방식을 살펴봅니다.

속성 설명
REQUIRED (기본값) 기존 트랜잭션에 참여. 없으면 새로 생성.
REQUIRES_NEW 항상 새로운 트랜잭션 생성. 기존 트랜잭션은 일시 중단.
SUPPORTS 트랜잭션이 있으면 참여, 없으면 트랜잭션 없이 실행.
NOT_SUPPORTED 트랜잭션 없이 실행. 현재 트랜잭션이 있으면 일시 중단.
MANDATORY 반드시 기존 트랜잭션에 참여. 없으면 예외 발생.
NEVER 트랜잭션이 있으면 예외 발생. 트랜잭션 없이 실행
NESTED 부모 트랜잭션 내부에서 독립적인 롤백이 가능한 트랜잭션 생성. (Savepoint 기반)

📌 주요 전파 속성의 상세 동작 방식과 예제


1️⃣ REQUIRED 전파 속성

  • 기존 트랜잭션이 있으면 참여, 없으면 새 트랜잭션 생성.
  • 기본값으로 설정되어 있으며, 가장 일반적으로 사용됩니다.

사용 예제: 주문 및 결제

  • 주문과 결제는 동일한 트랜잭션 안에서 처리되어야 합니다.

코드

@Service
public class OrderService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void placeOrder() {
        saveOrderDetails();  // 같은 트랜잭션
        processPayment();    // 같은 트랜잭션
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void saveOrderDetails() {
        // 주문 정보 저장
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void processPayment() {
        // 결제 처리
    }
}

결과

  • placeOrder 메서드에서 오류 발생 시, 모든 작업이 롤백됩니다.
  • 동일한 트랜잭션으로 처리됩니다.

2️⃣ REQUIRES_NEW 전파 속성

  • 항상 새로운 트랜잭션 생성.
  • 부모 트랜잭션은 일시 중단됩니다.

사용 예제: 주문 처리와 로그 저장

  • 주문 처리와 로그 저장은 서로 독립적이어야 합니다.

코드

@Service
public class OrderService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void processOrder() {
        saveOrderDetails();  // 부모 트랜잭션
        saveAuditLog();      // 독립 트랜잭션
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void saveOrderDetails() {
        // 주문 데이터 저장
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveAuditLog() {
        // 로그 데이터 저장
    }
}

결과

  • saveAuditLog는 부모 트랜잭션과 독립적으로 커밋/롤백됩니다.
  • saveOrderDetails에서 예외가 발생해도, 로그는 저장됩니다.

3️⃣ SUPPORTS 전파 속성

  • 트랜잭션이 있으면 참여, 없으면 트랜잭션 없이 실행.

사용 예제: 상품 조회

  • 상품 조회 작업은 트랜잭션 없이도 실행 가능하지만, 트랜잭션이 있다면 참여합니다.

코드

@Service
public class ProductService {
    @Transactional(propagation = Propagation.SUPPORTS)
    public List<Product> getAllProducts() {
        // 상품 조회
        return productRepository.findAll();
    }
}

결과

  • 트랜잭션 컨텍스트 내에서 호출되면 트랜잭션에 참여합니다.
  • 트랜잭션 컨텍스트가 없으면 트랜잭션 없이 실행됩니다.

4️⃣ NOT_SUPPORTED 전파 속성

  • 트랜잭션 없이 실행하며, 현재 트랜잭션이 있으면 일시 중단됩니다.

사용 예제: 파일 저장

  • 파일 저장 작업은 데이터베이스 트랜잭션과 분리되어야 합니다.

코드

@Service
public class FileService {
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void saveFile(File file) {
        // 파일 저장 작업
    }
}

결과

  • 데이터베이스 트랜잭션과 무관하게 파일 저장 작업이 수행됩니다.

5️⃣ MANDATORY 전파 속성

  • 반드시 기존 트랜잭션에 참여해야 하며, 없으면 예외 발생.

사용 예제: 주문 상태 업데이트

  • 주문 상태를 업데이트하려면 반드시 주문 생성 트랜잭션 내에서 실행되어야 합니다.

코드

@Service
public class OrderService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void processOrder() {
        updateOrderStatus();  // 기존 트랜잭션에 참여
    }

    @Transactional(propagation = Propagation.MANDATORY)
    public void updateOrderStatus() {
        // 주문 상태 업데이트
    }
}

결과

  • processOrder가 트랜잭션 없이 호출되면 예외 발생.
  • 트랜잭션 내에서 호출될 때만 정상 실행.

6️⃣ NEVER 전파 속성

  • 트랜잭션이 있으면 예외 발생, 없으면 트랜잭션 없이 실행.

사용 예제: 캐싱 작업

  • 캐싱 작업은 트랜잭션과 분리되어야 하며, 트랜잭션 내에서 호출되면 안 됩니다.

코드

@Service
public class CacheService {
    @Transactional(propagation = Propagation.NEVER)
    public void cacheData(Object data) {
        // 캐싱 작업
    }
}

결과

  • 트랜잭션 내에서 호출되면 IllegalTransactionStateException 발생.
  • 트랜잭션 없이 호출될 때만 실행.

7️⃣ NESTED 전파 속성

  • 부모 트랜잭션 안에서 동작하며, Savepoint를 생성해 트랜잭션의 일부 작업만 롤백 가능합니다.
  • 부모 트랜잭션은 유지되며, 자식 트랜잭션에서 발생한 오류는 Savepoint를 활용해 부분적으로 롤백됩니다.

📌 동작 원리

  1. 부모 트랜잭션이 시작되면, NESTED 전파를 가진 메서드가 호출될 때 Savepoint가 생성됩니다.
  2. 자식 트랜잭션에서 오류가 발생하면 Savepoint까지만 롤백되며, 부모 트랜잭션은 영향을 받지 않습니다.
  3. 부모 트랜잭션이 커밋되지 않으면 자식 트랜잭션도 커밋되지 않습니다.

📌 사용 예제: 주문과 배송 처리

상황

  • 주문 데이터 저장배송 데이터 저장을 처리해야 합니다.
  • 배송 처리 중 문제가 발생하면, 배송 데이터만 롤백되고 주문 데이터는 유지됩니다.

코드

@Service
public class OrderService {

    @Transactional(propagation = Propagation.REQUIRED)
    public void processOrder() {
        saveOrderDetails();     // 부모 트랜잭션
        processShipping();      // Savepoint 생성 (NESTED)
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void saveOrderDetails() {
        // 주문 데이터 저장 (부모 트랜잭션)
        System.out.println("Order details saved");
    }

    @Transactional(propagation = Propagation.NESTED)
    public void processShipping() {
        System.out.println("Processing shipping...");
        if (someConditionFails()) {
            throw new RuntimeException("Shipping failed!");
        }
        // 배송 데이터 저장
        System.out.println("Shipping details saved");
    }

    private boolean someConditionFails() {
        return true; // 테스트를 위해 항상 실패하도록 설정
    }
}

📌 동작 흐름

  1. processOrder() 호출
    • 부모 트랜잭션이 시작됩니다 (Propagation.REQUIRED).
    • saveOrderDetails() 실행 → 주문 데이터 저장.
  2. processShipping() 호출
    • NESTED 전파에 따라 Savepoint가 생성됩니다.
    • 배송 데이터 처리 중 예외 발생(throw new RuntimeException()).
  3. 롤백
    • 자식 트랜잭션(processShipping)이 Savepoint로 롤백됩니다.
    • 부모 트랜잭션(processOrder)은 유지되며 커밋 가능합니다.

📌 실행 결과

  1. saveOrderDetails()에서 주문 정보가 저장됩니다.
  2. processShipping()에서 예외가 발생해 배송 데이터 저장이 롤백됩니다.
  3. 부모 트랜잭션은 성공적으로 커밋됩니다.

📌 결과 해석

장점

  • 부모 트랜잭션을 유지하면서 자식 트랜잭션만 롤백 가능.
  • 복잡한 작업에서 부분 롤백이 필요한 경우 유용.

적용 사례

  • 주문은 유지해야 하지만, 배송 데이터를 별도로 롤백해야 하는 경우.
  • 부모 작업의 성공 여부와 관계없이 하위 작업만 롤백해야 할 때.

📌 전파 속성으로 인해 전체 작업이 롤백되는 상황과 해결 방법

문제

  • 부모 트랜잭션이 롤백되면, 참여 중인 모든 트랜잭션이 함께 롤백됩니다.

해결 방법

  • REQUIRES_NEW를 사용해 부모 트랜잭션과 독립된 트랜잭션 생성.

예시 코드

@Transactional
public void parentTransaction() {
    childTransaction(); // 부모 트랜잭션 참여
    saveLog(); // 독립 트랜잭션
}

@Transactional(propagation = Propagation.REQUIRED)
public void childTransaction() {
    throw new RuntimeException("Rollback!");
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog() {
    // 독립적으로 로그 저장
}

📌 독립적인 트랜잭션에서 예외 발생 시 처리 방법


문제

  • 독립 트랜잭션에서 예외가 발생해도 부모 트랜잭션에 영향을 미치지 않아야 함.

해결 방법

  • REQUIRES_NEW로 독립 트랜잭션 생성.
  • 부모 트랜잭션과 상관없이 커밋/롤백.

예시 코드

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog() {
    try {
        // 로그 저장 작업
    } catch (Exception e) {
        // 예외 처리
    }
}

📌 전파 속성으로 인해 새로운 트랜잭션을 만들지 못하는 상황

문제

  • 클래스 내부 호출로 인해 전파 속성이 적용되지 않음.

해결 방법

  1. AopContext 사용
    • 내부 호출 시 프록시 객체를 강제로 참조.
  2. 메서드 분리
    • 호출 메서드를 별도 빈으로 이동하여 외부 호출이 가능하도록 설계.

🚀 요약

  1. 전파 속성
    • 트랜잭션의 생성/참여 여부를 결정.
    • REQUIRED, REQUIRES_NEW, SUPPORTS 등 다양한 설정 제공.
  2. 주요 전파 속성
    • REQUIRED: 기본값. 트랜잭션 참여 또는 생성.
    • REQUIRES_NEW: 새로운 독립 트랜잭션 생성.
    • NESTED: 부모 트랜잭션의 일부로 동작하며 Savepoint 생성.
  3. 전파 속성 문제와 해결 방법
    • 부모 트랜잭션 롤백 문제는 REQUIRES_NEW로 해결.
    • 내부 호출 문제는 AopContext빈 분리로 해결.
반응형