Ranvier 시작하기

버전: 0.17 최근 업데이트: 2026-03-04


필수 요건


1. 첫 번째 Axon 만들기 (5분)

Ranvier는 워크플로우를 타입 안전한 파이프라인으로 모델링합니다. 각 단계는 Transition이고, 이들을 체이닝하면 Axon이 됩니다.

프로젝트 생성

cargo new my-ranvier-app
cd my-ranvier-app
cargo add ranvier-core ranvier-runtime tokio --features tokio/full
cargo add anyhow async-trait serde --features serde/derive
cargo add serde_json

첫 번째 트랜지션 작성

use async_trait::async_trait;
use ranvier_core::prelude::*;
use ranvier_runtime::Axon;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Serialize, Deserialize)]
struct Greeting {
    name: String,
    message: String,
}

#[derive(Clone)]
struct BuildGreeting;

#[async_trait]
impl Transition<String, Greeting> for BuildGreeting {
    type Error = String;
    type Resources = ();

    async fn run(
        &self,
        name: String,
        _resources: &Self::Resources,
        _bus: &mut Bus,
    ) -> Outcome<Greeting, Self::Error> {
        Outcome::Next(Greeting {
            name: name.clone(),
            message: format!("Hello, {}!", name),
        })
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let axon = Axon::<String, String, String>::new("greeting")
        .then(BuildGreeting);

    let mut bus = Bus::new();
    let result = axon.execute("World".to_string(), &(), &mut bus).await;

    match result {
        Outcome::Next(greeting) => println!("{}", greeting.message),
        Outcome::Fault(e) => eprintln!("Error: {}", e),
        _ => {}
    }

    Ok(())
}

핵심 개념

개념 설명
Transition<From, To> 단일 처리 단계: From을 받아 To를 생성
Axon 트랜지션을 체이닝한 파이프라인
Outcome<T, E> 결과 타입: Next(T), Fault(E), Branch, Jump, Emit
Bus 파이프라인을 관통하는 타입 인덱스 기반 역량 저장소
Resources 트랜지션에 주입되는 공유 의존성

실행

cargo run
# 출력: Hello, World!

예제 실행: cargo run -p hello-world (ranvier 워크스페이스 기준)


2. 트랜지션 체이닝 (5분)

트랜지션은 .then()으로 조합합니다:

#[derive(Clone)]
struct ValidateInput;

#[async_trait]
impl Transition<String, String> for ValidateInput {
    type Error = String;
    type Resources = ();

    async fn run(
        &self,
        input: String,
        _resources: &(),
        _bus: &mut Bus,
    ) -> Outcome<String, Self::Error> {
        if input.is_empty() {
            return Outcome::Fault("입력값이 비어있습니다".to_string());
        }
        Outcome::Next(input)
    }
}

let axon = Axon::<String, String, String>::new("pipeline")
    .then(ValidateInput)
    .then(BuildGreeting);

.then()은 단계를 추가합니다. 한 단계의 출력 타입이 다음 단계의 입력 타입과 일치해야 하며, 컴파일러가 빌드 타임에 이를 강제합니다.

예제 실행: cargo run -p typed-state-tree


3. 트랜지션 테스트 (10분)

트랜지션은 일반 비동기 함수이므로 직접 테스트할 수 있습니다:

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_validate_rejects_empty() {
        let validator = ValidateInput;
        let mut bus = Bus::new();
        let result = validator.run("".to_string(), &(), &mut bus).await;
        assert!(result.is_fault());
    }

    #[tokio::test]
    async fn test_full_pipeline() {
        let axon = Axon::<String, String, String>::new("test")
            .then(ValidateInput)
            .then(BuildGreeting);

        let mut bus = Bus::new();
        let result = axon.execute("Alice".to_string(), &(), &mut bus).await;
        match result {
            Outcome::Next(g) => assert_eq!(g.message, "Hello, Alice!"),
            other => panic!("Expected Next, got: {:?}", other),
        }
    }
}

예제 실행: cargo run -p testing-patterns (7개 단위 + 통합 테스트 포함)


4. 커스텀 에러 타입 (10분)

구조화된 에러를 위해 thiserror를 사용합니다:

cargo add thiserror
use thiserror::Error;

#[derive(Debug, Clone, Error, Serialize, Deserialize)]
enum AppError {
    #[error("찾을 수 없음: {0}")]
    NotFound(String),
    #[error("검증 실패: {0}")]
    Validation(String),
    #[error("권한 없음: {0}")]
    Unauthorized(String),
}

#[async_trait]
impl Transition<String, User> for FetchUser {
    type Error = AppError;
    type Resources = ();

    async fn run(
        &self,
        user_id: String,
        _resources: &(),
        _bus: &mut Bus,
    ) -> Outcome<User, Self::Error> {
        if user_id == "unknown" {
            return Outcome::Fault(AppError::NotFound(user_id));
        }
        Outcome::Next(User { id: user_id, name: "Alice".into() })
    }
}

