“스케줄이 또 안 돌았어요” — 우리가 Temporal을 선택한 이유
지긋지긋한 스케줄 장애, ‘실패해도 스스로 살아나는 시스템(Temporal)’ 도입기를 공유합니다.
“스케줄이 또 안 돌았어요” — 우리가 Temporal을 선택한 이유
어느 날 아침, 슬랙 알림이 울렸습니다. “출고지시 스케줄이 실행 안 된 것 같은데 확인 부탁드려요.”
Jenkins 콘솔을 열어보니 job이 멈춰있었고, 모니터링 job마저 함께 멈춰있었습니다. 급하게 수동으로 출고지시를 트리거하고 나서야 물류센터 작업이 시작될 수 있었습니다. 이런 일이 반복되면서 우리는 고민하게 되었습니다.
“스케줄 하나 도는 게 왜 이렇게 불안할까?”
출고지시, 그리고 우리가 마주한 문제들
무신사 풀필먼트의 OMS는 주문부터 출고까지 물류 전반을 책임지는 시스템입니다. 그 중에서도 출고지시는 물류센터의 하루를 시작하는 신호와도 같습니다. 정해진 시간에 출고지시가 생성되지 않으면 물류센터 작업이 지연되고, 그것은 곧 배송 지연으로 이어집니다.
초기에는 Jenkins crontab으로 충분했습니다. 정해진 시간에 실행하기만 하면 됐습니다. 하지만 화주사가 늘고 출고 물량이 커지면서, 이 구조의 한계가 하나둘 보이기 시작했습니다.
1. 실패를 놓치는 순간들
Jenkins 기반 스케줄은 job이 멈춰도 알려주지 않았습니다. 그래서 우리는 별도의 모니터링 job을 만들어 실행했습니다. “스케줄이 실행됐는지 확인하는 스케줄”을 또 만드는 거죠.
문제는 이 모니터링 job도 언제든 멈출 수 있다는 것이었습니다. 실제로 Jenkins 자체에 문제가 생기면 스케줄과 모니터링이 함께 멈췄고, 우리는 뒤늦게야 알게 되곤 했습니다. 모니터링을 위한 모니터링을 또 만들 수는 없었고, 이 구조 자체가 근본적인 한계를 가지고 있다는 걸 깨달았습니다.
2. 로그 속에서 원인 찾기
출고지시에 문제가 생기면, 저희는 Jenkins 콘솔 로그를 시작으로 애플리케이션 로그, DB 이력을 차례로 확인해야 했습니다.
“이번엔 어디서 실패한 거지?” “입력값은 뭐였지?” “결과는 어떻게 됐지?”
실행 이력을 한눈에 볼 수 있는 방법이 없었고, 문제를 분석하는 것보다 로그를 따라가는 데 더 많은 시간이 들었습니다. 빠른 대응이 필요한 순간일수록, 이런 가시성 부족은 운영 리스크를 키우는 요인이 되었습니다.
3. “다시 눌러주세요”
스케줄이 실패하면 저희가 직접 재실행해야 했습니다. 단순히 일시적인 네트워크 오류로 실패한 경우에도, 원인을 확인하고 수동으로 재실행하는 과정에서 시간이 소요되었습니다.
대응이 조금만 늦어도 출고 SLA에 영향을 주는 경우가 생겼고, 담당자의 부담은 자연스럽게 커질 수밖에 없었습니다. 실제로는 단순 재시도만으로 해결될 수 있는 케이스도 많았지만, 원인을 확인하는 절차 자체가 출고 지연으로 이어지는 경우가 반복되었습니다.
결국 이러한 구조에서는 안정적인 운영을 기대하기 어려워졌습니다.
4. 이벤트 기반으로의 확장
출고 도메인에서는 정해진 스케줄 외에도, 특정 이벤트를 기점으로 Workflow를 실행해야 하는 요구가 늘어나고 있었습니다. 하지만 Jenkins는 cron 기반 실행에 최적화되어 있었고, 이벤트 기반 트리거를 자연스럽게 처리하기에는 구조적인 한계가 있었습니다.
결국 저희는 깨달았습니다. “출고 물량과 요구사항이 늘어날수록, 기존 구조로는 안정적인 운영을 기대하기 어렵다.”
구조 자체를 다시 고민해야 할 시점이었습니다.
대안을 찾아서
저희는 여러가지 가능성을 열어두고 대안을 비교해보기 시작했습니다.
Jenkins + 모니터링 Job 개선
이미 운영 중인 구조라 리스크는 적었지만, 근본적인 한계를 해결하기는 어려웠습니다. 모니터링을 아무리 촘촘하게 만들어도, Jenkins 자체의 고가용성 문제는 해결되지 않았습니다.
Spring Batch + Quartz
Spring Batch + Quartz는 배치 처리에 최적화된 구조였고, 팀에서도 익숙한 스택이었습니다. 하지만 Batch Job 실행 이력은 확인할 수 있어도, “주문이 어디서 멈췄고, 왜 실패했는지”와 같은 비즈니스 흐름은 보이지 않았습니다. 재시도 로직 구현은 가능했지만, “이 조건이면 재시도, 저 조건이면 스킵” 같은 의사결정 로직이 코드 곳곳에 흩어지면서 전체 워크플로우를 파악하기 어려워졌습니다. 결국 “배치 Job”이 아닌 “비즈니스 워크플로우” 단위로 실행과 상태를 추적하고 싶었습니다.
Temporal
Temporal은 장기 실행되는 비즈니스 프로세스를 코드로 표현하고, 실패·재시도·상태 관리를 플랫폼 레벨에서 보장해주는 Workflow Engine입니다. Uber에서 만든 Cadence를 기반으로 개발되었으며, 서버 장애가 발생해도 중단된 지점부터 자동 복구됩니다.
생소한 이름이었습니다. 러닝 커브가 높다는 것도 부담이었습니다. 하지만 회의실에서 Temporal 문서를 보며 이야기를 나누다 보니, 저희가 원하던 것들이 하나하나 눈에 들어왔습니다.
- 스케줄 기반 실행
- Workflow 전체 흐름에 대한 가시성
- 자동 재시도와 복구
- 이벤트 기반 트리거
어느 순간 팀 모두가 적합해 보인다고 생각했고, 단순히 스케줄을 실행하는 기능을 넘어, 출고 도메인 전반의 흐름을 안정적으로 오케스트레이션할 수 있는 플랫폼이 될 수 있겠다고 판단했습니다.
설계하면서 고민한 것들
무엇을 목표로 설계하는가
기존 Jenkins crontab 기반 스케줄링 방식은 운영 안정성, 실행 가시성, 복구 가능성 측면에서 여러 한계를 갖고 있었습니다.
이에 따라 스케줄 실행 방식을 보다 안정적이고 자동화된 구조로 전환하고, 운영자가 수행하던 수동 확인과 재실행 작업을 최소화하는 것을 목표로 했습니다.
Workflow와 Activity, 어떻게 나눌 것인가
Temporal 공식 문서는 이렇게 말합니다.
Workflow는 전체 비즈니스 프로세스의 오케스트레이션을 담당하고, Activity는 외부 시스템과의 상호작용이나 단위 작업을 수행한다.
Workflow와 Activity의 경계를 정하는 데 가장 어려웠던 점은, 단순히 “외부 시스템을 호출하는가”가 아니라 “이 분기가 비즈니스 정책인가, 실행 세부사항인가”를 판단하는 일이었습니다. 예를 들어 “중복 주문이면 스킵한다”는 비즈니스 정책이므로 Workflow에서 분기하고, “DB에서 중복 여부를 조회한다”는 실행 세부사항이므로 Activity로 분리했습니다.
논의 끝에 저희가 세운 기준은 이렇습니다:
- Workflow: 비즈니스 정책에 따른 분기와 흐름 제어
- Activity: 멱등성이 보장되는 단위 작업, 외부 시스템 호출
출고지시의 경우, Workflow는 “정책 조회 → 중복 체크 → 차수 생성 → 파이프라인 트리거” 흐름을 조율하고, 각 단계의 실제 작업은 Activity가 수행합니다. 이 구분이 명확할수록 향후 출고 파이프라인 전체를 Workflow로 확장할 때도 일관성 있게 설계할 수 있을 거라 생각했습니다.

