
TRIO 프로젝트를 진행하며, 서비스에서 발생하는 핵심 데이터(예약, 결제, 유저 정보 등)와
그 외의 비즈니스 로그(에러 로그, 요청 추적, 사용자 활동 등)를 다르게 다뤄야 한다는 필요성을 느꼈습니다.
이에 따라 다음과 같이 설계를 나누었습니다:
MySQLMongoDB| 항목 | 설명 |
|---|---|
| 정합성 보장 (ACID) | 예약, 결제, 회원가입 등 정확성이 중요한 서비스에 적합 |
| 관계형 데이터 처리 | 외래키, JOIN, 정규화가 필요한 복잡한 관계 모델링에 최적 |
| JPA 연동 용이 | Entity 기반 ORM 관리가 편리함 (Spring JPA, Hibernate 등) |
| SQL 기반 | 표준 쿼리 사용으로 가독성 및 유지보수 용이 |
| 검증된 기술 | 대부분의 서비스 백엔드에서 안정적으로 사용 중 |
| 항목 | 설명 |
|---|---|
| 비정형 데이터 유연성 | 로그마다 구조가 달라도 유연하게 저장 가능 |
| 쓰기 성능 우수 | 초당 수많은 로그 처리에 유리함 (Write-heavy에 최적) |
| 수평 확장 쉬움 | 노드 추가로 쉽게 성능 확장 가능 |
| TTL 지원 | 오래된 로그 자동 삭제 가능 (Time-To-Live 설정) |
예를 들어 주문 생성 시 다음과 같은 흐름이 있다고 가정해봅시다.
1. 주문 정보 → MySQL에 저장
2. 주문 로그 → MongoDB에 저장
만약 2번(로그 저장)에서 오류가 발생하면?
→ 1번은 성공했지만, 로그는 유실되는 문제가 생깁니다.
즉, MySQL과 MongoDB 사이의 트랜잭션 일관성이 깨질 수 있습니다.
이를 “분산 트랜잭션 문제”라고 합니다.
모든 시스템(MySQL, MongoDB 등)을 동기적으로 묶어 커밋/롤백을 통제하는 방식입니다
정합성은 확실히 보장되지만, 동기적으로 처리되기 때문에 느리고 무겁다는 단점이 존재합니다.
MongoDB가 해당 방법을 지원하지않기 때문에 고려하지않았습니다.
핵심 로직(MySQL 저장)이 끝난 후, 관련된 부가 로직(로그 저장, 알림 등)을 이벤트로 발행하여 비동기적으로 처리하는 방법입니다.
또한 실패 시 SAGA패턴을 적용하여 이전 단계의 작업을 보상 트랜잭션으로 되돌리는 방식입니다.
위와 같은 흐름입니다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public void createOrder(String productName) {
Order order = new Order();
order.setProductName(productName);
orderRepository.save(order);
// 트랜잭션 커밋 이후 이벤트 발생
eventPublisher.publishEvent(new OrderCreatedEvent(order.getId()));
}
@Transactional
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
order.setCanceled(true);
}
}
@Component
@RequiredArgsConstructor
public class OrderEventHandler {
private final MongoTemplate mongoTemplate;
private final OrderService orderService;
@Async
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
try {
mongoTemplate.save(new OrderLog(event.getOrderId(), "주문이 생성되었습니다."));
} catch (Exception e) {
// 로그 저장 실패 시 보상 트랜잭션 수행 (주문 취소)
orderService.cancelOrder(event.getOrderId());
System.err.println("Mongo 저장 실패 → 주문 취소 수행");
}
}
}
추가적으로 아래 코드와 같이 @Retryable을 통해 재시도 로직을 추가할 수도 있습니다.
@Retryable(
value = MongoWriteException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 2000)
)
@Async
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
mongoTemplate.save(new OrderLog(event.getOrderId(), "주문 생성됨"));
}
MySQL과 MongoDB처럼 서로 다른 DB를 사용할 때, 하나의 트랜잭션으로 묶어 정합성을 보장하는 것은 기술적으로 쉽지 않습니다.
특히 MongoDB는 2PC(XA 트랜잭션)를 지원하지 않기 때문에, 전통적인 분산 트랜잭션 방식은 적용할 수 없습니다.
이에 따라 Kafka나 Redis 같은 메시지 브로커 없이도, Spring의 비동기 이벤트 기반 처리만으로 분산 트랜잭션 문제를 완화하는 구조를 선택했습니다.
현재는 이벤트 처리 실패 시를 대비해 @Retryable 기반의 재시도 로직만 우선 도입하여 구현할 예정이며,
SAGA 패턴을 통한 보상 트랜잭션은 추후에 확장 가능하도록 구조를 열어두었습니다.
핵심 데이터(MySQL)는 @Transactional로 안정적으로 처리하고, 로그 데이터(MongoDB)는 비동기 이벤트 리스너를 통해 분리 저장함으로써,
서비스 로직과 부가 로직을 효과적으로 분리할 수 있었고, 운영 안정성과 확장성도 확보할 수 있었습니다.
현재 프로젝트의 규모와 복잡도 기준에서는 이 방식이 가장 현실적인 선택이라고 판단했습니다.