내구성과 반복 속도, 두 마리 토끼를 잡는 런타임 독립적 워크플로우 패턴
프로덕션에서 신뢰할 수 있는 워크플로우를 운영하려면 각 처리 단계를 영속화하고 분산 처리해야 한다. 크래시, 재배포, 재시작 상황에서도 작업이 살아남으려면 상태를 외부 저장소에 기록하고, 각 스텝을 독립적으로 재실행할 수 있는 구조가 필요하다. 하지만 바로 이 메커니즘이 개발 단계에서 빠른 검증 루프를 무겁게 만드는 원인이 된다. 출력 품질을 빠르게 확인하고 싶을 때, 매번 메시지 큐와 분산 트랜잭션을 거쳐야 한다면 반복 속도는 급격히 떨어진다.
이 구조적 긴장 관계는 단순한 불편함이 아니라 설계 수준의 트레이드오프다. 내구성을 확보하는 요소들—단계별 체크포인트, 분산 큐, 재시도 정책—이 정확히 반복 속도를 저해하는 요소들이기 때문이다.
런타임 독립적 설계가 왜 필요한가
이 문제를 해결하는 핵심 아이디어는 워크플로우 로직을 특정 런타임으로부터 분리하는 것이다. 워크플로우의 각 스텝을 순수한 함수나 인터페이스로 정의하고, 런타임 환경(프로덕션용 내구성 런타임 vs. 개발용 경량 런타임)을 외부에서 주입하는 구조를 취한다.
Java 백엔드 관점에서 보면, 이는 전략 패턴(Strategy Pattern)과 유사한 접근이다. 워크플로우 스텝은 인터페이스로 추상화되고, 실행 환경에 따라 구현체를 교체한다.
// 워크플로우 스텝 추상화
public interface WorkflowStep<I, O> {
O execute(I input);
}
// 런타임 실행기 추상화
public interface WorkflowRunner {
<I, O> O run(WorkflowStep<I, O> step, I input);
}
프로덕션에서는 DurableWorkflowRunner가 각 스텝 실행 결과를 DB에 체크포인트로 기록하고 재시도 로직을 수행한다. 반면 개발/평가 환경에서는 InMemoryWorkflowRunner가 동일한 스텝을 오버헤드 없이 인메모리로 즉시 실행한다.
실무 적용 시 고려사항
이 패턴을 실제로 적용할 때는 몇 가지 설계 원칙을 지켜야 효과적이다.
- 스텝의 순수성 보장: 각 스텝은 부수효과를 최소화하고, 동일 입력에 동일 출력을 내도록 설계한다. 이래야 인메모리 런타임과 내구성 런타임 간 동작 차이가 없다.
- 컨텍스트 전파 분리: 트레이싱, 메트릭 수집 같은 관심사는 스텝 로직에 포함하지 않고 런타임 레이어에서 처리한다.
- 평가(Eval) 파이프라인 분리: 출력 품질 검증을 위한 평가 루프는 경량 런타임으로 독립 실행할 수 있도록 구성한다. 프로덕션 코드 변경 없이 빠른 실험이 가능해진다.
// 개발/평가 환경에서의 경량 실행
WorkflowRunner runner = new InMemoryWorkflowRunner();
var result = runner.run(new TextSummaryStep(), inputDocument);
assertQuality(result); // 빠른 품질 검증
이 구조는 스프링의 ApplicationContext 기반 빈 교체나 테스트 프로파일(@Profile)과도 자연스럽게 결합된다. 프로덕션 빈과 테스트 빈을 분리 등록해 두면, 동일한 워크플로우 코드를 환경에 따라 다른 런타임으로 실행할 수 있다.
정리
- 내구성을 위한 설계 요소(체크포인트, 분산 큐, 재시도)는 반복 속도를 저해하므로, 이 둘을 분리하는 런타임 독립적 구조가 필요하다.
- 워크플로우 스텝을 인터페이스로 추상화하고 런타임 구현체를 교체 가능하게 설계하면, 동일한 로직을 프로덕션과 개발 환경에서 각각 최적화된 방식으로 실행할 수 있다.
- 평가(Eval) 파이프라인을 경량 런타임으로 독립 실행할 수 있게 구성하면, 출력 품질 검증 루프를 프로덕션 인프라 부담 없이 빠르게 반복할 수 있다.