Skip to content

Instantly share code, notes, and snippets.

@Hardtack
Last active August 1, 2019 07:54
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Hardtack/7f1c9d61cc1e5c07d7751880934a29d2 to your computer and use it in GitHub Desktop.
Save Hardtack/7f1c9d61cc1e5c07d7751880934a29d2 to your computer and use it in GitHub Desktop.
순수 함수형 상태 전이와 Functional Reactive 프로그래밍을 이용한 Reactive Model View Controller 설계 패턴 (1)

순수 함수형 상태 전이와 Functional Reactive 프로그래밍을 이용한 Reactive Model View Controller 설계 패턴 (1)

최근 많은 프로그래밍 언어에서는 함수형 프로그래밍을 지원하기 위한 다양한 도구들을 제공하고 있습니다. 그중 Swift는 최신 트렌드가 많이 반영된 언어로써, 스몰토크형 객체지향 프로그래밍과 함수형 프로그래밍을 함께 지원함으로써 개발자들에게 함수형 프로그래밍에 대한 접근성을 증가시켰습니다.

그리고 Reactive 프로그래밍이 조명을 받기 시작하며, Reactive Extension과 같은 훌륭한 리액티브 프로그래밍을 위한 도구를 제공하고 있습니다. Reactive Extension은 Reactive 프로그래밍을 지향하고 있으며, 이를 기반으로 Functional Reactive 프로그래밍을 실현할 수 있습니다. 또한, 다양한 언어에 포팅이 되어있어 서로 다른 언어에서 일괄적인 패턴을 적용할 수 있습니다.

이 글에선 순수 함수적 상태 전이에 대해 간단하게 알아보고, 이를 기반으로 Functional Reactive 프로그래밍이 지향하는바와, 이러한 기법들의 장점을 극대화 할 수 있는 설계에 대한 기본 원칙, 그리고 그에 따른 설계의 예를 다룰 것 입니다.

순수 함수형 상태 전이 (Pure Functional State Mutation)

순수 함수형 프로그래밍의 경우에는, 불변값(Immutable Value)를 다루고, 부수효과(Side effect)를 만들지 않는다는 원칙 때문에 상태를 전이하는 방법에 대해서 낯설게 느껴질 수 있습니다.

순수 함수로 상태를 다루기 위해서는 먼저 초계함수(High order function)에 대하여 알고 넘어가야합니다.

초계함수 (High Order Function)

초계함수의 정의는 간단합니다. 이 글에선

함수를 인자로 받는 함수

정도로 정의를 하면 충분할 것 같습니다.

함수형 프로그래밍에서 쓰이는 초계함수는 셀 수 없이 다양하게 있지만 가장 일반적으로 사용되는 함수들을 꼽아보았습니다.

