본문 바로가기

Spring

Spring - @Transactional

`@Transactional` 은 Spring에서 제공하는 트랜잭션 관리 어노테이션으로, 특히 데이터의 `일관성`을 보장하기 위해 주로 사용됩니다. `트랜잭션(transaction)`은 데이터베이스 작업을 수행하는 중간에 오류가 발생하면, 수행한 작업이 취소되고 데이터가 원래 상태로 복원될 수 있도록 하는 개념입니다. 즉, `모든 작업이 성공`해야만 변경 사항이 데이터베이스에 반영되고, 실패하면 전부 취소됩니다. 이를 통해 데이터의 신뢰성과 무결성을 보장합니다.

 

1. 트랜잭션의 기초

트랜잭션은 데이터베이스 작업에서 다음과 같은 ACID 특성을 만족해야 합니다:

  • Atomicity (원자성): 트랜잭션 내 모든 작업은 하나의 단위로 수행됩니다. 일부 작업만 성공하고 나머지가 실패할 수는 없습니다. 모두 완료되거나, 모두 취소됩니다.
  • Consistency (일관성): 트랜잭션이 성공적으로 완료되면, 데이터베이스는 일관된 상태를 유지합니다.
  • Isolation (고립성): 여러 트랜잭션이 동시에 실행될 때도 서로 간섭하지 않도록 보장합니다.
  • Durability (지속성): 트랜잭션이 성공적으로 완료되면 그 결과는 영구적으로 저장됩니다.

 

간단한 예제로 주문 업데이트 로직을 보겠습니다.

@Service
public class OrderService {

    @Transactional
    public void updateOrderStatus(Long orderId, OrderStatus status) {
        Order order = orderRepository.findById(orderId)
                .orElseThrow(() -> new OrderNotFoundException("Order not found"));
        
        order.setStatus(status);

        if (status == OrderStatus.CANCELLED) {
            refund(order);  // 이 메소드도 트랜잭션 내에서 실행됩니다.
        }
    }

    private void refund(Order order) {
        // 환불 로직 수행
    }
}

 

위 코드에서 `@Transactional`은 `updateOrderStatus` 메소드가 예외 없이 실행될 때만 데이터베이스에 변경 사항이 반영되도록 보장합니다. `refund` 메소드에서도 예외가 발생하면 `updateOrderStatus` 의 모든 작업이 롤백됩니다.

2. @Transactional의 사용 목적과 기본 동작

Spring에서는 `@Transactional`을 메소드 또는 클래스에 적용하여 해당 메소드나 클래스에서 실행되는 모든 데이터베이스 작업이 트랜잭션으로 관리되도록 할 수 있습니다. 이 어노테이션을 사용하면 다음과 같은 동작을 제공합니다:

  • 메소드 실행 시작 시 트랜잭션이 시작됩니다.
  • 메소드가 성공적으로 끝나면, commit이 수행되어 데이터베이스에 변경 사항이 반영됩니다.
  • 메소드 중간에 예외가 발생하면 rollback이 수행되어 변경 사항이 취소됩니다.

 

 

3. 언제 @Transactional을 사용해야 하는가?

다음과 같은 경우에 @Transactional을 사용하는 것이 좋습니다:

  • Update, Delete 작업: 데이터의 변경이 발생할 때, 트랜잭션을 통해 작업이 완전하게 수행되거나 완전히 취소될 수 있도록 해야 합니다.
  • 여러 데이터베이스 작업이 하나의 로직에 포함될 때: 예를 들어, 주문 생성 시 상품 재고 감소, 결제, 주문 기록 저장 등 여러 작업이 포함되면 트랜잭션을 통해 모든 작업이 성공할 때만 최종 반영하도록 해야 합니다.
  • 데이터의 일관성을 유지해야 할 때: 여러 엔터티가 관련되어 있을 때, 하나의 엔터티가 변경되면 다른 엔터티들도 동일한 트랜잭션 내에서 변경이 이루어져야 할 수 있습니다.

여기서 여러데이터 베이스 작업이 하나의 로직에 포함될 떄와, 데이터의 일관성을 유지해야 할 때의 코드 예제를 보도록 하겠습니다.

 

