
이번 아티클은 취미로 진행 중인 사이드 프로젝트 커머스 플랫폼(pick-me)에서 Temporal 워크플로우 엔진을 도입한 과정을 공유한다.
1. 왜 Temporal이 필요했는가

pick-me는 주문, 결제, 재고, 정산, 파트너를 다루는 이커머스 플랫폼이다. 처음에는 Kafka 기반 코레오그래피 사가 패턴으로 서비스 간 통신을 설계했다.

각 서비스가 이벤트를 발행하고 구독하는 구조다. 단순한 흐름에서는 유연한 구조였으나, 도메인이 비대해지면서 코레오그래피 방식의 구조적 한계에 직면하게 되었는데 구체적으로 다음과 같다.
⚙️ 무엇이 문제였나
| 문제 | 무엇이 문제였나 | 어떤 고통이 있었나 |
| 트랜잭션 가시성 부재 | 진행 상태를 한눈에 볼 수 있는 곳이 없음 | 장애 발생 시 여러 서비스의 로그를 일일이 대조 |
| 프로세스 고착 | 이벤트 유실 시 주문이 특정 상태에서 멈춤 | 매일 새벽 배치를 돌려 수동으로 사후 복구 |
| 보상 로직 정합성 결여 | 복구 로직이 여러 컨슈머에 파편화됨 | 보상 순서 미보장 및 데이터 불일치 빈번 |
| 타임아웃 관리 불가 | 응답이 없어도 워크플로우가 무한 대기 | 실시간 장애 감지가 안 되어 고객 불만 가중 |
| 로직 파편화 | 전체 흐름이 6개 이상의 서비스에 분산 | 코드 파악 및 신규 인력 온보딩에 과도한 시간 소요 |
결국 본질은 기술(Kafka)의 한계가 아니라 설계(Choreography)의 부적합성이었다.
Kafka는 알림 전송이나 분석용 데이터 전파(CQRS)처럼 이벤트를 뿌리는 데는 적합한 도구이지만, 순차적 실행과 실패 시 복구가 필수적인 비즈니스 워크플로우 제어에는 자율적인 코레오그래피보다 중앙 집중식 오케스트레이션이 적합하다.
2. Temporal이란 무엇인가
Temporal은 한마디로 '지속 가능한 실행'을 보장하는 도구다. 서버가 꺼지거나 네트워크 장애가 발생해도 비즈니스 로직이 중단되지 않고 끝까지 실행되도록 돕는다.

일반 함수와 Temporal Workflow의 차이를 비교하면 다음과 같다.
| 구분 | 일반 함수 (Stateless) | Temporal Workflow (Stateful) |
| 실행 환경 | 휘발성 메모리 기반 | 상태 보존형(Stateful) 실행 |
| 장애 대응 | 프로세스 종료 시 진행 데이터 소실 | 장애 지점부터 자동 복구 및 재개 |
| 인프라 종속성 | DB 장애/서버 재시작 시 로직 증발 | 외부 환경과 무관하게 실행 정합성 유지 |
⚙️ 전체 아키텍처 구성

Temporal 아키텍처는 제어 기능을 담당하는 Server Cluster와 실제 로직을 수행하는 Client Application으로 분리된다.
- Workflow Client: 워크플로우의 시작, 상태 쿼리, 외부 신호(Signal) 전송을 담당하는 인터페이스다.
- Worker Process: 실제 비즈니스 로직(Workflow & Activity)이 실행되는 호스트다.
⚙️ Temporal Server 내부 핵심 서비스

서버 클러스터는 각자의 역할이 분리된 4개의 마이크로서비스로 구성되어 동작한다.
| 서비스 | 하는 일 |
| Frontend Service | 외부 요청의 관문 (gRPC/HTTP API 제공, 인증, Rate Limiting, 라우팅) |
| History Service | 워크플로우 상태 전이(State Transition) 관리 및 이벤트 히스토리 영속화 |
| Matching Service | Task Queue 관리 및 가용 Worker에 태스크 할당 (Load Balancing) |
| Worker Service | 시스템 내부 워크플로우 실행 및 가비지 컬렉션(GC), 아카이빙 등 운영 작업 |
⚙️ Client Application 구조