아래 함수들은 함수형 프로그래밍에서 많이 쓰이는 초계함수들이며, 이해의 편의를 위해 리스트 타입에서의 정의로 설명하겠습니다.

  1. map

    map 함수는 리스트의 각 요소를 변환하여 새로운 리스트를 만드는 함수입니다. 일반적인 정의는 다음과 같습니다. (정의의 경우 수도-코드(Pseudo Code)로 작성하겠습니다.)

    func <T, R> map(origin: List<T>, mapper: T -> R) -> List<R>

    사실 Swift에서는 map 함수가 Array타입의 메소드로 정의가 되어있지만, 여기선 사소한 내용은 넘어가겠습니다.

    다음은 Swift에서 map을 이용해서 특정 숫자 배열의 값을 1 증가시키고, 문자열으로 변환하는 예제입니다.

    let foo = [1, 2, 3];
    // => [1, 2, 3]
    
    /**
     * 숫자를 1 증가시킨 후 문자열 타입으로 변환한다.
     */
    func increaseAndToString(x: Int) -> String {
        return String.init(x + 1);
    }
    // => Function
    
    foo.map(increaseAndToString)
    // => ["2", "3", "4"]
  2. flatMap

    flatMap 함수는 map 함수와 유사하지만, mapper의 반환 타입이 리스트라는 점에서 차이가 있습니다.

    map함수가 mapper가 반환한 값들을 요소로 하는 새로운 리스트를 만들었다면

    flatMap함수는 mapper가 반환한 리스트를 이어붙인 새로운 리스트를 만듭니다.

    flatMap은 일반적으로 다음과 같이 정의됩니다.

    func <T, R> flatMap(origin: List<R>, mapper: T -> List<R>) -> List<R>
    

    다음은 Swift에서 flatMap을 이용해서 문자열의 리스트를 문자의 리스트로 변환하는 예제입니다.

    let foo = ["Hello", "World!"];
    // => ["Hello", "World!"]
    
    /**
     * 문자열을 문자의 리스트로 변환한다.
     */
    func expandCharacters(string: String) -> Array<Character> {
        return Array.init(string.characters);
    }
    // => Function
    
    foo.flatMap(expandCharacters)
    // => ["H", "e", "l", "l", "o", "W", "o", "r", "l", "d", "!"]
  3. filter

    filter 함수는 이름에서 리스트에서 특정 조건을 만족하는 요소들로 새로운 리스트를 만드는 함수입니다. 여기서 "특정 조건은" Boolean 타입을 반환하는 함수로 정의됩니다.

    다음은 일반적인 filter함수의 정의입니다.

    func <T> filter(List<T> origin, predicate: T -> Boolean) -> List<T>
    

    다음은 filter를 이용해 숫자 리스트에서 짝수만 걸러내는 예제입니다.

    let foo = [1, 2, 3, 4, 5];
    // => [1, 2, 3, 4, 5]
    
    /**
     * 짝수인지 판별한다.
     */
    func isOdd(number: Int) -> Bool {
        return number % 2 == 0;
    }
    // => Function
    
    foo.filter(isOdd);
    // => [2, 4]
  4. fold

    fold 함수는 리스트의 값을 순차적으로 이행함수 (인자가 두개인 함수)에 적용해 하나의 값을 도출해내는 함수입니다.

    다음은 일반적인 fold함수의 정의입니다.

    func <T, R> fold(List<T> origin, initialValue: R, accumulator: (R, T) -> R) -> R
    

    다음은 fold 함수를 이용해서 문자열에 숫자들을 더하는 예제입니다. (Swift에선 reduce 라는 이름을 가집니다.)

    let foo = [1, 2, 3, 4, 5];
    // => [1, 2, 3, 4, 5]
    
    /**
     * 문자열 뒤에 숫자를 이어붙인다.
     */
    func appendNumber(left: String, right: Int) -> String {
        return left + String.init(right);
    }
    // => Function
    
    foo.reduce("Numbers: " , appendNumber);
    // => "Numbers: 12345"
  5. scan

    scan 함수는 fold함수와 유사합니다. 한가지 다른점은, 모든 값을 계산한 결과가 아닌, 중간 결과를 리스트로 그대로 반환하는 점 입니다.

    다음은 일반적인 scan함수의 정의입니다.

    func <T, R> scan(List<T> origin, initialValue: R, accumulator: (R, T) -> R) -> List<R>
    

    아쉽게도 Swift의 리스트에는 scan의 구현이 없습니다. 만약 있었다면 다음과 같은 형태였을 것입니다.

    let foo = [1, 1, 1, 1, 1];
    // => [1, 1, 1, 1, 1]
    
    foo.scan(1) { (x, y) in x + y };
    // => [2, 3, 4, 5, 6]

위 함수들이 함수형 프로그래밍에서 주로 사용되는 초계함수이며, 이 다섯가지로 상당히 많은 문제를 해결할 수 있을것입니다.

이렇게 대체할 수 있습니다.

모나드

위에서 언급한 초계함수들은 모두 리스트를 위한 도구들로써 언급되었지만, 사실 리스트에만 국한된 도구가 아닙니다.