- 앞서 정의한 기준에 따라, Workflow는 출고지시 트리거의 전체 흐름과 분기·의사결정을 조율하고 Activity는 정책 조회, 중복 체크, 차수 생성 등 실제 작업을 수행합니다.
- Temporal 내 정의된 스케줄을 기반으로 Workflow가 실행되며, 각 단계는 Activity로 위임되어 상태를 명확히 관리하고 출고지시 흐름을 조율합니다.
출고지시 Workflow 구현
최종적으로 1개의 Workflow와 5개의 Activity로 구성했습니다. (아래는 간략화된 코드입니다.)
@WorkflowImplement
class CutOffTriggerWorkflowImpl : CutOffTriggerWorkflow {
override fun run() {
// 출고지시 정책 조회
val policies = getCutOffPolicies.get(now)
policies.forEach { policy ->
// 출고지시 차수 생성
val cutSequence = createCutSequence.create(policy)
// 파이프라인 트리거
triggerPipeline.trigger(cutSequence, now)
}
}
}
29CM 주문수집도 자동화하다
출고지시 트리거를 Temporal로 전환하고 나니, 또 다른 수동 작업이 눈에 들어왔습니다. 29CM 주문수집이었습니다.
담당자가 MOMS 화면에서 버튼을 눌러 수동으로 주문을 수집하고 있었는데, 물량이 늘면서 이 방식도 한계가 드러나기 시작했습니다. 버튼 클릭을 누락하는 경우도 있었고, 담당자 부재 시 주문 수집이 지연되기도 했습니다.
“출고지시와 비슷한 문제잖아?”
팀 내에서 자연스럽게 이러한 논의가 이어졌고, 29CM 주문수집도 Temporal Workflow로 자동화하기로 했습니다.
개인정보 보호를 고려한 설계
하지만 여기서 중요한 고민이 하나 있었습니다. 우리는 Temporal Cloud를 사용할 계획이므로 무신사 고객의 개인정보를 Temporal에 전달하는 것은 부적절했습니다.
회의실에서 여러 방안을 논의했고, 결국 다음과 같이 설계했습니다:
- 29CM에서 받은 API Payload를 MOMS DB에 저장
- Payload ID만 Temporal Workflow에 전달
- Activity에서 Payload ID로 DB 조회 후 처리
즉, 개인정보는 우리 인프라 안에 머물고, Temporal에는 ID만 전달되는 구조입니다.
주문수집 Workflow 시나리오

