본문으로 건너뛰기

ROP 참조

Railway Oriented Programming(ROP)은 Scott Wlaschin이 제안한 함수형 에러 처리 패턴입니다. 모든 함수가 성공(Success) 또는 실패(Failure) 트랙을 반환하며, 실패가 발생하면 이후 단계를 건너뛰고 실패 트랙으로 전파됩니다. Functorium의 FinResponse<A>는 이 ROP 패턴을 C#에서 구현한 것입니다.


성공 트랙: ──── f1 ────── f2 ────── f3 ────── 결과
│ │ │
실패 트랙: ──────────────────────────────────── 에러

모든 함수는 두 개의 출력을 가집니다:

  • 성공(Succ): 다음 함수로 값을 전달
  • 실패(Fail): 나머지 함수를 건너뛰고 에러를 전파

각 함수는 “스위치(분기기)“처럼 동작합니다:

입력 → [함수] → 성공 출력 (다음 단계로)
└→ 실패 출력 (실패 트랙으로)

// 성공 트랙
FinResponse<A>.Succ(value) // value를 담고 있음
// 실패 트랙
FinResponse<A>.Fail(error) // error를 담고 있음

FinResponse<A>는 항상 둘 중 하나의 상태입니다. C#의 Discriminated Union으로 두 트랙을 표현합니다.

result.Match(
Succ: value => $"성공: {value}", // 성공 트랙 처리
Fail: error => $"실패: {error}"); // 실패 트랙 처리

Match는 현재 어떤 트랙에 있는지에 따라 다른 함수를 실행합니다.

FinResponse<int> length = name.Map(s => s.Length);
성공 트랙: ──── "hello" ──── [Map: s.Length] ──── 5
실패 트랙: ──── error ──────────────────────────── error (그대로 통과)

Map은 성공 트랙의 값만 변환합니다. 실패 트랙에 있으면 함수를 실행하지 않고 에러를 그대로 전달합니다.

FinResponse<User> user = userId
.Bind(id => FindUser(id)) // FindUser가 실패할 수 있음
.Bind(u => ValidateUser(u)); // ValidateUser가 실패할 수 있음
성공 트랙: ──── id ──── [FindUser] ──── user ──── [ValidateUser] ──── validUser
│ │
실패 트랙: ──────────────── error ──────────────────── error

Bind는 스위치 함수를 연결합니다. 각 단계에서 실패하면 이후 단계를 건너뜁니다.

C#의 LINQ query syntax를 사용하면 Bind 체인을 읽기 쉽게 작성할 수 있습니다:

// LINQ 구문 (읽기 쉬움)
var result =
from request in Validate(input)
from product in CreateProduct(request)
from saved in SaveProduct(product)
select saved;
// 위는 아래와 동일
var result = Validate(input)
.Bind(request => CreateProduct(request))
.Bind(product => SaveProduct(product));

이것이 가능한 이유는 FinResponse<A>SelectSelectMany를 구현하기 때문입니다:

public FinResponse<B> Select<B>(Func<A, B> f) => Map(f);
public FinResponse<C> SelectMany<B, C>(
Func<A, FinResponse<B>> bind,
Func<A, B, C> project) =>
Bind(a => bind(a).Map(b => project(a, b)));

Mediator Pipeline도 ROP와 유사한 구조를 가집니다:

요청 → [Validation] → [Logging] → [Tracing] → [Handler] → 응답
│ │ │ │
└──── Fail ──┴─── Fail ──┴─── Fail ──┘

각 Pipeline은 스위치처럼 동작합니다:

  • 성공: 다음 Pipeline(next())을 호출
  • 실패: TResponse.CreateFail(error)로 실패 응답을 반환하고, 이후 Pipeline을 건너뜀

두 타입은 모두 ROP를 지원하지만, Pipeline 제약 가능 여부에서 결정적으로 다릅니다.

항목Fin<T> (LanguageExt)FinResponse<A> (Functorium)
타입sealed structabstract record
Pipeline 제약불가 (sealed struct)가능 (인터페이스 구현)
ROP 메서드Match, Map, BindMatch, Map, Bind
LINQ 지원OO
변환-Fin<A>.ToFinResponse()
팩토리Fin.Succ, Fin.FailFinResponse.Succ, FinResponse.Fail

Fin<T>는 Repository 계층에서, FinResponse<A>는 Usecase/Pipeline 계층에서 사용합니다. ToFinResponse() 확장 메서드로 두 계층을 연결합니다.



이 튜토리얼에서 사용하는 제네릭 변성, 타입 시스템, 함수형 프로그래밍, 아키텍처 관련 핵심 용어를 정리합니다.

부록 D: 용어집