fold, scan을 제외한 flatMap, map, filter는 "모나드" 라 불리는 모든 형태의 타입에 적용될 수 있습니다.

모나드의 엄밀한 정의는 조금 더 많은 내용을 포함하지만, 이 글에서는 어려운 내용을 포함하지 않기를 원하기에, 엄밀하지 않지만 단순한 정의를 하였습니다.

1. 값을 가질 수 있는 컨테이너 타입

2. flatMap 함수를 이용하여, 내부의 값을 다른 값으로 전이할 수 있는 타입

3. 이때 전이란, mapper에 의해 변환된 값을 가진 새로운 컨테이너를 만드는 것을 이야기합니다

map과 filter는 flatMap을 이용해 구현할 수 있는 함수이기에, 정의에 포함되지 않았습니다.

이 정의에 의하면 위에서 보았듯이 리스트는 모나드입니다.

Swift의 빌트인 타입인 Optional도 모나드입니다.

그리고 결정적으로 RxSwift의 Observable 타입 또한 모나드입니다.

그리고 RxSwift를 사용해 본 경험이 있다면 알 수 있듯이, fold(reduce)와 scan은 이미 Observable에 포함이 되어있습니다. 여기서 fold, scan등의 함수는 연속적인 값을 가질 수 있는 모나드에 적용될 수 있다는 것을 할 수 있을것입니다.

이를 명확하게 정의하면, "Foldable인 Monoid에 적용될 수 있다..." 이런식이지만 이해할 필요는 없습니다.

모나드에 대해서 언급을 한 것은, 위에서 언급한 다섯가지 초계함수들이 특정 종류에 속한 타입이라면 똑같이 적용될 수 있는 범용 개념이며, 리스트에 국한되지 않았다는것을 설명하기 위해서였습니다.

이제 모나드는 잊어버려도 좋습니다! (어차피 올바른 정의도 아닙니다)

순수 함수형 상태 전이 (Pure Functional State Mutation)

이제 순수 함수를 이용해 상태 전이를 이해하기 위한 기반은 다져졌습니다.

먼저 상태의 전이를 정의해보자면

기존 상태와, 전이 액션이 조합되어 새로운 상태를 만들어내는 현상

으로 정의 할 수 있습니다.

예를 들어, 1 이라는 숫자 상태에 1을 더하라 라는 액션이 조합된다면 2 라는 새로운 상태가 만들어집니다.

조금 더 현실 문제에 가까운 이야기를 해보겠습니다.

다음과 같은 타입의 상태가 있다고 합시다.

struct State {
    let loading: Bool
    let data: String?
}

위 타입은 데이터가 로딩중인지 나타내는 loading 필드와, 실제 데이터를 나타내는 data필드를 가집니다.

다음은 전이 액션에 대한 타입입니다.

enum Action {
    case setLoadingStart
    case setLoadingEnd
    case updateData(String?)
}

로딩의 시작, 끝, 그리고 데이터의 업데이트를 나타내는 액션입니다.

만약 초기상태가 다음과 같고

let initialState = State(loading: false, data: nil)

Action.setLoadingStart 액션을 수행하면, 전이된 상태는 다음과 같은 것 입니다.

State(loading: true, data: nil)

이는 다음과 같은 함수로 나타낼 수 있습니다.

func mutate(state: State, action: Action) -> State {
    switch action {
    case .setLoadingStart:
        return State.init(loading: true, data: state.data)
    case .setLoadingEnd:
        return State.init(loading: false, data: state.data)
    case .updateData(let data):
        return State.init(loading: state.loading, data: data)
    default:
        return state
    }
}

위 함수는 상태를 어떻게 전이할지에 대해 기술하고 있습니다. 이제 이 전이 방법을 실제로 적용하려면 어떻게 할까요?

어플리케이션, 또는 뷰 등의 생명주기동안에는 액션은 지속적으로 유입이 될 것 입니다. 이러한 액션의 지속적인 유입을 스트림으로 추상화하면 RxSwift를 이용해 Observable<Action>으로 나타낼 수 있습니다.