1. 주문 생성 시, 상품 재고 감소, 결제 처리, 주문 기록 저장 등의 작업을 예로 들어보겠습니다. 이 경우, 트랜잭션을 사용하여 하나라도 실패하면 모든 작업이 롤백되도록 설정할 수 있습니다.

@Service
@Required~
public class OrderService {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final PaymentService paymentService;

    @Transactional
    public OrderResponse createOrder(OrderRequest orderRequest) {
        // 1. 주문 생성
        Order order = new Order(orderRequest);
        Order savedOrder = orderRepository.save(order);

        // 2. 상품 재고 감소
        Product product = productRepository.findById(orderRequest.getProductId())
                            .orElseThrow(() -> new ProductNotFoundException("Product not found"));
        product.decreaseStock(orderRequest.getQuantity());
        productRepository.save(product);

        // 3. 결제 처리
        PaymentResponse paymentResponse = paymentService.processPayment(orderRequest.getPaymentDetails());
        if (!paymentResponse.isSuccessful()) {
            throw new PaymentFailedException("Payment failed");
        }

        // 4. 주문 저장 완료 후 응답 생성
        return new OrderResponse(savedOrder, paymentResponse);
    }
}

 

이 에시에서 트랜잭션 환경이 `@Transactional` 에서 명시되어 있어서, 만약 결제에 실패하거나 재고 감소 중 문제가 발생하면 모든 작업이 롤백됩니다.

 

2. 여러 엔티티가 관련된 경우

여러 엔터티가 서로 관련되어 있을 때, 하나의 엔터티가 변경되면 관련된 다른 엔터티도 동일 트랜잭션 내에서 변경이 이루어져야 할 수 있습니다. 예를 들어, 회원이 탈퇴할 때 회원의 모든 관련 댓글, 주문 기록도 함께 삭제해야 한다면, 이를 트랜잭션으로 묶어서 보장할 수 있습니다.

@Service
@Required~
public class UserService {

    private final UserRepository userRepository;
    private final CommentRepository commentRepository;
    private final OrderRepository orderRepository;

    @Transactional
    public void deleteUser(Long userId) {
        // 1. 유저 확인
        User user = userRepository.findById(userId)
                      .orElseThrow(() -> new UserNotFoundException("User not found"));

        // 2. 관련된 댓글 삭제
        commentRepository.deleteByUser(user);

        // 3. 관련된 주문 기록 삭제
        orderRepository.deleteByUser(user);

        // 4. 유저 삭제
        userRepository.delete(user);
    }
}

 

이 예제에서는 deleteUser 메소드가 호출될 때, 트랜잭션이 시작됩니다. 유저와 연관된 댓글과 주문 기록이 모두 삭제된 후 유저가 삭제됩니다. 만약 중간에 삭제 작업이 실패하면, 전체 작업이 롤백되어 데이터의 일관성이 유지됩니다.

 

이처럼 여러 엔터티가 서로 연관되어 있을 때, 트랜잭션을 사용하면 데이터의 무결성과 일관성을 보장할 수 있습니다.

 

그렇다 하지만, Update와 Delete 작업으로 데이터 베이스 변경이 일어나는 상황이라면 반드시 `@Transactional` 어노테이션을 명시하여 주어야 할까요?

    @Transactional
    public CommentSaveResponse saveComment(
    		AuthUser authUser, 
    		long todoId, 
            CommentSaveRequest commentSaveRequest
    ) 
    {
        User user = User.fromAuthUser(authUser);
        Todo todo = existCheck.isExistTodo(todoId);

        Comment newComment = new Comment(commentSaveRequest, user, todo);

        Comment savedComment = commentRepository.save(newComment);

        return new CommentSaveResponse(savedComment, new UserResponse(user));
    }

 

이 코드에서는 `@Transactional` 필요하지 않다고 볼 수 있습니다. 그 이유는 데이터의 무결성과 일관성을 보장하기 위해 트랜잭션이 꼭 필요한 상황이 아니기 때문입니다.

 

  • 단일 Insert 작업: saveComment 메소드에서 commentRepository.save(newComment) 호출로 단일 Insert 작업만 수행하고 있습니다. Spring의 JPA 구현체인 Hibernate는 기본적으로 단일 Insert에 대해 자동으로 트랜잭션을 처리합니다. 트랜잭션이 없는 상태에서도 이 작업은 안전하게 수행됩니다.
  • 예외 처리: saveComment 메소드 내에서 예외가 발생해도, 데이터베이스에 이미 커밋된 데이터는 없고, 기본적으로 save 메소드 자체에서 트랜잭션을 다루기 때문에 추가적인 롤백 관리가 필요하지 않습니다.

 

