들어가며: '내구성 있는 실행'이라는 숙제

서버가 크래시 나면 어떻게 될까? 전통적인 아키텍처에서 답은 '상황에 따라 다르다'입니다. 타임아웃이 나서 재시도하면 중복 처리가 발생하고, 부분적인 상태가 꼬여 다음 단계가 망가지기도 합니다. 특히 보험 청구 처리처럼 몇 분에서 며칠까지 걸리는 워크플로우라면 중단은 거의 필연적입니다.

업계는 이 문제를 해결하기 위해 전용 오케스트레이션 클러스터나 클라우드 관리형 워크플로우 서비스를 사용해왔습니다. 하지만 이들 솔루션은 운영 복잡성, 인프라 의존성, 아키텍처 제약이라는 대가를 치러야 했습니다.

Airbnb는 이 문제를 정면돌파하기로 합니다. 외부 의존성 없이 서비스 안에 직접 워크플로우 엔진을 심는 것. 그렇게 탄생한 것이 Skipper입니다.

이 글은 Airbnb 엔지니어링 블로그에 공개된 원문을 바탕으로, 국내 개발자 시각에서 핵심 인사이트와 실무 적용 포인트를 재구성했습니다. (근거자료: 원문 링크)

Airbnb Skipper embedded workflow engine architecture diagram showing durable execution flow Dev Environment Setup

기존 솔루션의 한계: 외부 오케스트레이션과 도메인 로직의 단절

외부 오케스트레이션의 문제점

전용 오케스트레이션 엔진은 '정확히 한 번 실행(exactly-once semantics)'을 보장하고 검증된 안정성을 제공합니다. 하지만 전용 인프라(클러스터 + 영속성 레이어) 가 필요하고, 이는 곧 운영 전문성을 요구합니다.

Airbnb의 'Tier 0' 서비스(사용자 트랜잭션에 직접 영향을 미치는 최고 중요도 서비스) 입장에서, 오케스트레이션 클러스터 장애는 모든 의존 서비스의 워크플로우를 마비시키는 단일 장애점(SPOF) 이 됩니다. 클라우드 관리형 서비스는 운영 부담을 덜어주지만, 벤더 종속, 규제/데이터 처리 문제, 실행 시간 제한이라는 또 다른 리스크를 안깁니다.

도메인 로직의 파편화

더 미묘한 문제는 도메인 로직이 파편화된다는 점입니다. 큐 컨슈머, 스케줄드 잡, 콜백 엔드포인트, 정합성 스크립트에 비즈니스 프로세스가 흩어집니다. 코드상에서 '이 비즈니스 프로세스가 실제로 무엇을 하는지' 한눈에 파악할 수 있는 곳이 없습니다. 게다가 각 조각은 재시도 백오프, 중복 제거 체크, 타임아웃 처리 등 인프라 관심사와 뒤엉킵니다.

국내 SI/스타트업 환경에서도 비슷한 경험을 해보셨을 겁니다. 여러 마이크로서비스에 걸친 보상 트랜잭션을 구현하다 보면, '이 로직이 왜 여기 있지?' 싶은 순간이 꼭 생기거든요. 😅

Java Kotlin workflow code example with annotations for durable execution Algorithm Concept Visual

Skipper의 설계 철학: '내장형'이라는 선택

Skipper는 서비스 안에 라이브러리 형태로 내장(Embedded) 되는 워크플로우 엔진입니다. 다섯 가지 원칙이 설계를 이끌었습니다.

원칙설명
간결한 사용성 (Succinct Ergonomics)워크플로우 코드가 비즈니스 로직 그 자체처럼 읽혀야 함
단일 장애점 제거각 서비스가 독립적으로 워크플로우 처리, 중앙 코디네이터 없음
기존 인프라 활용서비스가 이미 사용 중인 MySQL 등 동일 DB 사용
셀프서비스 통합라이브러리 의존성 추가만으로 사용 가능
성능 중립성 (Performance-Neutral)워크플로우 엔진이 호스트 서비스 확장성을 제약하지 않음

실제 코드로 보는 Skipper

// 1) 일반 타입 호출처럼 실행 (코드젠 클라이언트 불필요)
val out = workflow("reservation:${req.id}").execute(req)

// 2) 워크플로우 로직은 평범한 Kotlin 클래스
class ChargeAndAccept : Workflow() {
    private val billing = actions()
    private val reservations = actions()
    
    @StateParam var paymentCaptured = false
    
    @WorkflowMethod
    suspend fun execute(r0: Reservation): Reservation {
        val r1 = billing.charge(r0) // 내구성 있는 사이드 이펙트 경계
        waitUntil { paymentCaptured } // 내구성 있는 대기 (재시작 후 재개)
        return reservations.markAccepted(r1)
    }
}

