[Spring] @Transaction 정리

동긔·2024년 12월 5일

Spring

목록 보기
5/6

1. @Transactional 이란?

@Transactional은 스프링 프레임워크에서 제공하는 어노테이션으로, 특정 메서드 또는 클래스가 트랜잭션 내에서 실행되도록 보장합니다. 이는 주로 데이터베이스와 상호작용하는 서비스 레이어에서 사용되며, 트랜잭션의 시작, 커밋(성공 시 저장), 롤백(실패 시 복구)을 자동으로 처리합니다.

트랜잭션의 핵심 목표는 데이터의 무결성을 보장하는 것입니다. 예를 들어, 은행에서 계좌 이체를 할 때 출금이 성공했지만 입금이 실패하면 데이터의 무결성이 깨질 수 있습니다. 이러한 문제를 방지하기 위해 트랜잭션을 사용하여 출금과 입금 작업을 하나의 단위로 묶어, 둘 다 성공하거나 둘 다 실패하도록 합니다.

1.1 트랜잭션의 4가지 중요한 ACID 속성

  • Atomicity (원자성): 트랜잭션의 모든 작업이 전부 성공하거나 전부 실패해야 한다는 원칙입니다. 예를 들어, 하나라도 실패하면 나머지 성공한 작업도 모두 롤백해야 합니다.
  • Consistency (일관성): 트랜잭션이 실행된 후에도 데이터베이스의 상태는 일관성을 유지해야 합니다. 즉, 트랜잭션 전후의 데이터베이스는 무결성이 유지된 상태여야 합니다.
  • Isolation (격리성): 동시에 여러 트랜잭션이 실행될 때 서로 영향을 미치지 않도록 격리해야 합니다.
  • Durability (지속성): 트랜잭션이 성공적으로 완료되면 그 결과는 영구적으로 데이터베이스에 저장되어야 하며, 시스템이 갑작스럽게 중단되더라도 데이터는 유지되어야 합니다.

2. @Transactional의 사용법 및 옵션

2.1 기본적인 @Transactional 사용

기본적으로 @Transactional을 사용하면 스프링이 트랜잭션의 시작, 커밋, 롤백을 자동으로 관리합니다. 예외가 발생하지 않으면 트랜잭션이 커밋되고, 예외가 발생하면 자동으로 롤백됩니다. 이는 수작업으로 트랜잭션을 제어하는 복잡한 작업을 스프링이 자동으로 처리해주는 매우 편리한 방법입니다.

기본적인 @Transactional 사용 예시 코드

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    // @Transactional 어노테이션을 통해 트랜잭션을 관리
    @Transactional
    public void createUser(User user) {
        // 1. User 생성
        userRepository.save(user);

        // 2. 이후 다른 비즈니스 로직 수행
        // 만약 여기서 예외가 발생하면 롤백됨
    }
}
  • @Transactional을 메서드에 붙이면, 메서드 내에서 수행되는 모든 데이터베이스 관련 작업이 하나의 트랜잭션으로 묶입니다.
  • 예외가 발생하면 해당 트랜잭션 내의 모든 작업은 자동으로 롤백됩니다.
  • 만약 트랜잭션이 정상적으로 완료되면 데이터베이스에 커밋됩니다.

2.2 트랜잭션 전파 (Propagation)

트랜잭션 전파(Propagation)는 메서드가 실행될 때 이미 실행 중인 트랜잭션이 있는 경우, 해당 트랜잭션을 사용할지 또는 새로운 트랜잭션을 시작할지를 정의합니다. 스프링의 기본값은 Propagation.REQUIRED로, 현재 트랜잭션이 존재하면 이를 사용하고, 없으면 새로 시작합니다.

트랜잭션의 전파 옵션들은 다음과 같습니다.

  • REQUIRED : 이미 진행 중인 트랜잭션이 있으면 이를 사용하고, 없으면 새로 시작합니다. (기본값)
  • REQUIRES_NEW: 항상 새로운 트랜잭션을 시작하고, 기존 트랜잭션은 일시 중지됩니다.
  • SUPPORTS: 트랜잭션이 있으면 이를 사용하지만, 없으면 트랜잭션 없이 실행됩니다.
  • NOT_SUPPORTED: 현재 트랜잭션이 존재하면 이를 중지하고, 트랜잭션 없이 실행됩니다.
  • MANDATORY: 반드시 트랜잭션이 있어야 하며, 없으면 예외를 발생시킵니다.
  • NEVER: 트랜잭션 없이 실행되며, 트랜잭션이 존재하면 예외를 발생시킵니다.
  • NESTED: 부모 트랜잭션의 하위 트랜잭션으로 실행되며, 저장 지점(Savepoint)을 생성하여 독립적으로 롤백 가능합니다.