그리고 `Create`, `Read` 작업에서는 상대적으로 트랜잭션이 적게 필요하지만, 특정 상황에서는 트랜잭션이 필요할 수 있습니다.

Create (Insert) 작업에서 트랜잭션이 필요한 경우

다음과 같은 경우에는 `Insert` 작업이라도 트랜잭션이 필요합니다:

  • 여러 테이블에 데이터를 동시에 Insert하는 경우: 예를 들어, 댓글 저장 시 관련 알림 테이블이나 로그 테이블에 동시에 레코드를 추가하는 경우입니다. 이러한 작업은 원자성을 보장해야 하므로 트랜잭션이 필요합니다.
  • 비즈니스 로직이 복잡하고 여러 단계가 있는 경우: 새로운 엔티티를 저장하기 전에 다른 엔티티를 조회하고, 상태를 업데이트하는 등 복잡한 비즈니스 로직이 포함될 때 트랜잭션을 사용하는 것이 좋습니다. 이를 통해 예외 발생 시 모든 작업을 취소하고 롤백할 수 있습니다.
@Transactional
public void createOrder(OrderRequest orderRequest) {
    User user = userRepository.findById(orderRequest.getUserId())
                              .orElseThrow(UserNotFoundException::new);
    Product product = productRepository.findById(orderRequest.getProductId())
                                       .orElseThrow(ProductNotFoundException::new);
    
    Order newOrder = new Order(orderRequest, user, product);
    orderRepository.save(newOrder);

    product.decreaseStock(orderRequest.getQuantity());
    productRepository.save(product);
}

 

 

이 예제에서는 주문을 생성하고 동시에 제품의 재고를 줄이기 때문에, 작업 중간에 오류가 발생하면 전체 작업이 롤백되어 데이터 일관성이 유지됩니다.

 

Read 작업에서 트랜잭션이 필요한 경우

단순 조회는 트랜잭션이 필요하지 않지만, 특정 조건에서는 트랜잭션을 사용하는 것이 좋습니다:

  • 동시에 여러 트랜잭션이 같은 데이터를 수정할 가능성이 있는 경우: 읽기 작업 중 데이터가 수정되면 문제가 발생할 수 있습니다. 이 경우에는 트랜잭션을 사용하여 데이터를 읽는 동안 잠금을 걸어 데이터가 변경되지 않도록 보장할 수 있습니다.
  • 연속된 조회와 수정 작업이 동시에 일어나는 경우: 조회 후 바로 데이터 수정이 이루어지는 경우, 트랜잭션으로 묶어야 조회한 데이터가 수정되기 전에 유지되도록 보장할 수 있습니다.
@Transactional(readOnly = true)
public Product getProductDetails(Long productId) {
    Product product = productRepository.findById(productId)
                        .orElseThrow(ProductNotFoundException::new);
    
    // 추가 연산이나 조회가 이어질 때 트랜잭션을 유지하여 일관성 보장
    return product;
}

 

위 예시에서 단순히 읽기 작업에 @Transactional(readOnly = true)를 사용하는 것은 성능을 높이기 위해 설정할 수 있습니다. readOnly = true는 트랜잭션에서 쓰기 잠금을 사용하지 않고, 단순 조회만 수행하여 자원을 절약하게 됩니다.

 

 

4. 트랜잭션의 전파 옵션 (Propagation)

트랜잭션의 전파는 현재 트랜잭션이 진행 중일 때 새로운 트랜잭션을 어떻게 처리할지 결정하는 설정입니다. @Transactional에는 다양한 전파 옵션이 있으며, 상황에 따라 적절하게 선택해야 합니다.

  • REQUIRED: 기본값으로, 트랜잭션이 있으면 기존 트랜잭션을 사용하고 없으면 새 트랜잭션을 생성합니다.
  • REQUIRES_NEW: 항상 새로운 트랜잭션을 시작하며, 기존 트랜잭션은 잠시 중단됩니다.
  • NESTED: 부모 트랜잭션 내에서 중첩된 트랜잭션을 실행합니다. 부모가 롤백되면 중첩 트랜잭션도 롤백됩니다.

