본문 바로가기

BackEnd

DDD 구현 : 모나드로 Result 생성 함수 연결

반응형
Result type을 생성하는 함수끼리 깔끔하게 합성하는 법을 알아봅시다.
다음은 문제의 시각적 표현입니다. 일반 함수는 철도 트랙의 일부로 시각화할 수 있습니다.
이펙트가 없는 순수 함수입니다.

그러나 Result 출력이 있는 함수는 다음과 같이 두 개로 분할되는 철도 트랙으로 시각화할 수 있습니다.

Result 출력이 있는 함수
이러한 종류의 함수를 철도에 비유한 후 스위치 함수라 부를 것입니다. 종종 "모나딕" 함수라고도 합니다.
그렇다면 이러한 "스위치" 함수 중 두 개를 어떻게 연결해야 할까요?
출력이 성공하면 시리즈의 다음 함수로 넘어가고 싶지만, 출력이 오류라면 그림과 같이 우회하고 싶습니다.
출력이 성공하면 시리즈의 다음 함수로 넘어가고 싶지만, 출력이 오류라면 그림과 같이 우회하고 싶습니다.
두 오류 트랙이 모두 연결되도록 이 두 스위치를 어떻게 결합합니까? 다음과 같이 분명합니다.

오류 트랙의 연결

그리고 이러한 방식으로 파이프라인의 모든 단계를 연결하면 다음과 같은 "2트랙" 오류 처리 모델 즉 "철도 지향 프로그래밍"을 얻을 수 있습니다.

모든 오류 트랙의 연결

이 접근 방식에서 상단 트랙은 행복한 경로이고 하단 트랙은 실패 경로입니다.
성공 트랙에서 시작하여 운이 좋다면 끝까지 유지합니다.
그러나 오류가 있는 경우 실패 트랙으로 이동하고 파이프라인의 나머지 단계를 건너뜁니다.
함정이 있습니다.
2트랙 출력의 타입이 1트랙 입력 타입과 동일하지 않기 때문에 이러한 종류의 Result 생성 함수를 합성할 수 없습니다.
출력은 2, 입력은 1
2트랙 출력을 1트랙 입력에 어떻게 연결할 수 있습니까?
두 번째 함수에 2트랙 입력이 있는 경우 연결하는 데 문제가 없음을 관찰해 보겠습니다.

두 번째 함수에 2트랙 입력이 있는 경우 연결하는 데 문제가 없습니다.

따라서 하나의 입력과 두 개의 출력이 있는 "스위치" 기능을 2트랙 기능으로 변환해야 합니다.

 

스위치 함수용 슬롯이 있고
스위치 함수를 2트랙 함수으로 변환하는 특수 "어댑터 블록"을 생성해 보겠습니다.
스위치 함수를 2트랙 함수으로 변환하는 특수 "어댑터 블록"을 생성해 보겠습니다.
그런 다음 모든 단계를 2트랙 함수로 변환하면 멋지게 합성할 수 있습니다.
그런 다음 모든 단계를 2트랙 함수로 변환하면 변환된 후에 함께 멋지게 합성할 수 있습니다.
최종 결과는 우리가 원하는 대로 "성공" 트랙과 "실패" 트랙이 있는 2트랙 파이프라인입니다.

어댑터 블록 구현

우리는 앞에서 "함수 어댑터"의 개념에 대해 논의했습니다.
스위치 함수를 2트랙 함수로 변환하는 어댑터는 함수형 프로그래밍 툴킷에서 매우 중요한 어댑터입니다.
일반적으로 FP 용어로 bind 또는 flatMap이라고 합니다. 의외로 구현하기 쉽습니다.
로직은 다음과 같습니다.
  • 입력은 "스위치" 함수입니다. 출력은 2트랙 입력과 2트랙 출력이 있는 람다로 표현되는 새로운 2트랙 전용 함수입니다.
  • 2트랙 입력이 성공하면 해당 입력을 스위치 함수에 전달합니다. 스위치 함수의 출력은 2트랙 값이므로 더 이상 수행할 필요가 없습니다.
  • 2트랙 입력이 실패하면 스위치 함수를 우회하고 실패를 반환합니다.