- 29CM API에서 주문을 조회해서 DB에 저장(ORDER_COLLECTED)합니다.
- 성공한 건에 대해 29CM에 상품준비중 상태 변경을 요청한 뒤, 상태를 ORDER_ACCEPTED로 변경하는 플로우입니다.
주문수집 Workflow 구현
1개의 Workflow와 4개의 Activity로 구성했습니다.
@WorkflowImplement
class AddStandardOrderWorkflowImpl : AddStandardOrderWorkflow {
override fun addOrders(payloadId: Long) {
// 1. 주문 수집
val upsertResults = addStandardOrderActivity.addOrders(payloadId)
val successOrders = upsertResults.filter { it.success }
if (successOrders.isEmpty()) return
// 2. 상품 준비중 API 요청
val shippingResults = requestPrepareShippingActivity.request(successOrders)
// 3. 주문 상태를 ORDER_ACCEPTED로 변경
if (shippingResults.items.isNotEmpty()) {
val request = convertActivity.convert(shippingResults)
changeStatusActivity.updateToAccepted(request)
}
}
}
주문 수집 → 29CM API 호출 → 상태 변경까지의 흐름이 하나의 Workflow로 표현됩니다. 출고지시 Workflow와 비슷한 구조지만, 도메인의 특성에 맞게 Activity를 구성했습니다.
운영하면서 느낀 것들
좋았던 점들
1. 장애 대응이 ‘케이스 바이 케이스’에서 ‘시스템’으로
가장 먼저 체감한 변화는 자동 재시도였습니다. Activity에 정의한 Retry/Backoff/Timeout 규칙에 따라 자동으로 재시도되는 걸 보면서, “아, 이제 Spring Batch에 복잡하게 구현하지 않아도 되겠구나”라는 생각이 들었습니다.
실제로 운영 중 일시적인 네트워크 오류로 출고지시가 실패한 적이 있었는데, Temporal UI를 보니 자동으로 3번 재시도하고 성공한 걸 확인할 수 있었습니다.

