본문으로 건너뛰기

Step Functions으로 가는 길: Rails를 위한 워크플로우 엔진 geneva_drive

On the way to step functions: geneva_drive

작성자
발행일
2026년 02월 07일

핵심 요약

  • 1 geneva_drive는 Rails의 ActiveRecord 모델을 기반으로 설계되어 워크플로우의 상태를 데이터베이스에서 직접 쿼리하고 관리할 수 있는 강력한 가시성을 제공합니다.
  • 2 Ruby의 싱글톤 클래스와 인스턴스 메서드 구조를 활용하여 선언적인 DAG 정의와 명령적인 실행 코드를 완벽하게 분리함으로써 직관적인 API를 구현했습니다.
  • 3 외부 의존성 없이 Rails의 표준 스택인 ActiveRecord와 ActiveJob만을 사용하여 복잡한 비즈니스 프로세스를 안정적이고 내구성 있게 실행할 수 있도록 돕습니다.

도입

이 글은 'Step Functions으로 가는 길' 시리즈의 네 번째 기사로, Rails 애플리케이션에서 복잡한 비즈니스 로직을 관리하기 위한 오픈 소스 워크플로우 엔진인 geneva_drive를 상세히 소개합니다. 저자는 기존의 분산 시스템이나 워크플로우 도구들이 가진 과도한 복잡성과 불투명성을 지적하며, Rails 개발자들에게 친숙한 ActiveRecord와 ActiveJob을 활용해 직관적이고 내구성 있는 실행 환경을 구축하는 방법을 제시합니다. 특히 연속적인 흐름을 통제된 이산적 단계로 변환하는 메커니즘을 통해 장기 실행 프로세스를 안정적으로 관리하는 데 초점을 맞춥니다.

1. geneva_drive의 핵심 철학: 제네바 드라이브 메커니즘

‘geneva_drive’라는 명칭은 연속적인 회전 운동을 간헐적인 회전 운동으로 변환하여 영화 영사기에서 필름을 한 프레임씩 정확하게 전진시키는 기계 장치에서 유래했습니다. 소프트웨어 워크플로우 엔진으로서 geneva_drive의 목표 역시 이와 동일합니다. 끊임없이 흐르는 시간과 복잡한 비즈니스 로직을 통제 가능하고 이산적인 단계(Discrete Steps)로 변환하여, 시스템이 각 단계를 명확하게 인식하고 실행할 수 있도록 돕는 것입니다.

2. “Hero” 패턴을 통한 도메인 모델과의 긴밀한 결합

대부분의 워크플로우 엔진은 애플리케이션의 데이터 영역과 분리된 외부 시스템으로 작동하지만, geneva_drive는 ‘Hero’ 패턴을 통해 Rails의 도메인 모델과 직접 연결됩니다. - 주인공(Hero) 지정: 모든 워크플로우는 특정 ActiveRecord 모델(예: Payment, User, Order)을 주인공으로 삼아 다형성 관계(Polymorphic Association)를 맺습니다. - 안정적 식별과 제약: 데이터베이스 수준에서 {hero_type, hero_id, workflow_type} 조합을 통해 동일한 대상에 대해 중복된 워크플로우가 실행되는 것을 방지합니다. - 상태 엔티티화: 단순한 작업 티켓인 ActiveJob과 달리, 워크플로우 자체가 상태를 가진 영속적인 엔티티로 존재하여 비즈니스 로직의 흐름을 명확히 추적할 수 있습니다.

3. Ruby 객체 모델을 활용한 선언적 DAG와 명령적 실행의 분리

저자는 Ruby의 싱글톤 클래스(Eigenclass)와 인스턴스 메서드의 차이를 활용하여 워크플로우 설계의 고질적인 문제인 ‘구조 정의와 실행 코드의 혼재’를 해결했습니다. - 선언적 DAG (Class Body): 클래스 정의 시점에 실행되는 step 메서드 호출은 워크플로우의 전체 구조(DAG)를 정의합니다. 이는 로드 타임에 한 번 평가되며 선언적인 성격을 띱니다. - 명령적 실행 (Instance Blocks): step에 전달된 블록은 실제 실행 시점에 워크플로우 인스턴스 문맥에서 수행됩니다. 이를 통해 각 단계는 hero에 접근하거나 pause!와 같은 흐름 제어 메서드를 자유롭게 사용할 수 있습니다.

4. ActiveRecord 기반 아키텍처의 실질적 이점

geneva_drive의 가장 큰 특징은 워크플로우 자체가 ActiveRecord 모델이라는 점입니다. 이는 다음과 같은 강력한 이점을 제공합니다. - 강력한 쿼리 가능성: 표준 SQL을 사용하여 특정 단계에서 지연된 워크플로우를 찾거나, 특정 사용자의 온보딩 완료 여부를 조회하는 등의 작업이 매우 간편합니다. - 트랜잭션 일관성: 비즈니스 로직과 워크플로우의 상태 업데이트를 동일한 데이터베이스 트랜잭션 내에서 처리할 수 있어, 분산 시스템에서 발생하는 최종 일관성(Eventual Consistency) 문제를 최소화합니다. - 인프라 단순화: Redis, Kafka, gRPC 등 추가적인 외부 의존성 없이 기존 Rails 스택만으로 작동하므로 운영 및 유지보수 비용이 획기적으로 절감됩니다.

5. 정교한 흐름 제어와 내구성 있는 스케줄링

워크플로우 내부에서는 reattempt!, pause!, skip!, cancel!, finish! 등의 메서드를 통해 실행 흐름을 제어합니다. - Ruby throw 활용: 이러한 제어 메서드들은 내부적으로 Ruby의 throw 메커니즘을 사용하여 구현되었으며, 실행 엔진이 이를 캐치하여 적절한 상태 변경 및 재스케줄링을 수행합니다. - 안전한 재시도: ActiveJobwait_until을 활용하되, 모든 단계 실행 전에 StepExecution 레코드를 DB에 먼저 기록합니다. 이는 작업 큐 저장소가 유실되는 최악의 상황에서도 DB 레코드를 통해 전체 워크플로우를 복구할 수 있게 합니다.

6. 효율적인 테스트와 시간 여행 기능

수일에 걸쳐 실행되는 장기 워크플로우를 검증하기 위해 speedrun_workflow 테스트 헬퍼를 제공합니다. 이는 ActiveSupport::TestCase의 시간 여행(Time Travel) 기능을 활용하여, 실제 대기 시간 없이도 워크플로우의 모든 단계와 지연 로직을 밀리초 단위로 신속하게 테스트할 수 있도록 지원합니다.

결론

geneva_drive는 Temporal과 같은 거대한 시스템의 복잡한 인프라 없이도 Rails 환경에 최적화된 내구성 있는 워크플로우를 제공합니다. 불투명한 백그라운드 작업의 나열 대신 데이터베이스 기반의 가시성과 쿼리 능력을 갖춘 워크플로우 엔티티를 제공함으로써 비즈니스 프로세스의 투명성을 확보합니다. 이는 핀테크 등 고신뢰성이 요구되는 도메인에서 검증된 실무 경험의 산물이며, 추가적인 외부 의존성 없이 Rails 애플리케이션의 아키텍처를 한 단계 격상시킬 수 있는 실무적인 해결책입니다.

댓글 0

댓글 작성

댓글 삭제 시 비밀번호가 필요합니다.

이미 계정이 있으신가요? 로그인 후 댓글을 작성하세요.

0/1000
정중하고 건설적인 댓글을 작성해 주세요.