트랜잭션 전파 설정 예시 코드

import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class PaymentService {

    @Autowired
    private OrderService orderService;

    @Transactional(propagation = Propagation.REQUIRED)
    public void processPayment(Payment payment) {
        // 트랜잭션 내에서 처리됨
        // orderService.processOrder()는 동일한 트랜잭션을 사용
        orderService.processOrder(payment.getOrder());

        // 결제 처리 로직
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logPaymentDetails(PaymentLog paymentLog) {
        // 이 메서드는 별도의 새로운 트랜잭션에서 실행됨
        // 기존 트랜잭션과는 독립적임
        paymentLogRepository.save(paymentLog);
    }
}
  • Propagation.REQUIRED: 현재 진행 중인 트랜잭션이 있으면 이를 사용하고, 없으면 새로 시작합니다. 기본값입니다.
  • Propagation.REQUIRES_NEW: 항상 새로운 트랜잭션을 시작하고, 기존 트랜잭션을 중지한 뒤에 처리합니다. 이 경우 logPaymentDetails는 기존 트랜잭션과는 독립적으로 실행됩니다.

2.3 트랜잭션 격리 수준 (Isolation)

트랜잭션 격리 수준은 동시에 실행되는 여러 트랜잭션 간의 상호작용을 제어하는 방식입니다. 각 트랜잭션이 독립적으로 실행되도록 격리 수준을 설정할 수 있습니다.

  • DEFAULT: 데이터베이스의 기본 격리 수준을 따릅니다. 대부분의 경우 READ_COMMITTED가 기본값입니다.
  • READ_UNCOMMITTED: 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽을 수 있습니다. (Dirty Read 허용)
  • READ_COMMITTED: 다른 트랜잭션이 커밋한 데이터만 읽을 수 있습니다. (Dirty Read 방지)
  • REPEATABLE_READ: 트랜잭션 내에서 동일한 데이터를 여러 번 조회해도 항상 동일한 데이터를 반환합니다. (Non-repeatable Read 방지)
  • SERIALIZABLE: 트랜잭션이 직렬화되어 실행되며, 가장 엄격한 격리 수준입니다. 동시성을 거의 허용하지 않지만, 데이터 무결성을 가장 강력하게 보장합니다.

트랜잭션 격리 수준 설정 예시 코드 1 (Isolation.REPEATABLE_READ)

import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class AccountService {

    private AccountRepository accountRepository;

    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
        // 트랜잭션 시작

        Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow();
        Account toAccount = accountRepository.findById(toAccountId).orElseThrow();

        fromAccount.withdraw(amount);
        toAccount.deposit(amount);

        // 트랜잭션 종료 - 커밋
    }
}
  • Isolation.REPEATABLE_READ는 트랜잭션이 실행되는 동안 동일한 데이터를 여러 번 조회해도 변하지 않도록 보장합니다.
  • Non-Repeatable Read 문제를 방지합니다.
    • 예시로, 트랜잭션 A에서 같은 데이터를 두 번 조회했을 때, 트랜잭션 B가 데이터를 수정하더라도 트랜잭션 A의 조회 결과는 변하지 않습니다.
  • Phantom Read는 방지하지 못하며, 특정 조건의 데이터 집합이 트랜잭션 내에서 변경될 수 있습니다.
  • 성능과 데이터 일관성 간의 균형이 적절하여 대부분의 트랜잭션에서 추천되는 격리 수준입니다.
  • 즉, 트랜잭션이 진행되는 동안 다른 트랜잭션이 데이터를 수정할 수 없으므로, 데이터 일관성을 더욱 강하게 보장할 수 있습니다.

트랜잭션 격리 수준 설정 예시 코드 2 (Isolation.SERIALIZABLE)