클라이언트 사이드는 제어부와 실행부로 나뉘어 비즈니스 로직의 독립성을 확보한다.
- Workflow Client: 비즈니스 프로세스 외부에서 워크플로우를 제어(시작/조회/신호)한다.
- Worker Process: 실제 연산이 일어나는 곳으로, 성격에 따라 세분화된다.
- Workflow Worker: 전체적인 비즈니스 흐름(오케스트레이션)을 제어한다.
- Activity Worker: DB 입출력, API 호출 등 외부 시스템과 상호작용하는 최소 단위의 기능을 수행한다.
- Local Activity: 오버헤드가 적은 짧은 작업을 효율적으로 처리한다.
⚙️ Persistence Layer

Temporal은 모든 상태를 이벤트 소싱(Event Sourcing) 방식으로 기록하여 신뢰성을 확보한다.
- 저장 데이터: 워크플로우 메타데이터, 전체 실행 히스토리, Task 및 Timer 정보.
- 지원 DB: PostgreSQL, MySQL, Cassandra 등.
- 선택 배경: 기존 인프라와의 정합성 및 운영 효율성을 고려하여 PostgreSQL을 영속성 스토리지로 채택, 안정적인 트랜잭션 관리를 구현했다.
3. 아키텍처 설계
Temporal 도입에서 가장 고민했던 부분은 '기존 이벤트 기반 아키텍처와의 조화'와 '인프라 기술(SDK)로부터 도메인 로직을 보호하는 것'이었다.
⚙️ Temporal과 Kafka의 역할 분담 (Separation of Concerns)
| 비교 항목 | Temporal (Orchestrator) | Kafka (Message Broker) |
| 핵심 역할 | 사가(Saga) 워크플로우 제어 | 도메인 이벤트 전파 및 데이터 동기화 |
| 강점 | 상태 전이의 명시적 정의, 보상 트랜잭션, 타임아웃/재시도 관리 | 다수 컨슈머에 대한 팬아웃, 서비스 간 느슨한 결합 |
| 비즈니스 가치 | 프로세스의 실행 보장과 가시성 확보 | 시스템 간 비동기 통합 및 데이터 일관성 유지 |
⚙️ DDD 관점: Temporal SDK가 도메인을 오염시키면 안 된다
Temporal SDK가 도메인 레이어에 침투하여 비즈니스 로직을 오염시키는 것을 방지하기 위해, 의존성 역전 원칙(DIP)을 활용하여 모듈 구조를 설계했다.
pickme-orchestration-api: 특정 기술에 의존하지 않는 순수 Java 인터페이스와 DTO 정의 (도메인 보호).
pickme-orchestration: Temporal SDK를 포함하며, 실제 워크플로우와 액티비티 구현체가 위치하는 인프라스트럭처 레이어.
- 액티비티는 CommandPort 인터페이스를 통해 각 도메인 모듈에 작업을 위임한다.



또한, 이러한 구조적 약속이 코드 레벨에서 강제되도록 ArchUnit을 도입했다.
"Orchestration 모듈 이외의 곳에서 Temporal SDK를 참조할 경우 빌드가 실패한다."

⚙️ EDA 관점: 트랜잭셔널 아웃박스와의 연동
Temporal Activity에 의해 도메인 상태가 변경되면, 도메인 모델은 이전과 동일하게 이벤트를 발행한다.
- Temporal: "언제" 어떤 비즈니스 행위를 실행할지 지휘한다.
- Transactional CommandAdapter: 상태 변경과 이벤트 발행을 하나의 트랜잭션으로 묶어 Outbox 테이블에 기록한다.
- Kafka: Outbox Relay를 통해 발행된 이벤트를 알림 전송, CQRS 모델 갱신, 정산 시스템 등으로 전달한다.

