Celery/Redis에서 Temporal로: 멱등성과 신뢰할 수 있는 워크플로를 향한 여정

발행: (2026년 1월 8일 오후 08:49 GMT+9)
6 분 소요
원문: Dev.to

Source: Dev.to

분산 시스템에서 비동기 작업을 처리할 때 Celery와 Redis의 조합은 흔히 선택되는 방법입니다. 저도 처음에 KYC(고객 알기) 오케스트레이터를 설계할 때 익숙함 때문에 Celery를 선택했습니다. 하지만 서비스가 복잡해지면서 멱등성 보장과 복잡한 상태 관리라는 큰 장벽에 부딪혔습니다.

Celery는 “fire‑and‑forget”(한 번 실행하고 잊어버리는) 작업에 탁월하지만, 네트워크 장애나 워커 다운으로 인한 재시도 시 중복 실행될 위험이 높습니다. GPU를 많이 사용하는 얼굴 인식 작업에서는 중복 실행이 비용을 크게 늘리고 성능을 저하시켰습니다.

KYC 프로세스

  1. 사용자가 신분증 이미지 업로드.
  2. 사용자가 셀카 비디오 업로드.
  3. 두 파일이 모두 존재할 때 얼굴 유사도 비교.

Celery 환경에서는 이미지와 비디오가 언제 업로드될지 알 수 없었기 때문에, 매번 DB를 조회하거나 중간 상태를 Redis에 저장하는 복잡한 로직이 필요했습니다. “모든 파일이 수집되었는가?” 검사는 여러 곳에 흩어져 있어 유지보수가 어려웠습니다.

Why Temporal?

Temporal은 단순한 메시지 큐가 아니라 상태를 유지하는 워크플로 엔진입니다. 워크플로 코드는 재생 안전(replay‑safe)해야 합니다: 동일한 입력과 히스토리에 대해 동일한 워크플로 API 호출 순서를 생성해야 합니다. 따라서 네트워크 I/O, 파일 I/O, 시스템 시간, 무작위성, 스레딩과 같은 부수 효과는 액티비티로 옮겨야 하며, 워크플로 자체는 오케스트레이션에 집중합니다.

Official docs:

Core Logic of FaceSimilarityWorkflow

# workflow.py
from datetime import timedelta
from temporalio import workflow
from temporalio.common import RetryPolicy

@workflow.run
async def run(self, data: SimilarityData) -> SimilarityResult:
    # Wait up to 1 hour until both image and video are collected
    await workflow.wait_condition(
        lambda: any(f["type"] == "image" for f in self._files)
        and any(f["type"] == "video" for f in self._files),
        timeout=timedelta(hours=1),
    )

    # Execute GPU activity once all files are ready
    result = await workflow.execute_activity(
        check_face_similarity_activity,
        data,
        retry_policy=RetryPolicy(maximum_attempts=3),
    )
    return result

workflow.wait_condition은 조건이 충족될 때까지 워크플로를 일시 중단시키며 이벤트 루프를 차단하지 않습니다—이는 Celery에서 복잡한 폴링이나 웹훅 로직이 필요했을 상황을 대체합니다.

활동 수준에서의 멱등성

Temporal은 워크플로 진행 상황을 이벤트 히스토리로 기록하므로, 워커가 재시작될 경우 마지막으로 성공한 지점부터 정확히 이어서 실행됩니다. 그러나 최소 한 번 실행 모델을 따르는 활동은, 워커가 작업을 완료했지만 서버에 알리기 전에 충돌하면 해당 활동이 재시도될 수 있습니다. 따라서 공식 문서에서는 활동을 멱등하게 만드는 것을 강력히 권장합니다.

공식 문서:

이중 방어 전략

  1. 외부 멱등성 키 – 워크플로 실행 ID와 활동 ID를 결합합니다.
  2. 내부 가드 – 데이터베이스에 고유 키(또는 기존 결과 존재 여부)를 사용하여 중복 저장/처리를 방지합니다.
# activities.py
from temporalio import activity

@activity.defn
async def check_face_similarity_activity(data: SimilarityData) -> SimilarityResult:
    info = activity.info()
    idempotency_key = f"{info.workflow_run_id}-{info.activity_id}"
    session_id = data["session_id"]

    with get_db_context() as db:
        existing = (
            db.query(FaceSimilarity)
            .filter(FaceSimilarity.idempotency_key == idempotency_key)
            .first()
        )
        if existing:
            return SimilarityResult(success=True, message="Already processed.")

    # Perform actual GPU‑intensive work...
    # (store result with the same idempotency_key)

비교: Celery/Redis vs. Temporal

FeatureCelery/RedisTemporal
State ManagementDB/Redis에 수동 저장엔진이 자동으로 관리
Retry Strategy수동 지수 백오프선언적 재시도 정책
Visibility로그를 직접 찾아야 함Temporal UI에서 히스토리 확인
Idempotency보장하기 매우 어려움구조적으로 달성 가능

요약

Celery에서 Temporal로 전환한 것은 단순히 도구를 교체하는 것이 아니라 비즈니스 프로세스를 코드로 표현하는 방식을 재정의하는 것이었습니다. 멱등성이 가장 중요한 금융 및 인증 시스템에서 Temporal은 대체할 수 없는 안정성을 제공합니다.

복잡한 비동기 로직과 멱등성 문제로 잠을 못 이루고 있다면, Temporal로 마이그레이션할 것을 강력히 권장합니다.

Back to Blog

관련 글

더 보기 »

안녕, 뉴비 여기요.

안녕! 나는 다시 S.T.E.M. 분야로 돌아가고 있어. 에너지 시스템, 과학, 기술, 공학, 그리고 수학을 배우는 것을 즐겨. 내가 진행하고 있는 프로젝트 중 하나는...