그래서, ROP가 뭔데 씹덕아 (Railway oriented programming)

아프니까병원이다
Mar 15 · 13 min read

나다, 오늘은 ROP에 대해 알아보도록 하겠다.

서버에서 “유저의 이름과 이메일을 업데이트 해주세요” 라는 요구 사항이 있다고 생각해보자 개발자는 다음의 요소들을 떠올리며 코딩을 시작하게 될것이다. (그리고 아무런 오류 없이 잘 동작할 것이라고 생각하고 행복회로를 가동한다)

  • Request 받기
  • Request 검증 / 정규화
  • 로그 남기기
  • DB에 Update
  • 결과 리턴

대충 코드로 간단하게 표현해보자면 이런 식이 될 것이다.

interface Input {
name: string;
email: string;
}

function app(input: Input) {
validateInput(input);
canonicalizeEmail(input);
console.log(input);
db.updateUser(input);
return 'Success';
}

예외처리 없이 이렇게만 됐으면 얼마나 좋겠나 싶지만, 현실은 알다시피 시궁창이다. 갖가지 상황에서 오만가지의 오류가 일어난다. 개발자의 행복회로는 불타고 만다. 슬픈 상황이 벌어지고 만다는 것이다.

“A program is a spell cast over a computer, turning input into error messages”

아까 적어 놨던 요소들을 다시 보자. 실제로 서버에선 각 요소마다 여러가지 예외적인 상황들이 발생할 것이다.

  • Request 받기
  • Request 검증 / 정규화
    ❌ Name is blank
    ❌ Invalid email
  • 로그 남기기
  • DB에 Update
    🚨 User not found
    ❌ DB Error
  • 결과 리턴

Request를 검증 하는 도중, 이름이 없을 수도 있고, 이메일이 올바르지 못할 수도 있다. 막상 DB에 업데이트를 하려고 하니, 해당하는 유저가 없을 수도 있고, DB자체가 오류가 날 수 도 있다.

그래서 실제로는 코드가 이렇게 변한다.

이랬던 코드가

validateInput 에 대한 예외처리가 생기고

DB와 관련된 예외처리가 생기고

DB에 대한 예외처리가 또 추가 된다

다들 한번쯤은 겪어 봤을 일이다.

너무 슬픈 일이다. 원래 5줄이었던 코드는 15줄의 코드로 늘어났고, 200% 가량 코드가 불어나 버렸다.

이러한 현상은 심심치 않게 매일같이 벌어지는 현상이다.

이제, 함수형 코드에서는 이러한 것들이 어떻게 될지, 이런 슬픈 일들이 반복 될지 알아보도록 하겠다.

처음에 작성했었던(에러 처리가 없는) 코드를 함수형으로 표현해보면 이렇게 될 것이다.

const app = pipe(
validateInput,
canonicalizeEmail,
logInput,
updateDb,
returnMessage,
)

그리고 에러 처리가 들어간 함수형 코드는 이렇게 된다.

const app = pipe(
validateInput,
canonicalizeEmail,
logInput,
updateDb,
returnMessage,
)

(대충 물음표 짤)

똑같다. 함수형 프로그래밍을 하게되면 코드의 아름다운 모습을 지키면서도 에러 처리도 가능하게 된다.

함수형 프로그래밍

함수란 무엇일까?

함수는,

  • 하나의 Input을 받아서
  • 뭔가 한다음
  • 하나의 Output을 리턴한다

이것이 함수의 정의인데 좀더 정확히는 “순수함수”가 좀 더 함수형 프로그래밍에서 설명하는 함수에 적합하다.

순수함수란,

  • Input이 같으면 Output도 같아야 한다
  • 1을 넣으면 2가 나오는 함수는 어떤 상황에서든 1을 넣으면 2가 나와야 한다

이렇게 순수함수를 사용하게되면 다음과 같은 이점을 얻을 수 있다.

  • 캐싱 가능
    ➡️ 미리 연산을 해놓고 캐싱해놓으면, 다음에도 같은 값이 들어올때 캐싱한 값을 리턴하면 된다
  • 매우 쉬운 테스팅
    ➡️ 한 함수에 한가지 목적을 가진 로직만 있으므로, 좀더 엄격한 테스트를 짤 수 있다.

하지만 일반적으로 순수 함수로만 코드를 구성하는건 생각보다 지키기 어렵다.

숨겨진 Input

우리가 일반적으로 짠 코드들에는 숨겨진 Input이 존재한다.

대표적인 것이 바로 this 다. Input이 같아도 this.data 가 변경되었다면 다른 Output이 나올 것이다. 그래서 this.data도 Input이나 다름 없게 된다.

그리고 전역 변수, 예로 환경변수를 들 수 있을 것 같다. 환경 변수에 따라 결과가 바뀐다면 환경 변수도 숨겨진 Input 이다.

