본문 바로가기

BackEnd

파이프라인으로 워크플로 모델링하기 - 워크플로 합성 및 나머지

반응형

서브스텝을 조합하여 워크플로 만들기

이제 모든 단계에 대한 정의가 있습니다. 따라서 각각에 대한 구현이 있을 때 한 단계의 출력을 다음 단계의 입력에 연결하여 전체 워크플로를 구축할 수 있어야 합니다.
하지만 그렇게 간단하지 않을 것입니다! 입력과 출력만 나열되도록 종속성을 제거한 상태에서 모든 단계의 정의를 한 곳에서 살펴보겠습니다.
​ 	​type​ ValidateOrder =
​ 	  UnvalidatedOrder                                       ​// input​
​ 	    -> AsyncResult<ValidatedOrder,ValidationError ​list​>  ​// output​
​ 	
​ 	​type​ PriceOrder =
​ 	  ValidatedOrder                            ​// input​
​ 	    -> Result<PricedOrder,PricingError>     ​// output​
​ 	
​ 	​type​ AcknowledgeOrder =
​ 	  PricedOrder                                ​// input​
​ 	    -> Async<OrderAcknowledgmentSent option> ​// output​
​ 	
​ 	​type​ CreateEvents =
​ 	    PricedOrder               ​// input​
​ 	      -> PlaceOrderEvent ​list​ ​// output​

PriceOrder 단계의 입력에는 ValidatedOrder가 필요하지만 ValidateOrder의 출력은 AsyncResult<ValidatedOrder,ValidationError ​list​>이며 전혀 일치하지 않습니다.

 

마찬가지로 PriceOrder 단계의 출력은 AcknowledgeOrder 등에 대한 입력으로 사용할 수 없습니다.

 

이러한 함수들을 합성하기 위해 입력 및 출력 유형을 호환 가능하게 해야 합니다.

이것은 타입 기반 설계를 수행할 때 일반적인 문제이며 구현 장에서 이를 수행하는 방법을 볼 것입니다.

종속성이 설계의 일부인가요?

위의 코드에서 다른 컨텍스트(예: CheckProductCodeExists 및 ValidateAddress)에 대한 호출을 문서화할 종속성으로 처리했습니다. 각 하위 단계에 대한 설계는 이러한 종속성에 대한 명시적인 추가 매개변수를 추가했습니다.
​ 	​type​ ValidateOrder =
​ 	  CheckProductCodeExists    ​// explicit dependency​
​ 	    -> CheckAddressExists   ​// explicit dependency​
​ 	    -> UnvalidatedOrder     ​// input​
​ 	    -> AsyncResult<ValidatedOrder,ValidationError ​list​>  ​// output​
​ 	
​ 	​type​ PriceOrder =
​ 	  GetProductPrice                        ​// explicit dependency​
​ 	    -> ValidatedOrder                    ​// input​
​ 	    -> Result<PricedOrder,PricingError>  ​// output
프로세스가 작업을 수행하는 방법을 우리에게 숨겨야 한다고 주장할 수도 있습니다.
목표를 달성하기 위해 협력해야 하는 시스템이 무엇인지가 정말 우리의 관심사인가요?
 
이 관점을 취하면 프로세스 정의는 다음과 같이 입력 및 출력만으로 단순화됩니다.
​ 	​type​ ValidateOrder =
​ 	  UnvalidatedOrder                                      ​// input​
​ 	    -> AsyncResult<ValidatedOrder,ValidationError ​list​> ​// output​
​ 	
​ 	​type​ PriceOrder =
​ 	  ValidatedOrder                        ​// input​
​ 	    -> Result<PricedOrder,PricingError> ​// output
어떤 접근 방식이 더 낫습니까? 디자인에 정답은 없지만 다음 가이드라인을 따르도록 합시다.
  • 공개 API에 노출된 함수의 경우 호출자로부터 종속성 정보를 숨깁니다.
  • 내부적으로 사용되는 함수의 경우 종속성을 명시합니다.