코드의 구현은 다음과 같습니다.

​ 	​let​ bind switchFn  =
​ 	  ​fun​ twoTrackInput ->
​ 	    ​match​ twoTrackInput ​with​
​ 	    | Ok success -> switchFn success
​ 	    | Error failure -> Error failure​

더 일반적인 구현은 바인딩할 두 개의 입력 매개변수("스위치" 기능과 2트랙 값(Result))를 갖고 다음과 같이 람다를 제거하는 것입니다.

​ 	​let​ bind switchFn twoTrackInput =
​ 	  ​match​ twoTrackInput ​with​
​ 	  | Ok success -> switchFn success
​ 	  | Error failure -> Error failure​
두 번째 구현은 커링될 때 첫 번째 구현과 동일합니다.

또 다른 유용한 어댑터 블록은 단일 트랙 함수를 2트랙 기능으로 변환하는 것입니다.

단일 입출력 함수를 투트랙으로 변환

일반적으로 FP 용어로 맵이라고 합니다. 논리는 다음과 같습니다.
  • 입력은 1트랙 과 2트랙 값(Result)입니다.
  • 입력 결과가 성공하면 해당 입력을 1트랙 함수에 전달하고 출력을 Ok로 래핑하여 다시 Result로 만듭니다(출력이 2트랙이어야 하기 때문에).
  • 입력 결과가 실패인 경우 이전과 같이 함수를 무시합니다.

코드의 구현은 다음과 같습니다.

즉, Switch 함수는 생성자입니다.
​ 	​let​ map f aResult =
​ 	  ​match​ aResult ​with​
​ 	  | Ok success -> Ok (f success)
​ 	  | Error failure -> Error failure
bind, map 및 기타 유사한 함수를 사용하면 모든 종류의 일치하지 않는 함수를 합성하는 데 사용할 수 있는 강력한 툴킷을 갖게 됩니다.

Organizing the Result Functions (Result 함수 구성하기)

코드 구성에서 이러한 새로운 함수를 어디에 배치해야 합니까?
표준 접근 방식은 타입과 이름이 같은 모듈(이 경우 Result)에 넣는 것입니다. 그러면 모듈은 다음과 같이 보일 것입니다.
module Result =

    /// Define the Result type
    type Result<'Success,'Failure> = 
       | Ok of 'Success
       | Error of 'Failure
    
    //>Bind
    let bind switchFn twoTrackInput = 
      match twoTrackInput with 
      | Ok success -> switchFn success
      | Error failure -> Error failure
    //<

    //>Map
    let map f aResult = 
       match aResult with 
       | Ok success -> Ok (f success)
       | Error failure -> Error failure
    //<

    //>MapError
    let mapError f aResult = 
       match aResult with 
       | Ok success -> Ok success
       | Error failure -> Error (f failure)
    //<
Result 및 관련 함수는 도메인의 모든 곳에서 사용되기 때문에
일반적으로 새 유틸리티 모듈(예: Result.fs)을 만들고 프로젝트 구조에서 도메인 타입 앞에 배치합니다.
 

합성과 타입 체킹

우리는 "스위치" 함수를 "투트랙" 함수로 변환하여 함수의 "모양"을 일치시키는 데 중점을 두었습니다.
타입 체킹도 진행 중이므로 합성이 작동하려면 타입도 일치하는지 확인해야 합니다.
 
성공 브랜치에서 한 단계의 출력 타입이 다음 단계의 입력 타입과 일치하는 한 트랙을 따라 타입이 변경될 수 있습니다.
예를 들어 아래 세 함수는 FunctionA의 출력(Bananas)이 FunctionB의 입력과 일치하고 FunctionB의 출력(Cherry)이 FunctionC의 입력과 일치하기 때문에 bind를 사용하여 파이프라인으로 구성할 수 있습니다.
​ 	​type​ FunctionA = Apple -> Result<Bananas,...>
​ 	​type​ FunctionB = Bananas -> Result<Cherries,...>
​ 	​type​ FunctionC = Cherries -> Result<Lemon,...>​

bind 함수는 다음과 같이 사용됩니다.