마지막으로 I/O, 가장 일반적인 상황이 될텐데, 함수 안에서 DB에 쿼리를 한다면, 쿼리 결과도 숨겨진 Input 될 것이다.

숨겨진 Output

숨겨진 Input이 있다면, 숨겨진 Output도 있다.

대표적으로 throw new Error 가 있는데, 숨겨진 Output인 이유는 리턴 타입에 명시되지 않는다는 점 때문이다.

이게 무슨의미 인가 하면, VS Code 에서 이 함수의 정의를 보려고 함수에 마우스 올려 본다고 하자. 이때, 뜨는 정보에서는 throw new Error 에 대한 정보가 전혀 나오지 않아, 이 함수에서 에러가 날 가능성이 있는지 전혀 알 수 없다.

그래서 Railway oriented programming 은 에러도 타입에 포함시켜 처리한다. 그리고 이것은 Railway oriented programming의 핵심이다.

함수형 프로그래밍으로 나아가기

Javascript(or Typescript)에서 함수형 프로그래밍으로 나아가기 위해서는 다음과 같은 조건이 필요하다.

  • let이 없는 코드
  • this가 없는 코드
  • throw가 없는 코드
    ✨ Railway oriented programming의 핵심

이러한 원칙을 지키게 되면, 함수형으로 짜고 싶지 않아도, 자연스럽게 함수형 코드가 되어버린다.

let이 없는 코드가 어떻게 함수형으로 변하는지 알아보자

위의 코드가 let을 사용한 일반적인 절차지향적인 Javascript 코드, 밑의 코드가 let이 없는 Javascript 코드다.

위의 코드는 sum 의 값이 변경되지만, 밑의 코드에서는 sum의 값이 불변성을 얻으므로, 해당 값을 변경시키려고 하면 컴파일러는 에러를 띄우게 된다. 결과적으로 더 안전한 코드가 된다.

슬픈 상황도 설계에 포함해보자

throw가 없는 코드를 중점적으로 들여다 보며, 어떻게 슬픈 상황을 함수형 디자인으로 매끄럽게 처리할 수 있는지 알아보자

일반적 디자인

슬픈 상황에서 일반적 디자인은 함수 중간에 탈출하여 에러를 내뱉을 것이다. 보통 함수마다 throw new Error 를 이용하여 try-catch로 처리 할 것이다.

함수형 디자인

그렇담 throw new Error 가 없는 함수형 프로그래밍에서는 중간에 오류가 나면 어떻게 행동할까?

중간에 오류가 나면 어떻게 끊고, 다른 함수를 실행하지 않고, 마지막에 에러를 어떻게 뱉어 낼까?

이제 본격적으로 Typescript를 사용한다. 해당 언어에 대한 기본적인 지식이 있어야 한다.

방금 전 말했듯이, ROP에서는 에러도 타입으로 본다고 했다. 그러니 Result 타입을 만들고 여기에 성공과 각종 에러와 관련된 결과 타입을 만들어서 함수가 해당 타입으로 내뱉게 하면 된다.

그런데 에러타입이 너무 많아지면 복잡해지니, SuccessFailure 로 줄이자

그리고 함수는 데이터가 리턴되어야 하는데 이렇게 두개의 타입만 있게되면 데이터를 포함할 수 없으니, 제너릭을 사용해서 다음과 같이 타입을 선언한다.

이렇게 되면 Success에는 데이터를, Failure에서는 실패 사유를 가지고 있을 수 있게 된다.

그러면 이제 어떻게 함수에서 중간에 탈출을 할것인가?

각 단계는 함수 하나씩에 대응될 것이고,
각 함수는 “Success” | “Failure” 두개의 Union type을 리턴할 것이고,
동일한 원리로 각 단계도 더 작은 함수들을 연결해서 만들 수 있다.
어느 단계에서건 발생한 에러는 하나의 “Failure” 경로에 합쳐지게 될 것이다.

그런데, 에러가 발생하면 다른 경로로 어떻게 보내버릴까?

Railway oriented programming

A functional approach to error handling

함수를 하나의 철도라고 생각해보자.

사과를 바나나로 만드는 철도 Function1 있다고 가정해보자.
그리고 바나나를 체리로 만드는 철도 Function2 가 있다.

그리고 이 두개를 이어 붙인다고 생각해보자

이어 붙이는 이 행위를 함수 합성이라고 하는데 함수를 합성하게 되면 사과를 체리로 만드는 함수가 만들어 진다.

이제 이 함수합성을 응용해보자

함수 Validate 가 에러를 리턴하게 하려면 위와 같은 코드가 나올 것이다.
중요한건 값을 리턴할 때, success 혹은 failure를 감싸서 리턴해야 된다는 점이다.