그리고, 액션의 지속적인 유입은 액션으로 인해 전이된 상태의 스트림을 생산하게 되고, 이는 Observable<State>로 나타낼 수 있습니다.

다음 예시에서는 액션의 스트림과 위에서 정의한 mutate 함수를 이용해 상태의 스트림을 만들어냅니다.

let actionStream: Observable<Action> = /* Create action stream */
let stateStream: Observable<State> = actionStream.scan(initialState, accumulator: mutate)

만약 loading 상태를 숨기고, data만 상태로 노출하고 싶다면 map 함수를 이용하면 됩니다.

let publicStateStream = stateStream.map { x in x.data }

정말 간단하지 않나요? 정말 이걸로 복잡한 문제들을 해결할 수 있을지 의심이 들 수 있지만, 이미 Facebook에서는 비슷한 방식([1], [2])으로 복잡한 문제들을 해결하고 있습니다.

이로써 순수함수만을 이용해 상태 전이를 만들어내는 방법을 알았습니다. 이렇게 순수 함수를 이용해 상태를 Reactive하게 다루는 방식을 Functional Reactive Programming이라고 부릅니다. (Reactive에 대한 정의는 Rx 사용자들에게는 이제는 진부할테니 생략하겠습니다.)

다음에는 Functional Reactive Programming을 이용해 어떻게 실제로 UI를 가진 어플리케이션을 구성하는지와, 단방향의 데이터 흐름(Unidirectional Data Flow)을 가진 모델-뷰-컨트롤러(Model-View-Controller) 패턴에 대한 이야기를 하겠습니다.

@devxoul
Copy link

devxoul commented Mar 14, 2017

본문의 내용 중,

Reactive Extension은 Reactive 프로그래밍 중 Functional Reactive 프로그래밍을 지향하고 있으며

는 잘못된 설명입니다. Rx에 관해 가장 잘못 알려진 오해 중 하나인데요. Rx는 Functional Reactive Programming이 아니고, 지향하고 있지도 않습니다. ReactiveX 소개 페이지에 다음과 같이 나와있습니다:

It is sometimes called “functional reactive programming” but this is a misnomer. ReactiveX may be functional, and it may be reactive, but “functional reactive programming” is a different animal.

굉장히 유명한 글 중 하나인 The introduction to Reactive Programming you've been missing정정 댓글도 좋은 참고자료가 될 듯 합니다.

@Hardtack
Copy link
Author

Hardtack commented Mar 15, 2017

@devxoul
조언 감사합니다. 제안해준 내용 반영했습니다.
본문에서는 맥락을 벗어나기에 댓글로 부연설명을 하자면, Reactive Extension은 Functional Reactive 프로그래밍을 위한 조건을 만족합니다. 하지만 조건을 만족할 뿐 타게팅해서 지향하지는 않는것으로 보이네요.

FRP의 조건에 만족한다고 이야기하는 이유는 다음과 같습니다.

  1. Reactive 합니다.

  2. Functional 합니다.

  3. 연속적인 (Continuous) 값을 나타낼 수 있습니다. (Observable<State>) - Dynamic, Evolving Value

  4. 여러 스트림을 하나로 합치는 도구를 제공하여 결정적(Deterministic)인 값을 제공할 수 있습니다. - Deterministic Value