최상위 PlaceOrder 워크플로 함수에 대한 종속성은 사용자가 알 필요가 없습니다.
시그니처는 다음과 같이 입력 및 출력을 표시해야 합니다.
​ 	​type​ PlaceOrderWorkflow =
​ 	  PlaceOrder                                             ​// input​
​ 	    -> AsyncResult<PlaceOrderEvent ​list​,PlaceOrderError> ​// output​
그러나 워크플로의 각 내부 단계에 대해선 원래 디자인에서 했던 것처럼 종속성을 명시해야 합니다.
이것은 각 단계에 실제로 필요한 것이 무엇인지 문서화하는 데 도움이 됩니다.
단계에 대한 종속성이 변경되면 해당 단계에 대한 함수 정의를 변경할 수 있으며, 그 결과 구현을 변경해야 합니다.

전체 파이프라인 살펴보기

지금까지 만든 디자인을 살펴봅시다. (전부 다 있지는 않습니다.)
먼저 공개 API의 타입을 보겠습니다. 일반적으로 DomainApi.fs 또는 이와 유사한 파일과 같은 하나의 파일에 모두 저장합니다.

Public API

unvalidated state는 퍼블릭 API에 있습니다.

module DomainApi = 

    open System

    //>CompletePipeline_Api1 
    // ----------------------
    // Input data
    // ----------------------
	
// 입력입니다!!!!!!!!!!!!!!!!!!

    type UnvalidatedOrder = {
       OrderId : string
       CustomerInfo : UnvalidatedCustomer
       ShippingAddress : UnvalidatedAddress
       }
    and UnvalidatedCustomer = {
       Name : string
       Email : string
       }
    and UnvalidatedAddress = DotDotDot

    // ----------------------
    // Input Command
    // ----------------------

    type Command<'data> = {
       Data : 'data
       Timestamp: DateTime
       UserId: string
       // etc
       }
   
    type PlaceOrderCommand = Command<UnvalidatedOrder>  
    //<

// 다음은 출력 및 워크플로 정의 자체입니다.
    //>CompletePipeline_Api2
    // ----------------------
    // Public API
    // ----------------------

    /// Success output of PlaceOrder workflow
    type OrderPlaced = DotDotDot
    type BillableOrderPlaced = DotDotDot
    type OrderAcknowledgmentSent = DotDotDot
    type PlaceOrderEvent =
        | OrderPlaced of OrderPlaced
        | BillableOrderPlaced of BillableOrderPlaced 
        | AcknowledgmentSent  of OrderAcknowledgmentSent

    /// Failure output of PlaceOrder workflow
    type PlaceOrderError = DotDotDot
   
    type PlaceOrderWorkflow = 
      PlaceOrderCommand                                      // input command
        -> AsyncResult<PlaceOrderEvent list,PlaceOrderError> // output events
    //<

내부 단계. (Internal Steps)

내부 단계에서 사용하는 타입을 워크플로별로 별도의 구현 파일(예: PlaceOrderWorkflow.fs)에 넣습니다.
나중에 이 동일한 파일의 맨 아래에 구현을 추가합니다.
내부 단계는 종속성을 명시합니다.
(DotDotDot은 Undefined와 비슷한 의미로 생각합시다.)

unvalidated state는 퍼블릭 API에 있습니다.

module PlaceOrderWorkflow = 

    //>CompletePipeline_Int1
    
    // bring in the types from the domain API module
    open DomainApi

    // ----------------------
    // Order life cycle
    // ----------------------
    
    // validated state        
    type ValidatedOrderLine =  DotDotDot
    type ValidatedOrder = {
       OrderId : OrderId
       CustomerInfo : CustomerInfo
       ShippingAddress : Address
       BillingAddress : Address
       OrderLines : ValidatedOrderLine list
       }
    and OrderId = Undefined
    and CustomerInfo = DotDotDot
    and Address = DotDotDot

    // priced state            
    type PricedOrderLine = DotDotDot
    type PricedOrder = DotDotDot

    // all states combined
    type Order =
       | Unvalidated of UnvalidatedOrder
       | Validated of ValidatedOrder
       | Priced of PricedOrder
       // etc
    //<

    type ProductCode = Undefined
    type Price = Undefined

    //>CompletePipeline_Int2
    // ----------------------
    // Definitions of Internal Steps
    // ----------------------

    // ----- Validate order ----- 

    // services used by ValidateOrder
    type CheckProductCodeExists = 
        ProductCode -> bool

    type AddressValidationError = DotDotDot
    type CheckedAddress = DotDotDot
    type CheckAddressExists = 
        UnvalidatedAddress 
          -> AsyncResult<CheckedAddress,AddressValidationError>
    
    type ValidateOrder = 
        CheckProductCodeExists    // dependency
          -> CheckAddressExists   // dependency
          -> UnvalidatedOrder     // input
          -> AsyncResult<ValidatedOrder,ValidationError list>  // output
    and ValidationError = DotDotDot

    // ----- Price order ----- 

    // services used by PriceOrder
    type GetProductPrice = 
        ProductCode -> Price

    type PricingError = DotDotDot

    type PriceOrder = 
        GetProductPrice      // dependency
          -> ValidatedOrder  // input
          -> Result<PricedOrder,PricingError>  // output

    // etc
    //<