좀더 자세하게 해당 interface를 확인해 보자

success는 { success }true 로 리턴 하고, failure는 { success }false 로 리턴 하고 message를 보낸다.

success 혹은 failure를 감싸서 리턴을 하게 되면 이 일정한 규칙에 따라 함수들은 값을 리턴하게 될 것이다.

그래서 success 와 failure를 합한 합타입을 TwoTrack으로 정의 하였다.

이 TwoTrack을 리턴하는 함수는 결국 철도로 따지면 하나의 분기점이 되게 된다. 이 분기점은, Input을 받아서, 성공 트랙 혹은 실패 트랙으로 나눠진다.

함수합성… 그리고 분기점… 이제 우리는 이 분기점들을 이어 붙여나가 볼 것이다.

Validate를 한 다음, UpdateDb를 한다고 한다면, 성공한 경우에만 UpdateDb 를 해야된다. 실패한 경우에는 실패 트랙으로 그냥 지나가야 된다.

이 둘을 이어 붙이면 이런 느낌이 된다.

그리고 이러한 함수들을 이어 붙이면 이런 느낌이 된다.

아무리 많은 함수가 이어진다 해도, 다 이어 붙일 수 있게 되고, 하나의 투-트랙 모델이 된다.

이 투-트랙 모델을 사용하게 되면 에러 처리에 대한 “Railway oriented Programming”의 접근 방식을 취할 수 있게 된다.

성공하면 계속 성공한 트랙으로, 실패하면 실패한 트랙으로, 성공하다 실패하면 실패한 트랙으로, 마치 철도가 두개의 차선으로 동작하는 것 처럼 보인다.

하지만 우린 아까 함수 합성에 대해서 배웠다.
함수 합성은 앞의 함수의 Output이 그 다음 함수의 Input 타입과 일치해야만 가능 하다.

그렇기에 단일 트랙 함수들을 연결하는건 쉽다.

이와 같이 투-트랙 함수들을 연결하는 것도 마찬가지로 쉽다.

하지만 현재 우리가 가지고 있는 함수들은 이렇게 생겼다.

단일 트랙을 받아서 투-트랙을 내뱉고 있어, 이어 붙일 수가 없다.
단일 트랙을 투-트랙으로 만들어 주는 어댑터를 만들어야 한다.

어댑터를 만들자

위와 같이 우리는 단일 트랙을 투-트랙으로 만들어주는 어댑터를 만들어야 한다.

많은 함수형 프로그래밍에선 flatMap이라는 함수를 만들어서 제공하고 있다. 어댑터는 = flatMap 이다. 우리도 flatMap을 투-트랙에 맞게 만들어 보자

우선, 리턴 타입은 투-트랙으로 나오게 한다.
노란색 부분은 이전 함수에서 성공을 했을 경우, 지금 내 함수를 실행시킨다는 의미다.
파란색 부분을 보면 단일 트랙을 Input으로 받는 함수를 실행시켜주는걸 알 수 있다.

그리고 이전 함수가 실패했을 경우에는 그대로 실패 트랙으로 가게끔 만들어 준다.

정리해보자면, flatMap은 단일 트랙을 받는 함수를 투-트랙을 받는 함수로 변환을 시켜주는 역할을 한것이다.

그러면 다시, 아까 봤었던 함수들을 나열 해보자. 여전히 철도 조각에 불과할 것이다. 아직 이어 붙일 수 없다.

이 함수들에 flatMap을 씌워주면 이어 붙일 수 있는 철도 조각이 만들어 진다.

그리고 이어 붙이면 이렇게 된다. 이제 완전히 이어 붙게 된다.

그런데… 코드가 좀 못생겼다. 함수형 프로그래밍에서 자주 쓰이는 유틸리티중 하나인 Pipe를 이용해보자

그러면 이렇게 예쁘게 이어 붙일 수 있게 된다. 이 유틸 함수는 위에서 부터 하나식 절차적으로 함수를 실행시키는 기능을 가지고 있다.

그리고 한번더 validateRequest 에 대입을 하게 되면, 이 전체를 하나의 함수로 만들 수 있다. 세개의 부분 함수를 합성해서 validateRequest라는 새로운 함수를 만들게 된 것이다.

마무리

여기까지가 ROP의 기본적인 내용이었다. 이처럼, ROP를 도입하게 되면 좀더 에러처리를 깔끔하게 대응할 수 있게되어, 코드의 구조적 아름다움을 유지할 수 있게 해준다.

좀 더 심화한 부분을 다루기엔 내가 너무 지쳐서 다음 시간에 다뤄보도록 하려고 한다. 분명이 해뜰때 글쓰기 시작했는데 해 다 지고 글이 끝났다😢

끝.

참고

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade