Reddit

Railway-Oriented Programming

철도 지향 프로그래밍(Railway-Oriented Programming)이 오류를 다루는 방식

Railway-Oriented Programming(ROP)은 각 단계를 성공 경로와 실패 경로로 나눠 오류 흐름을 명시적으로 다루는 방법입니다. 글은 Kotlin의 Result 구현으로 map과 flatMap, 실패 복구를 설명하고, 언어의 타입 기능에 따라 적용 방식이 달라진다고 짚습니다.

AI 요약

프로그램에는 외부 입력과 I/O뿐 아니라 개발자가 예상하지 못한 오류도 생깁니다. 이 글은 예외 처리와 방어 코딩을 살펴본 뒤, 성공과 실패를 값으로 표현해 흐름을 관리하는 Railway-Oriented Programming(ROP)을 설명합니다. ROP는 Scott Wlaschin이 F# for Fun and Profit에서 소개한 방법론입니다.

오류를 다루는 여러 방식

글은 먼저 LBYL(Look Before You Leap)과 EAFP(Easier to Ask for Forgiveness than Permission)를 비교합니다. LBYL은 빈 리스트인지 먼저 확인한 뒤 값을 꺼내는 식으로, 예상 가능한 조건을 분기문으로 검사합니다. EAFP는 우선 작업을 시도하고 예외가 나면 처리합니다. 글은 어느 한쪽이 항상 낫다고 보지 않고 상황에 맞게 쓰는 방식이라고 설명합니다.

순수 함수(pure function)는 같은 입력에 항상 같은 결과를 돌려주므로 동작을 예측하기 쉽습니다. 다만 외부 I/O처럼 프로그램 바깥과 맞닿는 문제까지 없애지는 못합니다. 방어 조건을 함수 앞쪽에 모으는 가드 절(guard clause)은 중첩 분기를 줄이는 방법으로 소개합니다. try-catch는 오류 발생 지점과 처리 지점이 떨어져 흐름을 따라가기 어렵고, 어떤 예외가 발생하는지 호출자가 미리 알아야 한다는 부담이 있습니다. 그렇지만 서버처럼 프로그램이 오류 한 번에 종료되면 안 되는 곳에서는 유용하다고 설명합니다.

Result로 성공과 실패를 값으로 표현하기

글은 함수의 입력과 반환값을 타입으로 나타내는 함수형 프로그래밍 관점에서 설명을 이어갑니다. 나눗셈처럼 오류가 발생할 수 있는 함수는 반환 범위를 일반 정수만으로 표현하기 어렵습니다. 이를 위해 성공값과 오류값을 담는 별도 타입을 만들 수 있습니다. 글은 null 여부를 나타내는 Option의 Some과 None, 오류 여부를 나타내는 Result의 Success와 Failure를 Kotlin으로 구현합니다.

값을 변환하는 map은 성공값에 함수를 적용하고 결과를 다시 Result에 담습니다. 그런데 변환 함수 자체가 Result를 반환하면 Result 안에 Result가 들어가는 중첩이 생깁니다. flatMap은 변환 결과를 다시 감싸지 않고 그대로 이어 붙여 중첩을 줄입니다. 글은 리스트에서 map이 List<List<T>>를 만들 수 있는 반면 flatMap은 결과를 평탄화한다는 익숙한 예로 이 차이를 풉니다.

성공·실패·복구 경로

ROP에서는 각 단계가 순서대로 실행되고, 단계마다 성공 또는 실패로 갈라집니다. 실패를 처리해 성공 흐름으로 되돌리는 복구 경로도 둡니다. 글은 이를 성공 경로, 실패 경로, 복구 경로 세 가지로 설명합니다. Result에 recover를 추가하면 오류 종류에 따라 대체값을 돌려줄 수 있습니다. 오류를 sealed class의 하위 타입으로 구분하면 when 분기에서 각 경우를 처리하고, 컴파일러가 빠진 경우를 확인하게 할 수도 있습니다.

앞선 단계의 값을 뒤 단계에서 계속 참조하면 flatMap만으로도 코드가 중첩될 수 있습니다. Scala의 for-comprehension은 모나드 연산을 이어 쓰는 문법 설탕으로 이 중첩을 줄입니다. 글은 Kotlin에서는 Arrow의 기능을 참고할 수 있다고 덧붙입니다. Result와 Option처럼 여러 모나드를 함께 쓰는 상황에는 Higher-Kinded Types(HKT)와 모나드 변환기(monad transformer)가 도움이 되지만, 언어마다 지원 수준이 다릅니다. 그래서 모든 함수에 Result를 씌우기보다 필요한 곳에 제한해 쓰라고 권합니다.

