@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을 메서드에 붙이면, 메서드 내에서 수행되는 모든 데이터베이스 관련 작업이 하나의 트랜잭션으로 묶입니다.트랜잭션 전파(Propagation)는 메서드가 실행될 때 이미 실행 중인 트랜잭션이 있는 경우, 해당 트랜잭션을 사용할지 또는 새로운 트랜잭션을 시작할지를 정의합니다. 스프링의 기본값은 Propagation.REQUIRED로, 현재 트랜잭션이 존재하면 이를 사용하고, 없으면 새로 시작합니다.
트랜잭션의 전파 옵션들은 다음과 같습니다.
트랜잭션 전파 설정 예시 코드
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는 기존 트랜잭션과는 독립적으로 실행됩니다.트랜잭션 격리 수준은 동시에 실행되는 여러 트랜잭션 간의 상호작용을 제어하는 방식입니다. 각 트랜잭션이 독립적으로 실행되도록 격리 수준을 설정할 수 있습니다.
트랜잭션 격리 수준 설정 예시 코드 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는 트랜잭션이 실행되는 동안 동일한 데이터를 여러 번 조회해도 변하지 않도록 보장합니다.트랜잭션 격리 수준 설정 예시 코드 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로 설정하여 가장 높은 수준의 데이터 무결성을 보장합니다.스프링은 기본적으로 RuntimeException과 Error가 발생했을 때 트랜잭션을 롤백합니다. 하지만 개발자가 필요한 경우 특정 예외에 대해 롤백을 정의할 수 있으며, 이때 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에 대해서만 롤백을 처리하지만, 체크 예외에 대해서도 롤백이 필요할 경우 이 속성을 활용합니다.데이터베이스에서 데이터를 조회만 하고 수정할 일이 없는 경우, 트랜잭션을 읽기 전용으로 설정할 수 있습니다. 이는 성능을 향상시킬 수 있으며, 특히 데이터베이스 최적화를 통해 불필요한 락을 방지할 수 있습니다.
읽기 전용 트랜잭션 설정 예시 코드
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)는 읽기 전용 트랜잭션을 설정하며, 데이터 수정 작업은 허용되지 않습니다.스프링의 @Transactional 어노테이션은 데이터베이스 트랜잭션 관리의 복잡성을 줄여주고, 개발자가 트랜잭션을 선언적으로 처리할 수 있게 해줍니다. 트랜잭션 전파, 격리 수준, 롤백 처리, 읽기 전용 설정 등 다양한 속성을 통해 트랜잭션을 세밀하게 제어할 수 있으며, 트랜잭션은 데이터 일관성을 유지하는 핵심 요소이므로 매우 중요합니다.
적절한 트랜잭션 관리는 애플리케이션의 성능과 안정성을 보장하는 중요한 부분이므로, @Transactional을 올바르게 이해하고 사용하는 것이 필수적입니다.