⚙️ MSA 관점: 보상 트랜잭션의 중앙 집중화

기존 코레오그래피 방식에서는 보상 로직이 여러 서비스의 Kafka Consumer에 파편화되어 있어 흐름 파악이 어려웠다. 이제는 단일 워크플로우 정의서 내에 모든 보상 액션이 명시적으로 기술된다.
- 실패 지점별 자동 보상: 재고 예약 실패부터 주문 확정 실패까지, 각 단계에 대응하는 복구 로직이 순차적으로 실행된다.
- 운영 효율성: 신규 입사자가 하나의 워크플로우 코드만 읽어도 전체 주문 사가 흐름을 파악할 수 있다.
4. 마이그레이션 전략
운영 중인 커머스 시스템이라고 가정하고, 리스크를 최소화하기 위해 Feature Flag와 Shadow Mode를 활용한 4단계 전략을 수립했다.
⚙️ Phase 0: 기반 인프라 구축
실제 로직 변경 없이, Temporal이 동작할 수 있는 토대만 마련한 단계입니다.
- 추상화 레이어 도입: pickme-orchestration-api 모듈을 통해 도메인과 인프라의 접점 설계.
- 환경 설정: Docker Compose 및 쿠버네티스 환경에 Temporal 클러스터 배치.
- 가드레일 설치: pickme.temporal.enabled=false 설정을 통해 런타임 영향을 차단한 상태에서 CommandPort/Adapter 사전 구현.
⚙️ Phase 1: Shadow Mode (검증 모드)
실제 데이터는 Kafka(기존 방식)가 처리하고, Temporal은 뒤에서 동작하며 결과를 대조하는 단계다.
- Dry-run 실행: ShadowOrderActivitiesImpl을 통해 상태 변경(Side-effect) 없이 로직만 수행.
- 결과 정합성 비교: 기존 Kafka 코레오그래피의 최종 상태와 Temporal 워크플로우의 실행 결과를 비교 분석.
⚙️ Phase 2: 운영 전환
본격적으로 Temporal이 오케스트레이션을 주도하는 단계다. Spring의 @ConditionalOnProperty를 활용해 기존 Kafka Saga Consumer를 종료했다.
- Saga 제어권 이관: Feature Flag를 통해 기존 분산된 사가 로직 비활성화.
- 관심사 분리 유지: 알림, 정산 집계, 스냅샷 등 비사가(Non-Saga) 이벤트는 여전히 Kafka가 담당하도록 유지하여 시스템 복잡도 관리.


⚙️ Phase 3: 도메인 확장
주문 외에도 복잡한 비즈니스 프로세스가 필요한 도메인으로 Temporal을 확대 적용했습니다.
| 워크플로우 | 비즈니스 로직 | Task Queue |
| Refund Workflow | 환불 요청 → PG사 연동 → 재고 복원 → 상태 확정 | pickme-order-saga |
| Settlement Recon | 일일 데이터 스냅샷 생성 → 파트너별 정산 검증 → 리포트 발행 | pickme-settlement |
| Partner Onboarding | 파트너 등록 → 관리자 승인 대기 → 승인/거절 처리 | pickme-partner |
대표적으로 특히 관리자 승인이 필요한 온보딩 프로세스에서 Temporal의 필요성을 느꼈다.
@SignalMethod를 활용해 외부 시스템(관리자 App)의 신호를 기다리는 로직을 한 줄의 코드로 구현 가능하다.

워크플로우 안에서 최대 7일 동안 시그널을 기다린다.