이제 구현을 안내할 준비가 된 모든 타입이 한 곳에 있습니다.

길게 지속되는 워크플로

계속 진행하기 전에 파이프라인에 대한 중요한 가정을 다시 살펴보겠습니다.
원격 시스템에 대한 호출이 있더라도 파이프라인은 몇 초 안에 짧은 시간 내에 완료될 것으로 예상합니다.

 

그러나 이러한 외부 서비스를 완료하는 데 훨씬 더 오랜 시간이 걸린다면 어떻게 될까요?
예를 들어 기계가 아닌 사람이 검증을 수행하고 그 사람이 하루 종일 걸린다면 어떻게 될까요?
또는 가격 책정이 다른 부서에서 수행되고 해당 부서에서도 오랜 시간이 걸린다면 어떻게 될까요?
이러한 것들이 사실이라면 디자인에 어떤 영향을 미칠까요?
먼저 원격 서비스를 호출하기 전에 상태를 저장소에 저장하고, 서비스가 완료되었다는 메시지가 나타날 때까지 기다린 다음 저장소에서 상태를 다시 로드하고 워크플로의 다음 단계를 계속 진행합니다.
이것은 각 단계 사이에 상태를 유지해야 하기 때문에 일반 비동기식 호출을 사용하는 것보다 훨씬 무겁습니다.
오래 걸리는 원격 서비스 호출 결과 대기를 위해 영속성 저장소 사용

이를 통해 원래 워크플로를 이벤트에 의해 각각 트리거되는 더 작고 독립적인 청크로 나누었습니다.

하나의 단일 워크플로가 아니라 일련의 개별 미니 워크플로로 생각할 수도 있습니다.

 

여기서 상태 머신 모델은 시스템에 대해 생각하기 위한 귀중한 프레임워크입니다.
각 단계 전에 주문은 해당 상태 중 하나로 유지되어 스토리지에서 로드됩니다.
미니 워크플로는 주문을 원래 상태에서 새 상태로 전환하고 마지막에 새 상태가 스토리지에 다시 저장됩니다.
상태 머신 관점에서 Saga
이러한 종류의 장기 실행 워크플로를 때때로 Saga라고 합니다.
일반적으로 수작업이 관련된 경우가 많지만, 워크플로를 이벤트로 연결된 분리된 독립 실행형 조각으로 나누고 싶을 때마다 사용할 수도 있습니다.
(마이크로서비스)
이 예에서 워크플로는 매우 간단합니다.
이벤트 및 상태의 수가 증가하고 전환이 복잡해지면 들어오는 메시지를 처리하고 현재 상태를 기반으로 취해야 하는 조치를 결정하는 작업을 담당하는 특수 구성 요소인 프로세스 관리자(사가 오케스트레이터)를 생성해야 할 수도 있습니다.
그러면 프로세스 관리자가 적절한 워크플로를 트리거합니다.

요약

워크플로에 대한 입력, 특히 커맨드를 모델링하는 방법을 문서화하는 것으로 시작했습니다.
그런 다음 상태 머신을 사용하여 수명 주기가 있는 문서 및 기타 엔터티를 모델링하는 방법을 살펴보았습니다.
상태에 대한 새로운 이해를 바탕으로 우리는 워크플로로 돌아가 입력 및 출력 상태를 나타내는 타입을 사용하여 각 하위 단계를 모델링하고 각 단계의 종속성과 이펙트를 문서화하는 데 약간의 노력을 기울였습니다.
지금까지 한 작업들은 전부 코드를 문서화 하기 위함입니다.
반응형