MySQL & MongoDB 트랜잭션 - Workaway

chaean·2025년 4월 30일

Workaway

목록 보기
2/11

왜 서비스 DB는 MySQL, 로그 DB는 MongoDB를 선택했는가?

TRIO 프로젝트를 진행하며, 서비스에서 발생하는 핵심 데이터(예약, 결제, 유저 정보 등)
그 외의 비즈니스 로그(에러 로그, 요청 추적, 사용자 활동 등)다르게 다뤄야 한다는 필요성을 느꼈습니다.

이에 따라 다음과 같이 설계를 나누었습니다:

  • 서비스 DBMySQL
  • 로그 DBMongoDB

✅ MySQL을 서비스 DB로 선택한 이유

항목설명
정합성 보장 (ACID)예약, 결제, 회원가입 등 정확성이 중요한 서비스에 적합
관계형 데이터 처리외래키, JOIN, 정규화가 필요한 복잡한 관계 모델링에 최적
JPA 연동 용이Entity 기반 ORM 관리가 편리함 (Spring JPA, Hibernate 등)
SQL 기반표준 쿼리 사용으로 가독성 및 유지보수 용이
검증된 기술대부분의 서비스 백엔드에서 안정적으로 사용 중

✅ MongoDB를 로그 DB로 선택한 이유

항목설명
비정형 데이터 유연성로그마다 구조가 달라도 유연하게 저장 가능
쓰기 성능 우수초당 수많은 로그 처리에 유리함 (Write-heavy에 최적)
수평 확장 쉬움노드 추가로 쉽게 성능 확장 가능
TTL 지원오래된 로그 자동 삭제 가능 (Time-To-Live 설정)

트랜잭션 보장은 어떻게?

예를 들어 주문 생성 시 다음과 같은 흐름이 있다고 가정해봅시다.

1. 주문 정보 → MySQL에 저장
2. 주문 로그 → MongoDB에 저장

만약 2번(로그 저장)에서 오류가 발생하면?
→ 1번은 성공했지만, 로그는 유실되는 문제가 생깁니다.
즉, MySQL과 MongoDB 사이의 트랜잭션 일관성이 깨질 수 있습니다.

이를 “분산 트랜잭션 문제”라고 합니다.

해결방법

1. 2PC (2-Phase-Commit)

모든 시스템(MySQL, MongoDB 등)을 동기적으로 묶어 커밋/롤백을 통제하는 방식입니다

정합성은 확실히 보장되지만, 동기적으로 처리되기 때문에 느리고 무겁다는 단점이 존재합니다.

MongoDB가 해당 방법을 지원하지않기 때문에 고려하지않았습니다.

2. 비동기 이벤트 기반 처리 + SAGA

핵심 로직(MySQL 저장)이 끝난 후, 관련된 부가 로직(로그 저장, 알림 등)을 이벤트로 발행하여 비동기적으로 처리하는 방법입니다.

또한 실패 시 SAGA패턴을 적용하여 이전 단계의 작업을 보상 트랜잭션으로 되돌리는 방식입니다.

즉, 핵심 로직(MySQL)에 트랜잭션 처리 후 → 이벤트 발행 → MongoDB 로그 저장 → 실패 시 보상 처리

위와 같은 흐름입니다.

예시코드

@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)는 비동기 이벤트 리스너를 통해 분리 저장함으로써,

서비스 로직과 부가 로직을 효과적으로 분리할 수 있었고, 운영 안정성과 확장성도 확보할 수 있었습니다.

현재 프로젝트의 규모와 복잡도 기준에서는 이 방식이 가장 현실적인 선택이라고 판단했습니다.

profile
백엔드 개발자

0개의 댓글