import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountService {

    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
        // 특정 계좌의 정보를 가져옴. 계좌가 존재하지 않으면 예외 발생.
        Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow(() -> 
                new IllegalArgumentException("Sender account not found"));
        Account toAccount = accountRepository.findById(toAccountId).orElseThrow(() -> 
                new IllegalArgumentException("Receiver account not found"));

        // 출금 작업 수행. 금액이 부족하면 예외가 발생할 수 있음.
        fromAccount.withdraw(amount);

        // 입금 작업 수행.
        toAccount.deposit(amount);

        // 변경된 계좌 정보 저장. 트랜잭션 내에 포함되어 있음.
        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);
    }
}
  • isolation = Isolation.SERIALIZABLE는 트랜잭션 격리 수준을 SERIALIZABLE로 설정하여 가장 높은 수준의 데이터 무결성을 보장합니다.
  • 트랜잭션이 직렬화되며, 다른 트랜잭션이 현재 트랜잭션의 범위 내 데이터에 접근하거나 수정할 수 없습니다.
  • 이 설정은 Phantom Read를 방지하며, 동시성 문제가 완전히 제거됩니다.
  • 단점으로는 성능 저하가 일어날 수 있습니다. 데이터베이스에 많은 락이 걸리므로 대규모 트래픽 환경에서는 비효율적일 수 있습니다.

2.4 트랜잭션 롤백 처리 (RollBack)

스프링은 기본적으로 RuntimeExceptionError가 발생했을 때 트랜잭션을 롤백합니다. 하지만 개발자가 필요한 경우 특정 예외에 대해 롤백을 정의할 수 있으며, 이때 rollbackFor 속성을 사용하여 처리할 수 있습니다.

특정 예외에 대한 롤백 처리 예시 코드

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Transactional(rollbackFor = {CustomException.class})
    public void addProduct(Product product) throws CustomException {
        // 간단한 검증 로직
        if (product.getName() == null || product.getName().isEmpty()) {
            // 필요한 데이터가 없을 때 CustomException 발생
            throw new CustomException("상품 이름이 필요합니다.");
        }
        
        // 레포지토리를 통해 데이터베이스에 저장
        productRepository.save(product);
    }
}
  • rollbackFor 속성을 사용하면, CustomException과 같은 특정 예외가 발생했을 때도 트랜잭션을 롤백할 수 있습니다.
  • 기본적으로 스프링은 RuntimeException에 대해서만 롤백을 처리하지만, 체크 예외에 대해서도 롤백이 필요할 경우 이 속성을 활용합니다.

2.5 트랜잭션 읽기 전용 (ReadOnly)

데이터베이스에서 데이터를 조회만 하고 수정할 일이 없는 경우, 트랜잭션을 읽기 전용으로 설정할 수 있습니다. 이는 성능을 향상시킬 수 있으며, 특히 데이터베이스 최적화를 통해 불필요한 락을 방지할 수 있습니다.

읽기 전용 트랜잭션 설정 예시 코드

import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class ReportService {

    private ReportRepository reportRepository;

    @Transactional(readOnly = true)
    public List<Report> getAllReports() {
        // 이 메서드는 오직 읽기 전용 트랜잭션에서 실행됨
        return reportRepository.findAll();
    }
}
  • @Transactional(readOnly = true)는 읽기 전용 트랜잭션을 설정하며, 데이터 수정 작업은 허용되지 않습니다.
  • 읽기 전용 트랜잭션은 데이터베이스의 최적화 기능을 활용해 성능을 높일 수 있습니다.

3. 결론

스프링의 @Transactional 어노테이션은 데이터베이스 트랜잭션 관리의 복잡성을 줄여주고, 개발자가 트랜잭션을 선언적으로 처리할 수 있게 해줍니다. 트랜잭션 전파, 격리 수준, 롤백 처리, 읽기 전용 설정 등 다양한 속성을 통해 트랜잭션을 세밀하게 제어할 수 있으며, 트랜잭션은 데이터 일관성을 유지하는 핵심 요소이므로 매우 중요합니다.

적절한 트랜잭션 관리는 애플리케이션의 성능과 안정성을 보장하는 중요한 부분이므로, @Transactional을 올바르게 이해하고 사용하는 것이 필수적입니다.

profile
배우고 느낀점들을 기록합니다. 열정 넘치는 백엔드 개발자로 남고싶습니다.

0개의 댓글