이전에는 스케줄러, 상태 DB, 폴링 배치가 필요했던 복잡한 '대기 로직'이 코드 한 줄로 해결되었다.
5. 구현 과정에서 겪은 것들
⚙️ 서비스 성격에 따라 다른 재시도 전략
모든 액티비티에 동일한 정책을 적용하는 것은 비효율적이다. 외부 연동 여부와 응답 속도를 고려해서 액티비티마다 별도로 튜닝을 진행했다.
- 내부 서비스 호출 (재고/주문 확정): 저지연 응답이 중요하므로 짧은 타임아웃과 공격적인 재시도 간격(500ms)을 설정하여 일시적인 네트워크 장애에 빠르게 대응한다.
- 외부 PG 연동 (결제): 외부 인프라의 가변성을 고려하여 긴 타임아웃(60s)과 지수 백오프를 적용했다. 특히, 비즈니스 에러는 재시도 대상에서 제외하여 불필요한 리소스 낭비를 차단했다.
Tip: Temporal은 동일한 인터페이스라도 각기 다른 ActivityOptions가 주입된 스텁(Stub)을 생성할 수 있어, 정책 관리가 매우 유연하다.

⚙️ 트랜잭션 동기화와 데이터 정합성 (afterCommit)
비즈니스 DB 트랜잭션과 워크플로우 실행 사이의 Race Condition을 해결하는 것이 가장 큰 과제였다.
- Race Condition 방지: @Transactional 내부에서 워크플로우를 즉시 실행하면, DB 커밋이 완료되기 전에 액티비티가 주문 데이터를 조회하여 NotFound 에러가 발생할 수 있다. 이를 막기 위해 afterCommit() 이벤트를 활용해 커밋 완료 시점에 워크플로우를 트리거했다.
- 고아 주문(Orphan Order) 방지: 만약 DB 커밋은 성공했으나 워크플로우 실행 호출이 실패할 경우, '주문'은 존재하지만 '프로세스'가 없는 불일치 상태가 된다.
- 자가 치유(Self-healing): 이를 해결하기 위해 ConsistencyCheckBatch를 구축, 워크플로우가 없는 주문을 사후 탐지하여 자동으로 워크플로우를 생성해 주는 보정 메커니즘을 마련했다.

⚙️ 3단계 멱등성 설계
분산 환경에서 발생할 수 있는 중복 처리를 차단하기 위해 멱등성 전략을 3단계로 설계했다.
| 계층 | 어떻게 | 해결하는 문제 |
| Workflow 계층 | Workflow ID 기반 유일성 보장 | 동일 주문에 대해 중복 워크플로우가 생성되는 현상 방지 |
| Activity 계층 | Idempotency Filter (Business Key) | 액티비티 재시도 시 외부 API(결제 등)가 중복 호출되는 리스크 차단 |
| Event 계층 | Event ID 기반 중복 제거 | Kafka 메시지 브로드캐스트 시 중복 소비 문제 해결 |
특히 액티비티 내부에서는 주문 ID와 단계명을 조합하여 UUID를 생성하고, 이를 멱등성 키로 활용하여 인프라 장애 상황에서도 비즈니스 로직의 안전성을 확보했다.

6. 배운 것
Temporal Workflow 엔진을 도입하면서 아래와 같은 내용에 대해 고민해볼 수 있었다.
⚙️ 발견 1: 결제 성공 후 주문 확정이 실패하면?
발견된 문제: 결제 승인은 완료되었으나 주문 시스템의 확정(confirmOrder) 처리 중 장애가 발생할 경우, 결제 금액은 차감되고 주문은 생성되지 않는 데이터 불일치 발생.
개선 대책: 전방위적 보상 트랜잭션 체인(safeRefundAndRestoreAndCancel) 구축.
- 결제 취소 → 재고 복원 → 주문 무효화로 이어지는 일련의 과정을 원자적으로 처리.
- 보상 로직 자체의 실패에 대비하여 compensationFailed 플래그를 통한 모니터링 및 수동 조치 경로 확보.
⚙️ 발견 2: 배포할 때 워크플로우 코드를 바꾸면?
발견된 문제: 프로덕션 환경에서 실행 중인 워크플로우가 있는 상태에서 코드를 수정·배포할 경우, Temporal의 리플레이 특성상 실행이 깨지며 런타임 에러 발생.
개선 대책: 첫 배포 시점부터 버전 게이트 로직을 의무화.
- Workflow.getVersion()을 사용하여 구버전 히스토리와 신버전 로직의 호환성 유지.