1. REQUIRED(기본값)

@Service
public class OrderService {

    @Transactional(propagation = Propagation.REQUIRED)
    public void processOrder() {
        // 트랜잭션 시작
        orderRepository.save(new Order());
        paymentService.processPayment(); // 별도의 트랜잭션 없이 같은 트랜잭션을 사용
    }
}

 

2. REQUIRES_NEW

@Service
public class PaymentService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processPayment() {
        // 항상 새로운 트랜잭션 시작
        paymentRepository.save(new Payment());
    }
}

 

위에서(1번예시) `processOrder` 메소드가 호출되어 트랜잭션이 진행 중인 경우라도, `processPayment`는 항상 새로운 트랜잭션에서 실행됩니다. 따라서 `processOrder`가 실패해도 `processPayment`는 롤백되지 않습니다.

 

3. NESTED

@Service
public class UserService {

    @Transactional(propagation = Propagation.NESTED)
    public void saveUserAndAudit(User user) {
        userRepository.save(user);  // 부모 트랜잭션
        auditService.logUserCreation(user);  // 중첩 트랜잭션
    }
}

 

`saveUserAndAudit`에서 부모 트랜잭션이 롤백되면, `logUserCreation` 메소드의 중첩 트랜잭션도 함께 롤백됩니다.

 

 

5. 트랜잭션의 격리 수준 (Isolation Level)

격리 수준은 동시에 실행되는 트랜잭션 간의 간섭 정도를 제어합니다. Spring의 @Transactional은 기본적으로 데이터베이스의 기본 격리 수준을 따르지만, 필요에 따라 설정할 수 있습니다.

  • READ_UNCOMMITTED: 다른 트랜잭션에서 아직 커밋되지 않은 변경 사항을 읽을 수 있습니다.
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public List<Order> getAllOrders() {
    return orderRepository.findAll(); // 커밋되지 않은 변경 사항도 읽을 수 있음
}
  • READ_COMMITTED: 다른 트랜잭션에서 커밋된 데이터만 읽을 수 있습니다.
@Transactional(isolation = Isolation.READ_COMMITTED)
public Order getOrder(Long orderId) {
    return orderRepository.findById(orderId).orElseThrow(OrderNotFoundException::new);
}
  • REPEATABLE_READ: 트랜잭션 동안 동일한 데이터를 여러 번 읽어도 같은 값을 보장합니다.
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void processOrder(Long orderId) {
    Order order = orderRepository.findById(orderId).orElseThrow(OrderNotFoundException::new);
    // 트랜잭션 내에서는 order의 상태가 계속 동일하게 유지됨
    // 다른 트랜잭션이 변경하더라도 동일한 값 보장
}
  • SERIALIZABLE: 트랜잭션을 직렬적으로 수행하여 동시성 문제를 완전히 방지합니다.
@Transactional(isolation = Isolation.SERIALIZABLE)
public void processOrderWithSerializable(Long orderId) {
    // 직렬화된 트랜잭션으로 다른 트랜잭션과의 동시 접근을 차단
    Order order = orderRepository.findById(orderId).orElseThrow(OrderNotFoundException::new);
    order.process();
    orderRepository.save(order);
}

 

6. 트랜잭션 롤백 조건 설정

기본적으로 @Transactional은 런타임 예외가 발생하면 롤백되도록 설정되어 있습니다. 하지만 특정 예외에 대해서만 롤백하거나, 롤백을 방지할 수도 있습니다.

  • rollbackFor: 특정 예외가 발생했을 때 롤백하도록 설정할 수 있습니다.
  • noRollbackFor: 특정 예외에 대해서는 롤백하지 않도록 설정할 수 있습니다.

 

이렇게 @Transactional 어노테이션의 다양한 사용법과 사용되어야 하는 이유, 예제 코드등 다양하게 알아보았습니다.

 

감사합니다.

728x90
반응형

'Spring' 카테고리의 다른 글

Spring - Database Driver  (1) 2024.11.09
Spring - H2 Database  (0) 2024.11.08
Spring - REST API 와 RESTful  (0) 2024.10.18
Spring - JPA, Entity, 영속성 컨텍스트  (4) 2024.10.10
Spring - IoC & DI  (1) 2024.10.01