Reddit 반응

  • @u/gruengle — 이 개념이 다시 퍼지는 건 반갑습니다. 다만 원래 만든 Scott Wlaschin을 언급하지 않았고, FSharp for Fun and Profit의 원문 링크도 없는 점은 아쉽습니다.
    • @u/kciter — 제 실수입니다. 처음부터 출처를 밝혔어야 합니다. Scott Wlaschin의 이름과 원문 링크를 글에 추가했습니다.
  • @u/Ghi102 — 부동소수점 함수 예제도 순수 함수입니다. 같은 입력을 주면 몇 번 호출하든 같은 결과가 나옵니다. 부동소수점 구현에 까다로운 점이 있다는 건 별개의 문제입니다. 순수 함수가 이해하기 쉬워야 하는 건 아니고, 함수가 외부 의존성을 갖지 않는다는 뜻입니다.
  • @u/Har-Har-Mahadev — 원작자를 먼저 밝혀야 하지 않나요? https://fsharpforfunandprofit.com/rop/
  • @u/loopis4 — 래더 로직으로 끝내면 안 되나요?
    • @u/TheBananaKart — 래더와 Function Block Diagram은 각자 용도에 맞는 훌륭한 언어입니다. Instruction List는 욕을 먹어도 마땅합니다.
  • @u/Wise_Ambassador4504 — 시스템 설계에서 배운 개념 중 가장 좋은 것 하나입니다.
  • @u/Evening-Gur5087 — 새 글을 읽다가 결국 오래된 개념에 새 이름을 붙여 다시 설명한 내용인 걸 보게 되는 일이 가끔 있습니다. 하하.
  • @u/miyakohouou — 유용한 기법도 있지만, 모나드 코드가 잘 맞지 않는 언어에 그 형식을 억지로 가져오려는 경우가 있다고 느낍니다. 이 방식은 개발자가 추상화를 받아들여야 하는데, 많은 언어는 그 추상화를 제대로 표현하기 어렵습니다. 고차 종류 타입(Higher-Kinded Types)을 제대로 지원하는 언어에서는 모든 펑터나 모나드에 공통으로 적용되는 코드를 작성할 수 있어 추상화에 들인 비용을 만회하기 쉽습니다. 그런 지원이 없으면 추상화라기보다 마찰을 계속 드러내는 설계 패턴에 그칩니다. ROP의 기본 생각은 여전히 타당하지만, 타입 표현력과 사용성이 부족한 언어에서는 Rob Pike의 ‘Errors are Values’라는 관점이 더 유용하다고 봅니다.
  • @u/-Redstoneboi- — ROP라는 약칭은 이미 Return Oriented Programming에 쓰입니다. 이건 모나드 오류 처리, 즉 ‘Errors are Values’와 대수적 데이터 타입(Algebraic Data Types)을 결합한 방식입니다.
    • @u/Proper-Ape — 글쓴이가 이 이름을 만든 건 아닙니다. 서로 다른 분야에서 약칭을 공유할 수도 있습니다. Scott Wlaschin의 F# 연재에서 나온 이름이고, 머릿속에 비유를 만들어 주는 좋은 이름입니다. 그 연재의 요점 중 하나는 함수형 프로그래밍의 어려워 보이는 수학 용어를 잊어도 된다는 것입니다. 개념 자체는 간단하고 더 편하게 코드를 작성하게 해줍니다.
  • @u/mristic — Rust를 막 배우기 시작했는데 F#처럼 패턴 매칭을 지원하고 Result 타입도 기본으로 제공합니다. Rust 생태계에서도 관련 있는 방식인가요? Rust 개발자들이 업무용 프로젝트에서 이 패턴을 쓰는지 궁금합니다.
  • @u/Meleneth — 초반 EAFP 예제는 꼭 수정해야 합니다. Exception을 잡아 null을 반환하는 건 심각한 안티패턴입니다. ‘리스트가 비어 있음’과 ‘예상치 못한 오류가 발생함’을 구분하지 못하게 만들고 진단 정보도 버립니다. 코드가 자라면 새 버그가 같은 catch 블록에 잡혀 정상 결과처럼 조용히 사라질 수도 있습니다. 디버깅할 때 특히 괴롭습니다. 의미 있게 처리할 수 있는 구체적인 예외만 잡고, 예상하지 못한 오류는 전파해야 합니다. EAFP를 설명하면서 임의의 예외를 삼키는 방식을 정상화하면 안 됩니다.
  • @u/greenergarlic — Result 타입 하나를 설명하는 데 정말 많은 수고를 들였네요. 이 타입이 왜 유용한지 이해하는 데 모나드나 함수형 프로그래밍을 알 필요는 없습니다.
  • @u/Finchyy — Ruby on Rails에서 dry-operation을 써서 업무에 이 개념을 적용해 왔습니다. 작은 결정은 순수 함수로, 큰 결정은 명시적으로 다루려는 점이 좋습니다. 매번 ‘이 함수가 부작용을 만들까? 무엇이 잘못될 수 있을까? 실패를 방어해야 할까? 실패 경로는 어떻게 처리할까?’를 생각하게 합니다. 다만 그 결과 단순하게 쓸 수도 있는 상황에서 순수 함수가 길게 이어지기도 합니다. 특히 마법 같은 기능을 선호하는 Rails에서는 그렇습니다. 그래서 예외 처리가 복잡하고 오류 허용도가 거의 없는 작업에만 쓰고 있습니다. 이 방식을 사용하는 다른 Rails 프로젝트가 있는지 궁금합니다.

원문: kciter.so / 번역·요약: Trawling