⚙️ 발견 3: Kafka 롤백할 때 메시지가 한꺼번에 밀려온다
발견된 문제: Temporal 전환 기간 동안 비활성화되었던 Kafka Consumer를 다시 롤백할 경우, 그동안 누적된 메시지가 한꺼번에 유입되어 시스템 부하 가중.
개선 대책: 전략적 컨슈머 오프셋(Offset) 관리 정책 수립.
- 롤백 시나리오에 오프셋 리셋 절차 포함.
- Temporal 활성화 중에도 Kafka 오프셋을 주기적으로 동기화하여 전환 시점의 갭 최소화.
⚙️ 발견 4: 진행 중인 워크플로우를 취소할 방법이 없었다
발견된 문제: 이미 실행 중인 장기 워크플로우에 대해 관리자가 즉시 중단하거나 개입할 수 있는 인터페이스 부재.
개선 대책: cancelByAdmin 시그널(Signal) 및 인터럽트 로직 구현.
- 비즈니스 단계별 상태 체크 포인트에 취소 시그널 확인 로직을 배치하여 즉각적인 흐름 제어권 확보.

오케스트레이션은 Temporal: 트랜잭션의 순서 보장, 타임아웃, 보상 트랜잭션 등 제어가 필요한 영역에 집중.
브로드캐스트는 Kafka: 시스템 전반에 걸친 이벤트 전파, 데이터 동기화, 분석용 데이터 스트리밍에 활용.
두 기술은 대체 관계가 아닌, 보완 관계로서 시스템의 견고함을 지탱한다.
가장 큰 수확은 의존성 격리였다.
pickme-orchestration-api와 같은 순수 Java 인터페이스 모듈 없이 도입했다면 인프라 기술인 Temporal SDK가 도메인 로직 전반을 오염시켰을 것이다.
출처
“스케줄이 또 안 돌았어요” — 우리가 Temporal을 선택한 이유
지긋지긋한 스케줄 장애, ‘실패해도 스스로 살아나는 시스템(Temporal)’ 도입기를 공유합니다.
techblog.musinsa.com
카카오 면접관과 함께하는 워크플로우 기반의 대용량 트래픽 처리 기법| Hong - 인프런 강의
현재 평점 4.9점 수강생 494명인 강의를 만나보세요. 폭증하는 트래픽, 어떻게 견딜 것인가? Kafka, Spring, CDC, Temporal을 활용한 EDA(Event-Driven Architecture) 기반의 실전 설계 패턴을 통해, 장애에 강하고
www.inflearn.com
https://tech.korbit.co.kr/posts/building-chronos-with-temporal/
입출금 운영 자동화 시스템 Chronos 개발기 (feat. Temporal)
새벽 3시, 입출금 중단을 위해 알람을 맞춰 일어나본 적 있으신가요? 100% 수동이었던 운영 업무를 자동화한 Chronos 프로젝트를 소개합니다. Temporal을 사용해 개발자를 24시간 대기에서 해방시키고
tech.korbit.co.kr
'Dev' 카테고리의 다른 글
| 코빗은 왜 체결 엔진에 Rust를 선택했을까? (feat. 추리) (1) | 2026.04.22 |
|---|---|
| Kafka 간 맞추기 (1) | 2026.04.20 |
| 와 CDC(Change Data Capture)! Debezium 로그 기반 아시는구나! (0) | 2026.04.08 |
| Kafka 간보기 (1) | 2026.04.07 |
| DDD 안에 객체지향이 있잖아! (0) | 2026.04.06 |