(Ref. http://stackoverflow.com/a/1030631)

@kciter
Copy link

kciter commented Mar 15, 2017

좋은 글 감사합니다. 평소 Rx를 사용하면서 FRP라고 생각을 했었지만 @devxoul님이 남겨주신 것 처럼 Functional은 잘못된 설명이라는 자료를 보고 많이 혼란스러웠습니다. 그런데 댓글에 남겨주신 것 처럼 FRP 조건에 만족한다고 생각하면 고민했던 내용들이 정리가되네요.

이 글에서는 iOS개발시 Flux 아키텍처 패턴과 Rx를 사용하고 액션이 스트림으로 흐른다는 점에서 redux-observable과 유사해보입니다. 웹 개발에서는 node.js를 사용할 때 데이터의 흐름을 편리하게 관리할 수 있다는 점과 React와 궁합이 잘 맞다는 점에서 잘 사용했었습니다. 그런데 iOS개발시 Flux 아키텍처를 따라하여 개발을 해보다가 설계 미스일지도 모르지만 특유의 단방향성과 뷰를 표현하는 방식에서 iOS 개발시 불편하다는 생각을하고 접었던 기억이 있습니다. 이 글을 읽다보니 혹시 제가 Flux에 대해 잘못이해하고 코딩을해서 불편했던게 아닐까하는 생각이 들었는데 마침 이 글을 써주셔서 몇가지 궁금증이 생겼는데요,

  1. 다음글에서 이 패턴으로 진행하실지는 모르겠지만 만약 진행하신다면 MVVM 패턴으로 개발할 때와 비교해서 어떤 점에서 더 좋을까요?
  2. API 로직은 Action에 두는게 맞을까요? 아니면 서비스 레이어를 별도로 두는 것이 더 좋을까요?
  3. Redux에서 영향 받아 만들어진 ReSwift 라이브러리는 어떻게 생각하시나요?

아무래도 지식이 부족하다보니 질문도 추상적이지만 혹시 답변이 가능하시다면 많은 가르침 부탁드립니다. 😄

다음 글도 기대하겠습니다! 👍

@devxoul
Copy link

devxoul commented Mar 15, 2017

혹시 테스팅도 다루시나요?

@Hardtack
Copy link
Author

@kciter
사실 이 내용에 대해서는 다음에 쓸 글에서 이야기하려고 준비중이었습니다.
간단하게 먼저 이야기하자면, 제가 처음 FRP에 관심을 가진 이유는 Redux를 이용하여 개발을 하던 중 Flux, Redux의 중요한 요소인 거대한 하나의 Data Store 에서 큰 불편을 느꼈기에 대안을 찾고 있었기 때문입니다.
아마 Flux에서 불편을 느꼈다면, 한가지 액션을 위해 글로벌 레벨에 있는 Store를 수정하는데에서 손이 많이가는점에서 느꼈을 것 이라고 예상해봅니다.

MVC에 대해서 이야기 하는 이유는, 기본적으로 많은 UI 아키텍쳐 패턴들의 경우 MVC를 기반으로하여 시작했고, MVC 자체를 돌아보았습니다.
제가 내린 결론은 MVVM, MVP, Flux등의 설계는 MVC의 각 요소를 어떻게 묶느냐에 따른 variation이라는 생각을 갖게 되었고 각자 어느 부분에선 강점을 가지지만 그 강점의 근원은 MVC에서 나온다고 생각을 하여 MVC에 대해서 고민을 하고 글을 작성하고있습니다.

질문에 대한 답변을 드리자면

  1. MVC에 대한 이야기를 할 생각입니다. MVVM 패턴보다의 강점이라하면, 크게 없을 것 같습니다. 마찬가지로 단점도 없을 것 같습니다. 굳이 이야기하자면 조금 더 광범위하게 쓰이는 용어이라서 접근하기 용이하다는 점?

  2. API로직은 Controller에 두는게 맞다고 생각합니다.

  3. Redux와 같은 이유로 좋은 아이디어이지만 개선의 여지는 많다고 생각합니다.

@kciter
Copy link

kciter commented Mar 15, 2017

@Hardtack
답변 감사합니다. Store라는 것이 너무 거대해지고 개발자적인 본성 때문인지 글로벌 레벨에 있다는 점이 효용성은 둘째치고 마음에 안들긴합니다. ㅠㅠ
MVC가 불편하다고 느껴 나온 아키텍처에서 반대로 원점인 MVC로 돌아가서 고민하신다니 어떤 글이 나올지 기대되네요 :)

@Hardtack
Copy link
Author

@devxoul 해...야겠죠..?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment