
MongoDB의 트랜잭션은 데이터의 일관성을 보장하고 복잡한 비즈니스 로직을 처리할 수 있는 중요한 기능입니다.
하지만 트랜잭션을 도입하는 데 있어 몇 가지 난관이 존재할 수 있습니다.
특히, 트랜잭션과 이벤트 발행 사이의 타이밍 문제, Write Conflict, 그리고 적절한 트랜잭션 설계가 그 중 주요한 부분입니다.
이러한 문제를 해결하는 방법을 코드와 함께 살펴보겠습니다.
MongoDB는 Replica Set과 Sharded Cluster 환경에서만 트랜잭션을 지원하며, WiredTiger 스토리지 엔진을 필요로 합니다.
트랜잭션의 주요 특징은 Read Committed 격리 수준과 Snapshot Isolation을 통해 데이터의 일관성을 보장합니다.
그러나 트랜잭션이 진행되는 동안 Write Conflict가 발생할 수 있기 때문에 이를 피하기 위한 설계가 필요합니다.
MongoDB에서는 트랜잭션을 시작하고, 트랜잭션을 커밋하는 방식으로 데이터베이스의 일관성을 유지합니다.
Spring에서는 @Transactional을 활용해 트랜잭션을 관리할 수 있습니다.
@Transactional
public void executeTransaction() {
// 트랜잭션을 시작하고, 여러 작업을 수행합니다.
orderRepository.save(order);
userRepository.updateBalance(user);
// 트랜잭션이 종료되면서 자동으로 커밋됩니다.
}
트랜잭션 커밋 전에 이벤트를 발행하면 데이터 불일치가 발생할 수 있습니다.
예를 들어, 결제 트랜잭션이 성공적으로 끝나지 않았는데 이벤트가 먼저 발행되면, 결제 실패 후에도 이벤트가 발송될 수 있습니다.
이를 해결하기 위해, 트랜잭션이 커밋된 후 이벤트를 발행하는 방법을 사용합니다.
Spring에서는 @TransactionalEventListener를 활용하여 트랜잭션이 커밋된 후 이벤트를 발행할 수 있습니다.
이 방식은 트랜잭션 커밋 후 이벤트가 발행되어 데이터 정합성을 유지하는 데 도움이 됩니다.
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Transactional
public void placeOrder(Order order) {
orderRepository.save(order);
// 트랜잭션이 커밋된 후에 이벤트를 발행
eventPublisher.publishEvent(new OrderPlacedEvent(order));
}
}
@Component
public class OrderPlacedEventListener {
@TransactionalEventListener
public void handleOrderPlacedEvent(OrderPlacedEvent event) {
// 트랜잭션이 커밋된 후에 이벤트 처리
sendConfirmationEmail(event.getOrder());
}
}
트랜잭션 도중 데이터 변경이 일어날 경우, Write Conflict가 발생할 수 있습니다.
이를 해결하기 위해서는 트랜잭션을 명확하게 정의하고, 필요한 경우 retry 로직을 구현해야 합니다.
Spring에서 MongoDB를 사용할 때 Write Conflict가 발생하면 MongoException을 발생시킬 수 있습니다.
이 때 트랜잭션을 다시 시도하는 retry 로직을 구현할 수 있습니다.
@Transactional
public void transferFunds(String fromAccount, String toAccount, BigDecimal amount) {
int attempts = 0;
while (attempts < 3) {
try {
// 송금 작업
accountRepository.decreaseBalance(fromAccount, amount);
accountRepository.increaseBalance(toAccount, amount);
break; // 성공적으로 커밋되면 루프 종료
} catch (MongoException e) {
if (e instanceof WriteConflictException) {
// Write Conflict 발생 시 재시도
attempts++;
if (attempts >= 3) {
throw new RuntimeException("트랜잭션 실패: 재시도 횟수 초과");
}
continue;
} else {
throw e; // 다른 예외 발생 시 처리
}
}
}
}
마이크로서비스 아키텍처에서 여러 서비스가 데이터를 처리하는 경우, MongoDB 트랜잭션만으로는 데이터의 일관성을 보장하기 어려울 수 있습니다.
이때는 SAGA 패턴을 사용하여 분산 트랜잭션을 관리할 수 있습니다.
SAGA 패턴은 여러 마이크로서비스가 트랜잭션을 분산적으로 관리할 수 있게 도와줍니다.
트랜잭션이 실패하면 롤백 절차를 수행하여 일관성을 유지합니다.
@Transactional
public void startSaga(Order order) {
try {
orderService.placeOrder(order);
inventoryService.reserveInventory(order);
paymentService.processPayment(order);
} catch (Exception e) {
// 트랜잭션 실패 시 롤백 처리
inventoryService.cancelReservation(order);
paymentService.refundPayment(order);
throw new RuntimeException("SAGA 실패: 롤백 처리");
}
}
MongoDB에서 트랜잭션을 사용할 때 성능 저하가 발생할 수 있습니다.
따라서 트랜잭션의 범위를 최소화하고, 필요한 경우 비동기 처리를 고려하는 것이 좋습니다.
트랜잭션 내에서 데이터 처리 후 이벤트 발행 등의 작업을 비동기로 처리하여 성능을 최적화할 수 있습니다.
@Transactional
public void processOrder(Order order) {
orderRepository.save(order);
// 비동기로 이벤트 발행
CompletableFuture.runAsync(() -> eventPublisher.publishEvent(new OrderProcessedEvent(order)));
}
MongoDB 트랜잭션을 Spring Boot에 도입하는 것은 복잡할 수 있지만, 위와 같은 해결책을 통해 안정적이고 일관된 시스템을 구축할 수 있습니다.
트랜잭션과 이벤트 발행의 타이밍 문제를 해결하고, Write Conflict 문제를 예방하는 등의 방법으로 시스템의 안정성과 성능을 보장할 수 있습니다.