Ranvier는 core prelude에 RanvierError도 제공합니다. Message, NotFound, Validation, Internal 변형을 가진 serde 호환 에러 타입입니다.

예제 실행: cargo run -p custom-error-types


5. Bus 사용하기 (5분)

Bus는 파이프라인을 통해 역량(capability)을 전달하는 타입 인덱스 저장소입니다:

use ranvier_core::prelude::*;

// 역량 정의
#[derive(Clone, Debug)]
struct RequestId(String);

// 실행 전 삽입
let mut bus = Bus::new();
bus.insert(RequestId("req-123".to_string()));

// 트랜지션 내부에서 읽기
async fn run(&self, input: Input, _res: &(), bus: &mut Bus) -> Outcome<Output, Error> {
    let req_id = bus.read::<RequestId>();
    match req_id {
        Some(id) => println!("요청 처리 중: {}", id.0),
        None => println!("요청 ID 없음"),
    }
    Outcome::Next(output)
}

Bus는 insert(), read(), read_mut(), has(), remove()를 지원합니다. 타입은 Any + Send + Sync + 'static이어야 합니다.

예제 실행: cargo run -p bus-capability-demo


6. 공유 리소스 (5분)

모든 트랜지션에 공유되는 의존성(DB 풀, 설정 등)에는 Resources를 사용합니다:

use ranvier_core::transition::ResourceRequirement;

#[derive(Clone)]
struct AppConfig {
    api_key: String,
    max_retries: u32,
}

impl ResourceRequirement for AppConfig {}

#[async_trait]
impl Transition<Request, Response> for CallExternalApi {
    type Error = String;
    type Resources = AppConfig;

    async fn run(
        &self,
        req: Request,
        config: &Self::Resources,
        _bus: &mut Bus,
    ) -> Outcome<Response, Self::Error> {
        println!("API 키 사용: {}", config.api_key);
        Outcome::Next(response)
    }
}

// 실행 시 리소스 전달
let config = AppConfig { api_key: "key".into(), max_retries: 3 };
let result = axon.execute(input, &config, &mut bus).await;

리소스는 ResourceRequirement (마커 트레이트)를 구현해야 합니다. 유닛 타입 ()는 기본 구현을 제공합니다.


7. 장애 복원력: 재시도 & DLQ (10분)

Ranvier는 데드 레터 큐(DLQ) 지원 기본 재시도를 제공합니다:

use ranvier_core::prelude::*;
use ranvier_runtime::Axon;

let axon = Axon::<Input, Input, String>::new("resilient")
    .then(UnreliableStep)
    .with_dlq_policy(DlqPolicy::RetryThenDlq {
        max_attempts: 3,
        backoff_ms: 100,
    })
    .with_dlq_sink(MyDlqSink::new());

max_attempts회 지수 백오프 재시도 후, 실패한 이벤트는 DLQ 싱크로 전송됩니다.

예제 실행: cargo run -p retry-dlq-demo


8. 영속성: 체크포인트 & 재개 (10분)

장기 실행 워크플로우에서는 영속성 레이어로 상태를 체크포인트하고 장애 시 재개합니다:

use ranvier_runtime::{
    InMemoryPersistenceStore, PersistenceHandle,
    PersistenceTraceId, PersistenceAutoComplete,
};
use std::sync::Arc;

let store = Arc::new(InMemoryPersistenceStore::new());
let handle = PersistenceHandle::from_arc(store.clone() as Arc<dyn PersistenceStore>);

// 첫 실행 — auto_complete=false로 장애 시 트레이스를 열어둠
let mut bus = ranvier_core::ranvier_bus!(
    handle.clone(),
    PersistenceTraceId::new("order-123"),
    PersistenceAutoComplete(false),
);
let result = axon.execute(input, &(), &mut bus).await;

예제 실행: cargo run -p state-persistence-demo


9. 다음 단계

난이도별

수준 예제 시간
초급 hello-world, typed-state-tree, bus-capability-demo 각 5분
중급 routing-demo, flat-api-demo, testing-patterns, custom-error-types 각 10-15분
고급 order-processing-demo, retry-dlq-demo, state-persistence-demo, multitenancy-demo 각 15-20분

주제별

주제 예제
HTTP & 라우팅 routing-demo, routing-params-demo, flat-api-demo, session-demo
인증 & 보안 auth-jwt-role-demo, guard-demo
데이터 & 영속성 db-example, persistence-production-demo, ecosystem-redis-demo
관측성 observe-http-demo, otel-demo, otel-concept-demo
장애 복원력 retry-dlq-demo, state-persistence-demo
엔터프라이즈 synapse-demo, job-scheduler-demo, multitenancy-demo

학습 경로

  1. 빠른 시작: hello-world → typed-state-tree → testing-patterns → custom-error-types → routing-demo
  2. HTTP 서비스: routing-params → flat-api → session → multipart-upload → websocket → sse-streaming → openapi → auth-jwt-role
  3. 고급 패턴: order-processing → retry-dlq → state-persistence → multitenancy

예제 실행

# ranvier 워크스페이스 루트에서
cargo run -p <예제-이>

참고 문서