배경
드롭쉬핑 기반의 이커머스 자동화 플랫폼을 새로 설계할 기회가 생겼다. 도매몰에서 상품을 수집하고, 여러 오픈마켓에 자동 등록하며, 주문 발생 시 도매몰에 자동 발주하는 전 과정을 자동화하는 시스템이다.
초기에는 모놀리식으로 빠르게 만들까 고민했지만, 이 플랫폼의 특성상 외부 API 의존도가 매우 높고 각 기능의 장애 특성이 달랐다. 마켓 API 장애가 주문 처리를 멈추거나, AI 분석 실패가 상품 등록을 막는 상황은 피해야 했다.
8개 마이크로서비스 분해
비즈니스 도메인을 기준으로 8개 서비스를 도출했다.
| # | 서비스 | 역할 | 핵심 기능 |
|---|---|---|---|
| 1 | 상품 수집기 | 도매몰 연동 | 도매몰 API에서 상품 수집, 표준 구조로 변환 |
| 2 | 상품 분석기 | AI 분석 | 동일상품 그룹화, 콘텐츠 자동 생성 |
| 3 | 마켓 등록기 | 마켓 연동 | 마켓별 API로 상품 등록/수정/삭제 |
| 4 | 주문 처리기 | 주문 자동화 | 마켓 주문 수신 후 최저가 도매몰에 자동 발주 |
| 5 | 배송 관리기 | 물류 추적 | 송장 연동, 배송 상태 추적, 반송 처리 |
| 6 | 정산 관리기 | 수익 계산 | 실시간 수익 계산, 마켓 정산 검증 |
| 7 | 클레임 처리기 | CS 자동화 | 취소/반품 모니터링 및 자동 처리 |
| 8 | 알림 봇 | 알림 발송 | 취소 안내, 반품 안내 메시지 전송 |
서비스 분해의 기준은 단순했다. 장애가 발생했을 때 독립적으로 격리할 수 있는가이다. 상품 수집이 멈춰도 이미 등록된 상품의 주문은 정상 처리되어야 하고, AI 분석이 실패해도 수동 등록은 가능해야 한다.
Go + Python 혼합 전략
모든 서비스를 하나의 언어로 통일하지 않았다. 서비스의 특성에 따라 Go와 Python을 나눠 적용했다.
Go를 선택한 서비스 (6개)
상품 수집기, 마켓 등록기, 주문 처리기, 배송 관리기, 정산 관리기, 클레임 처리기에 Go를 적용했다.
이 서비스들의 공통점은 다음과 같다.
- 높은 동시성 요구: 여러 마켓 API를 동시에 호출해야 한다
- 메모리 효율이 중요: 수천 개의 상품 데이터를 동시에 처리한다
- 안정적인 장기 운영: 24시간 돌아가는 데몬 프로세스다
Go의 goroutine은 수천 개의 동시 API 호출을 적은 메모리로 처리할 수 있었고, 컴파일된 바이너리는 컨테이너 이미지 크기도 작게 유지할 수 있었다.
Python을 선택한 서비스 (2개)
상품 분석기와 알림 봇에는 Python을 적용했다.
- 상품 분석기: AI API 연동이 핵심이고, 프롬프트 엔지니어링과 데이터 전처리에 Python 생태계가 압도적으로 유리하다
- 알림 봇: 메시지 템플릿 처리, 자연어 생성 등 AI 기능이 포함된다
언어 혼합의 핵심 원칙은 서비스 간 통신은 언어에 무관하게 동일한 프로토콜을 사용하는 것이었다. REST API와 Redis Streams 기반 메시지 큐를 표준 통신 채널로 정했다.
Redis Streams를 메시지 큐로 선택한 이유
서비스 간 비동기 통신에 Kafka나 RabbitMQ 대신 Redis Streams를 선택했다.
선택 이유:
- 이미 Redis를 캐시로 사용 중: 별도 MQ 인프라 추가 없이 기존 Redis에 통합
- Consumer Group 지원: Kafka와 유사한 컨슈머 그룹 기능으로 메시지 분배와 ACK 처리 가능
- 운영 복잡도 감소: 소규모 팀에서 Kafka 클러스터를 운영하는 것은 오버엔지니어링
- 충분한 처리량: 초당 수천 건 수준의 메시지 처리에는 Redis Streams로 충분
물론 트레이드오프가 있다. 메시지 영속성이나 대규모 스트림 처리에서는 Kafka가 유리하다. 하지만 이 플랫폼의 규모에서는 Redis Streams가 적절한 선택이었다.
데이터 파이프라인 설계
이 플랫폼의 핵심 설계 원칙은 중간 결과 보존이다. 상품이 수집되고 분석되어 마켓에 등록되기까지, 각 단계의 결과가 DB에 저장된다.
[수집] → DB 저장 → [분석] → DB 저장 → [등록] → DB 저장 → [주문처리]
이렇게 설계한 이유는 명확하다.
- 부분 실패 허용: AI 분석이 실패해도 수집된 원본 데이터는 보존된다
- 재처리 가능: 어떤 단계에서 실패하든, 해당 단계부터 다시 시작할 수 있다
- 디버깅 용이: 각 단계의 입력/출력이 DB에 남아 있어 문제 추적이 쉽다
작업 상태 머신
모든 작업은 상태 머신으로 관리된다.
PENDING → PROCESSING → COMPLETED
↘ FAILED → PENDING (재처리)
COMPLETED → ROLLBACK → PENDING (재처리)
각 작업에는 상태와 함께 시도 횟수, 마지막 오류 메시지, 처리 시작/완료 시각이 기록된다. 이를 통해 실패한 작업의 패턴을 파악하고 자동 재처리 전략을 세울 수 있었다.
인프라 구성
소규모 프로젝트였기 때문에 Kubernetes 대신 Docker Compose를 선택했다.
| 컴포넌트 | 기술 | 역할 |
|---|---|---|
| API Gateway | Nginx | SSL 종료, 리버스 프록시, 라우팅 |
| Database | MariaDB | 데이터 영속 저장 |
| Cache + MQ | Redis | 캐싱, Redis Streams MQ |
| Container | Docker Compose | 서비스 오케스트레이션 |
인증 구조
- 프론트엔드 - 백엔드: JWT 토큰 기반 인증
- 서비스 - 서비스: mTLS + 서비스 토큰으로 상호 인증
서비스 간 통신에 mTLS를 적용한 이유는, 내부 네트워크라도 서비스 간 신뢰를 보장해야 하기 때문이다. 각 서비스가 독립적으로 배포되므로, 잘못된 서비스가 다른 서비스의 API를 호출하는 것을 방지한다.
데이터베이스 전략
초기에는 단일 MariaDB 인스턴스에 공유 DB를 사용한다. 마이크로서비스 순수주의자들은 서비스별 DB 분리를 주장하지만, 현실적으로 소규모 팀에서 여러 DB를 운영하는 것은 부담이 크다.
대신 서비스별 스키마를 논리적으로 분리하고, 향후 필요 시 물리적으로 분리할 수 있는 구조로 설계했다.
MariaDB Instance
├── schema: product (상품 수집기, 상품 분석기)
├── schema: market (마켓 등록기)
├── schema: order (주문 처리기, 배송 관리기)
├── schema: settlement (정산 관리기)
└── schema: common (공통 코드, 설정)
설계하면서 배운 점
마이크로서비스는 만능이 아니다. 분산 시스템의 복잡성, 서비스 간 데이터 정합성, 네트워크 장애 처리 등 모놀리식에서는 신경 쓰지 않아도 되는 문제들이 생긴다.
하지만 이 플랫폼처럼 외부 API 의존도가 높고 각 도메인의 장애 특성이 뚜렷한 경우, 마이크로서비스의 장애 격리 이점은 운영 복잡성을 상쇄하고도 남았다.
다음 글에서는 이 아키텍처에서 적용한 장애 격리 패턴들(Circuit Breaker, Bulkhead, Dead Letter Queue 등)을 구체적으로 다룬다.
참고 자료
- Redis Streams for Microservices — Redis 공식 마이크로서비스 간 통신 튜토리얼