영속적 지연(Durable Delays): 분산 상태로서의 시간 관리
SchemaBridge Team · 2026-01-10 · Time, Scheduling, Orchestration
메모리 누수 없이 30일 대기 기간을 처리하는 방법. `sleep()`이 분산 시스템의 안티패턴인 이유.
sleep()의 거짓말: 시간이 가장 다루기 어려운 분산 프리미티브인 이유
모든 프로그래밍 언어에는 대기를 위한 명령이 존재합니다. Python에서는 time.sleep(), Node.js에서는 setTimeout(), Java에서는 Thread.sleep()입니다. 이 명령들은 단순하고 직관적이지만, 몇 초를 넘어서는 복잡한 상황에서는 프로덕션 분산 시스템에서 완전히 쓸모없습니다.
겸손해 보이는 sleep()이 거짓말인 이유는 환경이 안정적이라고 가정하기 때문입니다. 코드를 실행하는 머신이 계속 살아있을 것이고, 프로세스가 로드 밸런서에 의해 종료되지 않을 것이며, 메모리가 OS에 의해 회수되지 않을 것이라고 가정합니다. 현대 클라우드 환경에서 이러한 가정은 성립하지 않습니다. 표준 Kubernetes 파드에서 sleep(24 60 60)(하루)을 실행한다면, 하루가 지나기 전에 해당 파드가 교체되거나, 축소되거나, 재배포될 확률이 99%입니다.
프로세스가 죽으면 여러분의 "sleep"도 함께 소멸합니다. 비즈니스 로직은 허공으로 사라집니다. 이렇게 해서 "건망증 걸린 소프트웨어(Forgetful Software)"가 탄생합니다. 고객 체험판을 놓치고, 구독 갱신을 빠뜨리고, 중요한 후속 이메일을 보내지 못하는 시스템 말입니다.
시간의 계층 구조: 초 단위에서 개월 단위까지
시간을 올바르게 관리하려면 먼저 이를 분류해야 합니다. 모든 지연이 동등하게 만들어진 것은 아닙니다.
1. 일시적 지연 (밀리초에서 초 단위): 이는 대개 네트워크 백오프이거나 빠른 데이터베이스 락을 위한 대기 시간입니다. 50ms 구간에서 크래시가 발생할 위험이 낮기 때문에 여기서는 sleep()이 허용될 수도 있습니다.
2. 짧은 지연 (분 단위): 여기서부터 sleep()이 실패하기 시작합니다. 아무것도 하지 않으면서 몇 분 동안 워커 스레드나 컨테이너 리소스를 붙잡아 두게 됩니다. 이는 비용 낭비이자 커넥션 풀에 대한 위험입니다.
3. 영속적 지연 (시간에서 개월 단위): 이는 비즈니스 생애주기(Business Lifecycles)의 영역입니다. 14일 무료 체험, 30일 결제 조건, 6개월 유지보수 일정 등입니다. 이런 것들은 코드 안에 존재할 수 없습니다. 인프라 안에 존재해야 합니다.
잠자는 워크플로의 아키텍처
SchemaBridge에서 우리는 시간을 영속적 상태(Durable State)로 취급합니다. 워크플로가 "지연 버텍스(Delay Vertex)"에 도달하면, 스레드를 블로킹하지 않습니다. 디스크에 자기 자신을 직렬화하고 메모리상에서는 존재를 멈춥니다.
DB 폴링 모델 (낮은 정밀도, 높은 내구성)
"기상 시각(Wake-up Time)"을 데이터베이스 테이블에 저장합니다. 백그라운드 워커("폴러")가 몇 초마다 테이블을 조회합니다: SELECT * FROM timers WHERE wake_up < NOW(). DB 레코드는 몇 년이고 살아남을 수 있기 때문에 이는 대단히 견고합니다.
SchemaBridge의 접근 방식
SchemaBridge는 DynamoDB를 기반으로 하는 영속적 스케줄링 엔진(Persistent Scheduling Engine)을 사용합니다. 우리는 "깨어나겠다는 의도"를 ScheduledTaskRepository에 명시적으로 영속화합니다. 이는 데이터베이스 레코드 수준의 천년 단위 내구성을 제공합니다.
시그널과 인터럽트: 미래를 바꾸는 방법
취소하거나 수정할 수 없는 영속적 지연은 쓸모가 없습니다. 사용자가 "결제를 위해 3일 대기" 버텍스에 있는데 2시간 만에 결제를 완료했다면, 워크플로를 즉시 깨워서 다음 단계로 진행해야 합니다.
SchemaBridge는 외부 시그널(External Signals)을 지원합니다. 시그널은 실행 중인 워크플로로 전송되는 이벤트입니다.
- 대기: 워크플로는
T + 3일시점 또는Payment_Received시그널이 도착할 때까지Delay버텍스에 머무릅니다. - 인터럽트: 시그널이 엔진에 도달하면, 엔진은 해당 특정 인스턴스를 조회하고, 상태를 재구성(rehydrate)한 뒤, 즉시 버텍스를 전이시킵니다.
우리는 이러한 예약 작업을 취소하거나 인터럽트하기 위한 명시적인 API를 제공합니다(예: nuke 명령으로 cron 작업을 중지하거나, 웹훅 시그널로 특정 지연을 인터럽트하는 방식). 지연과 시그널 모두 엔진이 처리하기 때문에, 시스템은 경쟁 조건(race condition)에 영향을 받지 않습니다.
글로벌 메시에서의 클럭 드리프트와 정밀도
여러 AWS 리전에 걸쳐 분산된 시스템에서는 클럭이 결코 완벽하게 동기화되지 않습니다. 이것이 바로 클럭 드리프트(Clock Drift) 현상입니다. 리전 A의 클럭이 리전 B보다 50ms 앞서 있다면, "1초 대기"는 작업이 어디서 처리되는지에 따라 다른 결과를 낳을 수 있습니다.
SchemaBridge는 중앙 집중형 메타데이터 계층에서 파생된 논리적 클럭 시그니처(Logical Clock Signature)를 사용하여 이를 해결합니다. 개별 워커는 실행을 위해 자신의 로컬 클럭을 사용하지만, "시간의 의도(Intent of Time)"는 전역적으로 동기화되어 불변 이력에 기록됩니다. 워커의 클럭이 약간 어긋나 있는지는 중요하지 않습니다. 중요한 것은 실행 기간(Duration of Execution)이 비즈니스 의도와 일치하는지입니다.
사례 연구: 실패율 0%로 30일 드립 캠페인 자동화하기
우리는 "웰컴 여정(Welcome Journey)" 로직 때문에 어려움을 겪고 있던 마케팅 자동화 플랫폼과 협업했습니다.
과제
하나의 여정은 다음과 같이 구성되었습니다.
- 이메일 1 발송 (1일 차).
- 3일 대기.
- 사용자가 클릭하지 않았다면 이메일 2 발송.
- 7일 대기.
- 사용자가 업그레이드하지 않았다면 프로모션 코드 발송.
그들은 Cron 작업과 Postgres 테이블로 만든 자체 구축 솔루션을 사용하고 있었습니다. 매주 데이터베이스 유지보수 시간대나 배포 시점마다, 약 5%의 사용자가 대기 상태에 "멈춰" 다음 이메일을 영영 받지 못했습니다. 매달 수천 달러에 달하는 잠재적 전환 수익을 잃고 있었습니다.
SchemaBridge 방식
그들은 사용자 여정을 SchemaBridge 워크플로로 이전했습니다.
1. 시각적 지연: 그들은 말 그대로 "지연(Delay)" 버텍스를 그래프에 드래그해 넣고 3d와 7d로 설정했습니다.
2. 영속적 재개: 배포 중에는 워크플로가 단순히 데이터베이스 안에서 일시 정지되었습니다. 엔진이 다시 온라인 상태가 되면, 다운타임 동안 만료된 타이머들을 확인하고 즉시 올바른 순서로 재개했습니다.
3. 시그널 연동: 그들은 우리의 웹훅 게이트웨이를 사용해 "클릭" 시그널을 전송했습니다. 사용자가 이메일을 클릭하면, 워크플로가 즉시 깨어나 "성공" 경로로 전이되어 남은 지연을 건너뛰었습니다.
결과
- 신뢰성: 5%의 실패율에서 손실된 여정 0건으로 개선되었습니다.
- 개발자의 기쁨: 여정 관련 코드베이스가 70% 줄었습니다. 복잡한 Cron 로직과 자체 제작한 "작업 폴러(Job Poller)" 서비스를 삭제했습니다.
- 비즈니스 효과: 사용자가 마침내 정확한 시점에 이메일을 받게 되면서 전환율이 12% 향상되었습니다.
비교: 기존 스케줄링 전략들
| Feature | setTimeout() | Cron 작업 / Quartz | SchemaBridge 영속적 지연 |
| :--- | :--- | :--- | :--- |
| 영속성 | 없음 (휘발성) | 수동 (DB 기반) | 네이티브 (영속적 이력) |
| 확장성 | 낮음 (RAM에 종속) | 중간 (DB 병목) | 높음 (샤딩된 DDB) |
| 취소 | 복잡함 (수동 핸들) | 수동 DB 정리 | 시각적 시그널/인터럽트 |
| 가시성 | 없음 | SQL 쿼리 | 시각적 진행 대시보드 |
| 해상도 | 밀리초 | 초/분 | 관리형 폴링 |
장기 실행 비즈니스 로직을 위한 전문가 체크리스트
5분 이상 대기하는 시스템을 설계하고 있다면 다음 원칙을 따르세요.
1. 스레드를 멈추세요: 대기하는 동안 절대 워커를 블로킹하지 마세요. 워커는 상태를 갖지 않아야(stateless) 하며 언제든 종료될 준비가 되어 있어야 합니다.
2. 시계를 외부화하세요: 로컬 머신의 시스템 시간이 아니라 중앙 엔진을 사용해 시간의 흐름을 관리하세요.
3. 인터럽트를 고려해 설계하세요: 지연이 만료되기 전에 사용자가 여러분이 기다리는 행동을 먼저 취할 수 있다고 항상 가정하세요. 시그널을 사용해 대기를 "인터럽트 가능하게(Interruptible)" 만드세요.
4. 대기실을 감사하세요: 대시보드를 사용해 현재 몇 천(혹은 몇 백만) 개의 워크플로가 "수면" 상태에 있는지 확인하세요. 이는 비즈니스 건강도를 보여주는 중요한 지표입니다.
5. 논리적 오프셋을 활용하세요: 기상 시각에 고정된 타임스탬프(예: "1월 15일")를 사용하지 마세요. 워크플로의 첫 단계가 지연되더라도 상대적인 간격이 일정하게 유지되도록 논리적 오프셋(예: +3d)을 사용하세요.
결론: 시간은 인프라 문제다
잊어버리는 소프트웨어는 실패하는 소프트웨어입니다. 시간을 일급(first-class) 영속적 분산 상태로 취급함으로써, 우리는 실시간 이벤트와 고지연 인간 행동 사이의 간극을 메웁니다. SchemaBridge를 사용하면, 밀리초 단위의 로직을 만들 때와 동일한 확신을 가지고 몇 개월에 걸친 여정을 구축할 수 있습니다.
"경험 경제(Experience Economy)"의 시대에, 상호작용의 타이밍을 완벽하게 관리하는 능력은 여러분의 가장 큰 경쟁 우위입니다. 우리는 시계를 제공하고, 여러분은 여정을 제공하십시오.
8부에서는 "관측 가능성 격차(Observability Gap)"를 살펴보고, 로그 속에서 정신을 잃지 않으면서도 이렇게 복잡한 다중 일 단위의 분산 체인을 시각적으로 디버깅하는 방법을 알아보겠습니다.