EventID 12 : 재시도 후 성공한 케이스
2. Workflow 단위로 보이는 실행 이력
이전에는 Jenkins 로그, 애플리케이션 로그를 모두 열어봐야 했지만, Temporal UI에서 타임라인을 한눈에 확인할 수 있게 되었습니다.

하단부터 시간순 타임라인
특히 Activity 단위로 input, output을 빠르게 확인할 수 있어 원인 파악이 훨씬 쉬워졌습니다. “이 시점에 어떤 정책이 들어왔고, 어떤 결과가 나왔는지”를 클릭 몇 번으로 확인할 수 있게 되니, 장애 대응 시간이 크게 줄었습니다.

특정 Activity의 Input, Output
3. 부분 도입만으로도 체감된 변화
출고지시 전체 파이프라인이 아닌 “트리거 + 특정 화주 주문수집”만 Temporal로 옮겼는데도, 해당 구간의 장애 대응과 모니터링 편의성은 즉시 체감할 수 있었습니다.
또한, 동일 도메인을 Spring Batch + Jenkins와 Temporal 두 체계에서 비교해보면서, 어떤 유형의 작업이 Temporal에 더 적합한지 기준을 잡는 데 도움이 되었습니다.
고려해봐야 할 것들
1. Workflow·Activity 경계 설정의 어려움
“무엇을 하나의 Workflow로 보고, 어느 단위를 Activity로 분리할 것인가”에 대한 기준을 잡는 게 쉽지 않았습니다. 팀 내에서도 의견이 엇갈릴 때가 많았고, 결국 “실제로 만들어보고 조정하자”는 방식으로 진행했습니다.
이번에는 일부에만 적용했지만, 향후 출고 전체 파이프라인으로 확장하려면 더 명확한 설계 원칙이 필요할 것 같습니다.
2. 디버깅 포인트의 증가
Temporal을 도입하면서 애플리케이션 로그 외에 Temporal History, Worker 메트릭까지 함께 봐야 해서 디버깅 지점이 늘어난 것은 사실입니다. 하지만 이는 단순히 복잡도가 증가했다기보다, 실행 상태를 구조적으로 관측할 수 있게 된 결과라고 느끼고 있습니다.
다만 이 장점을 살리기 위해서는, 로그·메트릭·Workflow History를 어떤 순서로 확인할지에 대한 팀 차원의 디버깅 가이드가 반드시 필요하다는 점도 함께 깨달았습니다. 현재는 이슈 대응 시 “애플리케이션 로그 → Temporal History → Worker 메트릭” 순으로 확인하는 기준을 정리 중이며, Datadog/Slack 알림 임계값도 함께 다듬어가고 있습니다.
3. Batch와 Temporal의 공존
Batch와 Temporal이 공존하는 현재 상태는 운영 부담이 분명 존재합니다. “이 배치는 Jenkins에서 보고, 저 워크플로우는 Temporal에서 본다”는 식으로 운영하다 보니 온콜 대응 가이드도 두 체계로 작성해야 했습니다.
하지만 이 공존은 모든 파이프라인을 한 번에 전환하지 않기 위한 의도적인 과도기이기도 합니다. 이 기간 동안 동일 도메인을 두 체계에서 비교해보면서, “장기 실행·재시도·이력 추적이 중요한 작업”은 Temporal에, “단순 반복 처리”는 Batch에 적합하다는 기준을 점점 명확히 하고 있습니다. 이러한 기준이 쌓이면 전환 시점에 대한 판단도 훨씬 명확해질 것으로 기대하고 있습니다.
다음 여정: 출고지시 파이프라인 전체를 Temporal로
이제 저희는 더 큰 그림을 그리고 있습니다. 출고지시 트리거와 주문수집을 Temporal로 전환하면서, 다음 단계가 분명해졌습니다.
출고지시 파이프라인 전체를 Workflow로 전환하는 것.
현재 출고지시는 대용량 처리 시 약 30분이 소요됩니다 (2만 건 기준). 이를 Workflow 기반 병렬 처리 구조로 전환하면 평균 90% 이상 단축할 수 있을 것으로 예상하고 있습니다.
또한 출고 전 구간의 상태, 처리 속도, 실패 지점을 실시간으로 가시화하여 문제의 원인과 영향 범위를 신속하게 파악할 수 있는 Observability 체계를 구축하려 합니다.
지금은 Child Workflow, Signal 기반 접근 방식 등 다양한 방법을 테스트하고 있습니다. 만만치 않은 도전이겠지만, 트리거와 주문수집을 통해 얻은 경험이 큰 자산이 되고 있습니다.
AS-IS

