#Ranvier 시작하기
버전: 0.43.0 최근 업데이트: 2026-03-26 적용 대상: ranvier, ranvier-core, ranvier-macros, ranvier-runtime 카테고리: 시작하기
#필수 요건
- Rust 툴체인 (1.85+):
rustup,cargo - Git
#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 워크스페이스 기준) GitHub에서 소스 보기
#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-treeGitHub에서 소스 보기
#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개 단위 + 통합 테스트 포함) GitHub에서 소스 보기
#4. 커스텀 에러 타입 (10분)
구조화된 에러를 위해 thiserror를 사용합니다:
cargo add thiserroruse 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-typesGitHub에서 소스 보기
#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.get_cloned::<RequestId>();
match req_id {
Ok(id) => println!("요청 처리 중: {}", id.0),
Err(_) => println!("요청 ID 없음"),
}
Outcome::Next(output)
}Bus는 insert(), read(), read_mut(), has(), remove()를 지원합니다. 타입은 Any + Send + Sync + 'static이어야 합니다.
예제 실행:
cargo run -p bus-capability-demoGitHub에서 소스 보기
#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-demoGitHub에서 소스 보기
#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-demoGitHub에서 소스 보기
#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 |
#학습 경로
- 빠른 시작: hello-world → typed-state-tree → testing-patterns → custom-error-types → routing-demo
- HTTP 서비스: routing-params → flat-api → session → multipart-upload → websocket → sse-streaming → openapi → auth-jwt-role
- 고급 패턴: order-processing → retry-dlq → state-persistence → multitenancy
#예제 실행
# ranvier 워크스페이스 루트에서
cargo run -p <예제-이름>#다음 단계
상황에 맞는 경로를 선택하세요:
- 간단한 API를 만들고 있다면? 퀵스타트 또는 TODO API 튜토리얼을 계속하세요.
- 복잡한 비즈니스 로직이 필요하다면? 결제 파이프라인 심화 튜토리얼을 따라가 보세요. Saga 보상, 분기, Schematic 검사까지 7단계로 익힐 수 있습니다.
- 기존 Axum 프로젝트에 통합하려면? Axum + Ranvier 하이브리드 가이드를 읽어보세요.
#참고 문서
- 퀵스타트 -- 5분 만에 첫 HTTP API 만들기
- Hello World 튜토리얼 -- CLI로 프로젝트 생성
- 튜토리얼: TODO API -- 완전한 CRUD API 구축
- 결제 파이프라인 튜토리얼 -- 엔드투엔드 Saga, 분기, 구조 검사
- Axum + Ranvier 하이브리드 가이드 -- Axum 핸들러에서 Ranvier 사용
- Bus 접근 패턴 -- 올바른 Bus 메서드 선택
- 패턴 카탈로그 -- 12가지 재사용 가능한 패턴