​ 	​let​ functionA : FunctionA = ...
​ 	​let​ functionB : FunctionB = ...
​ 	​let​ functionC : FunctionC = ...
​ 	
​ 	​let​ functionABC input =
​ 	  input
​ 	  |> functionA
​ 	  |> Result.bind functionB
​ 	  |> Result.bind functionC
반면에 FunctionA와 FunctionC는 타입이 다르기 때문에 bind를 사용해도 직접 합성할 수 없습니다.

일반 오류 타입으로 변환

각 단계에서 타입이 변경될 수 있는 성공 트랙과 달리 오류 트랙은 트랙 전체에 걸쳐 동일한 균일 타입을 갖습니다.
즉, 파이프라인의 모든 함수는 동일한 오류 타입을 가져야 합니다.
 
많은 경우, 이는 오류 type을 조정하여 서로 호환되도록 해야 함을 의미합니다.
이를 위해 map과 유사하지만 실패 트랙의 값에 작용하는 함수를 생성해 보겠습니다.
이 함수는 mapError라고 하며 다음과 같이 구현됩니다.
​ 	​let​ mapError f aResult =
​ 	  ​match​ aResult ​with​
​ 	  | Ok success -> Ok success
​ 	  | Error failure -> Error (f failure)
예를 들어 AppleError와 BananaError가 있고 이들을 오류 타입으로 사용하는 두 개의 함수가 있다고 가정해 보겠습니다.
​ 	​type​ FunctionA = Apple -> Result<Bananas,AppleError>
​ 	​type​ FunctionB = Bananas -> Result<Cherries,BananaError>
오류 타입의 불일치는 FunctionA 및 FunctionB를 합성할 수 없음을 의미합니다. 우리가 해야 할 일은 AppleError와 BananaError를 둘 다 포용할 수 있는 새로운 타입을 만드는 것입니다. 초이스 타입이지요. 이것을 FruitError라고 합시다:)
​ 	​type​ FruitError =
​ 	  | AppleErrorCase ​of​ AppleError
​ 	  | BananaErrorCase ​of​ BananaError
그런 다음 functionA를 다음과 같은 FruitError의 결과 타입으로 변환할 수 있습니다.
​ 	​let​ functionA : FunctionA = ...
​ 	
​ 	​let​ functionAWithFruitError input =
​ 	  input
​ 	  |> functionA
​ 	  |> Result.mapError (​fun​ appleError -> AppleErrorCase appleError)
이것은 다음과 같이 단순화할 수 있습니다.
 	​let​ functionAWithFruitError input =
​ 	  input
​ 	  |> functionA
​ 	  |> Result.mapError AppleErrorCase
변환 다이어그램은 다음과 같습니다.
mapError
map과 비교

functionA와 functionAWithFruitError의 시그니처를 보면 우리가 원하는 대로 오류 케이스에서 서로 다른 타입이 있음을 알 수 있습니다.

​ 	​// type of functionA​
​ 	Apple -> Result<Bananas,AppleError>
​ 	
​ 	​// type of functionAWithFruitError​
​ 	Apple -> Result<Bananas,FruitError>
마찬가지로 functionB의 오류 사례를 BananaError에서 FruitError로 변환할 수도 있습니다.
모두 합치면 코드는 다음과 같을 것입니다.
​ 	​let​ functionA : FunctionA = ...
​ 	​let​ functionB : FunctionB = ...
​ 	
​ 	​// convert functionA to use "FruitError"​
​ 	​let​ functionAWithFruitError input =
​ 	  input |> functionA |> Result.mapError AppleErrorCase
​ 	
​ 	​// convert functionB to use "FruitError"​
​ 	​let​ functionBWithFruitError input =
​ 	  input |> functionB |> Result.mapError BananaErrorCase
​ 	
​ 	​// and now we can compose the new versions with "bind"​
​ 	​let​ functionAB input =
​ 	  input
​ 	  |> functionAWithFruitError
​ 	  |> Result.bind functionBWithFruitError
functionAB의 서명은 다음과 같습니다.
​val​ functionAB : Apple -> Result<Cherries,FruitError>

 

반응형