TO-BE


마치며
“스케줄이 또 안 돌았어요”라는 알림에서 시작된 이 여정은, 단순히 기술 스택을 바꾸는 것 이상의 의미를 가졌습니다.
저희는 “실패하지 않는 시스템”을 만드는 게 아니라, “실패해도 스스로 복구되는 시스템”을 만들고 있습니다. Temporal을 도입하면서 가장 크게 느낀 건, 기술 선택이 단순히 “어떤 라이브러리를 쓸 것인가”가 아니라 “우리가 어떻게 운영할 것인가”를 결정한다는 점이었습니다.
물론 아직 갈 길이 멉니다. Workflow 설계 기준도 계속 다듬어야 하고, 모니터링 체계도 개선해야 합니다. 출고 파이프라인 전체를 Workflow로 전환하는 것도 쉽지 않은 도전이 될 것입니다.
하지만 새벽에 출고지시 알림을 받고 급하게 수동 실행하던 날들이 조금씩 줄어들고 있습니다. 그리고 그 시간에 저희는 더 중요한 문제를 고민할 수 있게 되었습니다.
긴 글 읽어주셔서 감사합니다. 저희의 경험이 비슷한 고민을 하고 계신 분들께 조금이나마 도움이 되었으면 좋겠습니다.
Platform Business Operation 조직 및 팀 소개
무신사 플랫폼 비즈니스 오퍼레이션 조직은 국내외 물류 서비스, 재고 관리, 스토어 운영을 위한 물류 프로덕트를 구축하고 다양한 오프라인 비즈니스 모델에 맞춘 스토어 관리 시스템을 개발·고도화하고 있습니다. 또한, 무배당발 서비스를 포함한 무신사의 차별화된 고객 경험을 브랜딩하고 확장할 수 있는 멤버십 구조를 설계하며, 온·오프라인을 넘나드는 통합 커머스 경험을 기술로 실현하고 있습니다.
저희 팀은 OMS(주문관리시스템)를 기반으로 온라인 주문부터 재고·출고·배송·정산에 이르는 전 과정을 유기적으로 연결하고, 무신사의 다양한 오프라인 스토어를 효과적으로 운영할 수 있는 관리 시스템을 구축하여 고객이 온라인(무신사 스토어, 29CM 등)과 오프라인(무신사 스탠다드, 편집숍 등)에서 끊김 없는 쇼핑 경험을 누릴 수 있도록 지원합니다.
🚀 팀 무신사 채용 페이지 (무신사/29CM 전체 포지션 확인이 가능해요)
메타데이터
- post_id
- f491e79a0f8f
- slug
- 스케줄이-또-안-돌았어요-우리가-temporal을-선택한-이유-f491e79a0f8f
- url
- https://techblog.musinsa.com/%EC%8A%A4%EC%BC%80%EC%A4%84%EC%9D%B4-%EB%98%90-%EC%95%88-%EB%8F%8C%EC%95%98%EC%96%B4%EC%9A%94-%EC%9A%B0%EB%A6%AC%EA%B0%80-temporal%EC%9D%84-%EC%84%A0%ED%83%9D%ED%95%9C-%EC%9D%B4%EC%9C%A0-f491e79a0f8f
- canonical_url
- https://techblog.musinsa.com/%EC%8A%A4%EC%BC%80%EC%A4%84%EC%9D%B4-%EB%98%90-%EC%95%88-%EB%8F%8C%EC%95%98%EC%96%B4%EC%9A%94-%EC%9A%B0%EB%A6%AC%EA%B0%80-temporal%EC%9D%84-%EC%84%A0%ED%83%9D%ED%95%9C-%EC%9D%B4%EC%9C%A0-f491e79a0f8f
- author_url
- https://medium.com/@sjm726
- status
- ok
- fetched_at
- 2026-06-12 22:02:08