// 3) 사이드 이펙트는 Actions에; 하나의 어노테이션으로 체크포인트 가능
class BillingActions : Actions() {
    @Execute(checkpoint = true)
    suspend fun charge(r: Reservation): Reservation =
        billingApi.chargeAsync(r.id, r.amount).await()
}

이 코드는 자연스럽게 읽힙니다: '결제하고, 캡처될 때까지 기다리고, 예약을 승인한다.' 재시도 로직, 큐 관리, 비동기 조정 코드가 전혀 보이지 않습니다.

내구성의 핵심: 리플레이(Replay)

Skipper는 체크포인트된 액션 결과를 DB에 저장하고, 장애 발생 시 워크플로우 메서드를 처음부터 리플레이합니다. 이미 실행된 액션은 재실행되지 않고 체크포인트된 결과를 즉시 반환합니다. waitUntil 같은 대기 구간에서는 현재 상태를 저장하고 워크플로우가 휴면(Hibernate) 상태로 들어가 컴퓨팅 리소스를 전혀 소모하지 않습니다.

해피 패스(Happy Path)에서는 오버헤드가 거의 없습니다. 대부분의 워크플로우 엔진이 모든 실행에 오버헤드를 부과하는 반면, Skipper는 크래시가 발생했을 때만 리플레이 메커니즘이 작동합니다. 이는 지연 시간에 민감하고 높은 처리량이 필요한 서비스에 매우 적합한 설계입니다.

보상 트랜잭션: @Compensate

Skipper는 보상(Compensation)을 일급 시민(First-class citizen) 으로 취급합니다. @Compensate 어노테이션으로 각 액션의 '실행 취소' 메서드를 정의하면, 액션 실패 시 Skipper가 자동으로 역순으로 보상 메서드를 실행합니다. 분산 트랜잭션 없이 궁극적 일관성(Eventual Consistency) 을 달성하는 셈이죠.

class BillingActions : Actions() {
    @Execute(checkpoint = true)
    suspend fun charge(r: Reservation): Reservation { ... }
    
    @Compensate
    suspend fun compensateCharge(r: Reservation) {
        // 결제 취소 로직
        refundApi.refund(r.id, r.amount)
    }
}

Workflow engine comparison table embedded vs external orchestration pros and cons

실전 적용과 한계: 국내 개발자가 주목할 점

생산성과 확장성

Skipper는 1년 넘게 프로덕션에서 운영되며 보험, 결제, 미디어, 인프라 등 15개 이상의 유스케이스를 지원하고 있습니다. 피크 시 Amazon DynamoDB에서 초당 10,000개 워크플로우를 처리할 정도로 확장성이 입증되었습니다.

주의사항 및 한계

  1. 결정론(Determinism) 요구사항: 리플레이 모델은 워크플로우 메서드가 결정론적이어야 합니다. API 호출, 시간 의존 로직, 난수 생성 등은 모두 Actions 안에 있어야 합니다.
  2. 최소 한 번 실행(At-least-once): 액션이 실행된 후 체크포인트 전에 크래시가 나면 중복 실행될 수 있습니다. 액션은 멱등성(Idempotent)을 가져야 합니다.
  3. 워크플로우 진화(Evolution): 실행 중인 워크플로우의 구조를 변경하면 문제가 생깁니다. 버저닝 전략이 필수적입니다.

국내 환경에서는 특히 레거시 시스템과의 통합을 고려해야 합니다. Skipper가 Java/Kotlin에 최적화되어 있어, Node.js나 Python 기반 마이크로서비스가 많은 환경에서는 적용이 까다로울 수 있어요.

다음 단계 학습 방향

  • Temporal.io / Uber Cadence: 외부 오케스트레이션 엔진의 대표주자. Skipper와의 비교를 통해 '내장형 vs 외부형'의 트레이드오프를 이해해보세요.
  • AWS Step Functions / Azure Durable Functions: 클라우드 관리형 서비스의 장단점을 파악하면 Skipper의 설계 결정이 더 선명하게 보입니다.
  • Saga 패턴: 보상 트랜잭션의 개념을 더 깊이 공부하고 싶다면, 분산 트랜잭션에서의 Saga 패턴을 살펴보세요.

Skipper의 핵심 통찰은 '리플레이 기반 실행 + 체크포인트 액션' 으로 조정 서비스 없이도 내구성을 확보할 수 있다는 점입니다. 모든 상황에 완벽한 해법은 아니지만, 의존성을 최소화하면서 내구성 있는 실행이 필요한 서비스라면 충분히 고려해볼 가치가 있습니다.


함께 보면 좋은 글

본 콘텐츠는 신뢰할 수 있는 출처를 바탕으로 AI 도구를 활용하여 초안이 작성되었으며, 편집자의 검토를 거쳐 발행되었습니다. 전문가의 조언을 대체하지 않습니다.