개발/Functional Programming

[함수형 반응형 프로그래밍] #1. 듣기를 멈추자!

lanace 2020. 10. 17. 20:17

목표

  • FRP의 정의
  • 이벤트의 정의와 이벤트가 문제를 야기하는 경우
  • FRP의 목표 → 우리가 해결하려는 문제의 정의
  • FRP의 장점
  • FRP 시스템의 작동 원리
  • FRP의 바탕이 되는 사고방식

 

FRP는 표준적인 프로그래밍 언어에 간단한 라이브러리 형태로 제공되며 관찰자 패턴에서 널리 사용하는 리스너를 대신한다.

FRP를 사용하면 코드가 깔끔하고, 명확하며, 튼튼해지고 관리도 쉬워진다.

⇒ 코드가 간단해진다.

 

FRP는 우리가 기존에 하던 방식과는 다른 방식으로 문제를 해결한다.

FRP는 함수형 프로그래밍의 아이디어를 기초로 하지만 FP를 몰라도 문제없다.

⇒ 즉, 함수형 프로그래밍과 FRP는 별개다?? 흠...

듣기를 멈추고 반응을 시작하자!

1.1 프로젝트, 복잡도의 벽에 부딪치다


큰 프로젝트는 어느 순간 이후 복잡도의 벽에 부딪치기 마련이다.

복잡도는 기하급수적으로 올라간다.

처음엔 눈치채기 어렵다. 이후엔 다음과 같은 상황이 나타난다.

  • 진행이 보류된다.
  • 처음부터 다시 시작한다. → 비용
  • 인력을 보강한다. → 생산성 저하
  • 대규모 리팩터링을 통해 결국 관리 가능한 코드를 만든다.

4가지 중 리팩터링만이 전진하는 길이다.

FRP는 리팩터링과 잘 어우러져 복잡도를 낮추는데 도움을 준다.

FRP는 방법론이 아니고, 모든 문제를 해결하진 않는다.

FRP는 복잡도의 일반인 근원으로 떠오른 이벤트 전달을 다루는 구체적인 프로그래밍 기법이다.

1.2 함수형 반응형 프로그래밍이란?

FRP를 여러 각도에서 살펴보자

  • FRP는 자주 쓰이는 관찰자 패턴을 대치한다. 관찰자 패턴을 리스너나 콜백이라고도 한다.
  • FRP는 이벤트 위주의 로직을 조합 가능한 모듈로 코딩하는 방법이다.
  • FRP는 다른 방식으로 사고한다.
    프로그램을 입력에 대한 반응 또는 데이터의 흐름으로 표현한다.
  • FRP는 프로그램 상태를 관리하는 데 질서를 부여한다.
  • FRP는 근본적인 방식으로, 관찰자 패턴으로 문제를 해결하려고 노력하는 사람이라면 결국에는 FRP를 발명하게 될 것이라 생각한다.
  • FRP는 일반적인 프로그래밍 언어의 경량 소프트웨어 라이브러리로 구현된다.
  • FRP는 상태가 있는 로직을 위한 완전한 내장 언어라고 볼 수 있다.

FRP를 상태가 있는 로직을 표현하기 위한 최소이자 완전한 DSL이라고 이해할 수 있다.

DSL은 Domain Specific Language이다.

1.2.1 더 엄격한 정의

FRP는 두 가지로 집약된다.

  1. 표시적
    각각의 타입과 구성요소의 의미를 정확히 지정해주는 엄밀하고 단순하며 구현과는 무관한 합성 가능한 의미론을 기초로 FRP가 이루어져야 한다는 뜻

  2. 시간 연속적
    ???

표시적 의미론

프로그래밍 언어의 엄밀한 의미를 수학식을 사용해 정의한 것

FRP 시스템의 경우 표시적 의미론은 시스템에 대한 정확한 명세를 제공하고, 모든 경우의 모든 구성요소에 대해 합성성이라는 특성이 성립함을 증명한다.

1.2.2 소듐 소개

FRP를 기반으로 만든 라이브러리이다.

책에선 소듐을 기반으로 설명한다.

소듐을 선택한 이유는 다음과 같다고 한다.

  • 다양한 언어 지원. 검증된 라이브러리
  • FRP의 진정한 정의를 장려하기 위한 수단
  • 미래의 혁신에 대한 기준점이자 벤치마크 대상
  • 최소주의 설계 사상에 따른 견실한 학습 플랫폼 역할

1.3 FRP가 적합한 분야와 현재 상황


 

FRP는 함수형 프로그래밍과 반응형 프로그래밍의 교집합이다.

함수형 프로그래밍

수학적 의미의 함수에 기반한 프로그래밍 방식 또는 패러다임

공유된 변경 가능한 상태를 의도적으로 회피한다.

불변 데이터 구조를 사용

합성성을 강조

반응형 프로그래밍

프로그램을 전통적인 제어의 흐름이 아니라 이벤트 기반의 입력에 반응하며 동작하는 데이터의 흐름으로 보는 것을 뜻하는 광범위한 용어이다.

프로그램 구성요소 간의 더 느슨한 결합을 허용하며, 모듈화 된다.

함수형 반응형 프로그래밍

반응형 프로그래밍을 달성하기 위해 함수형 프로그래밍의 규칙을 따르도록 강제하는 구체적인 방법이다.

특히 합성성이란 특성을 강조

Rx는 뭐지?
마이크로소프트의 Reactive Extensions는 아카와 FRP의 중간으로, 이벤트 핸들러를 사슬처럼 엮는데 관심을 갖는다. 6장에서 자세히 배운다.

1.4 대화형 애플리케이션: 이벤트가 뭐야?


대부분의 애플리케이션은 두 프로그래밍 모델 중 하나를 중심으로 두거나 둘을 혼합해서 구조를 잡을 수 있다.

  • 스레드
  • 이벤트

입력에 대한 반응으로 상태를 변화시키는 것을 관리하려고 한다.

목표는 같지만 방법은 다르다.

스레드 - 상태변화를 제어 흐름으로 모델링한다. 스레드는 상태 변화 순서가 명확히 정의될 때나 I/O에 잘 어울리는 경향이 있다. 액터나 제너레이터도 이 범주에 넣을 수 있다.

이벤트 - 프로그램에서 전파되는 이산적이고 비동기적인 메시지이다. 이벤트는 순서가 덜 명확할 때 적합하다. 특히 구성요소 간의 상호작용이 복잡할 때 잘 맞는다.

1.5 상태 기계 분석의 어려움


상태 기계는 다음과 같은 방식으로 동작하는 시스템이다.

  1. 시스템에 입력 이벤트가 들어온다.
  2. 프로그램 로직이 입력 이벤트와 프로그램의 현재 상태를 바탕으로 판단을 내린다.
  3. 프로그램 로직이 프로그램의 상태를 바꾼다.
  4. 프로그램 로직은 출력을 만들 수도 있다.

대부분의 프로그래밍 상태 기계라고 할 수 있다.

효율적이지만 읽기 어렵고 깨지기 쉽다.

1.6 버그 없는 대화형 애플리케이션


문제를 다루는 방법에서 문제가 발생한다.

1.7 리스너는 이벤트 처리의 기둥. 하지만...


리스너나 콜백은 이벤트를 전파하는 주된 방식이다. 하지만 이 방식만 있는 것은 아니다.

옛날엔 생산자가 소비자에 의존했다.

이벤트에 새로운 소비자를 추가하고 싶다면 생산자가 소비자도 호출하도록 만들어야 했다.

지금은 관찰자 패턴을 통해 이러한 문제점을 해결하였다.

이를 통해 의존성을 뒤집었고, 소비자는 이제 생산자에 의존한다.

⇒ 확장하기 쉽고 구성 요소 간의 연결이 느슨해져서 모듈화 하기 좋다.

1.8 리스너의 여섯 가지 재앙 퇴치하기


예측 불가능한 순서

리스너가 복잡하게 얽혀있다면 이벤트가 도착하는 순서는 등록한 순서에 따라 정해지는데, 별로 도움이 되지 않는다.

⇒ 이벤트의 처리 순서를 감지할 수 없기 때문에 이벤트 처리 순서가 문제 되지 않는다.

??? 이건 그냥 못한다는 거 아닌가??

첫 번째 이벤트 소실

생산자가 첫 번째 이벤트를 생산하기 전에 리스터가 등록되리라 보장하기 어렵다.

⇒ FRP는 트랜잭션 방식이라 보장한다.

지저분한 상태

콜백으로 인해 코드가 전통적인 상태 머신 형태로 바뀌며 지저분해진다.

⇒ FRP는 질서를 가져온다.

스레드 문제

리스너를 스레드 안전하게 만들려 하다가 교착상태를 일으킬 수 있다.

리스너가 등록 해제된 다음에 호출되지 않도록 보장하기 어려울 수 있다.

⇒ FRP는 발생하지 않는다.

콜백 누수

리스너를 해제하는 것을 잊으면 메모리 누수가 발생한다.

⇒ FRP는 발생하지 않는다.

의도치 않은 재귀

로컬 상태 갱신과 리스너 통지의 순서가 매우 중요한데, 순서를 정할 때 실수하기 쉽다.

⇒ FRP는 괜찮다.

1.9 그냥 리스너만 고쳐서 사용하지 않는 이유


위의 문제를 해결하려고 하다 보면 자연스럽게 보이게 되는 것이 FRP이다.

따라서 새로운 것을 만드는 것이 아니라 기존에 있는 것을 사용하건, 랩핑을 해서 사용하길 권장한다.

1.10 "재시작해봤어?" 또는 상태가 문제가 되는 이유


프로그램이 정상적으로 동작하지 않는 상태에서 어떻게 하는가?

인터넷 짤로 많이 돌아다니는 것들 중 다음과 같은 것들이 있다.

  • 껐다 켜봐
  • Ctrl + Alt + Del
  • 재시작해봐

대부분의 언어에서는 변수를 다음과 같이 정의한다.

int x = 10;

x = x + 1;

FRP에서는 실행 순서에 민감한 일반적인 변경 가능한 변수를 사용하지 않는다.

흔한 버그의 예로, 어떤 변수의 값을 변경한 다음 그 값을 읽으려 했는데, 그와 반대 순서로 처리되는 경우가 있다.

FRP는 상태를 셀이라는 컨테이너에 담아둔다.

변경 통지와 실행 순서 관리가 자동으로 들어가ㅇ므로 셀은 이런 실행 순서의 문제를 해결해준다.

FRP는 항상 셀을 최신의 상태로 유지해준다.

이상 사례: 프로그램 설정

복잡한 설정을 가진 프로그램을 다룰 때 실행 중 설정을 바꾸면 어떻게 될까?

설정을 사용하는 모듈은 상태 변경을 감지하기 위해 리스너를 사용해야 한다.

이는 코드를 복잡하게 만들고, 적절하게 전파되도록 만드는 과정에서 실수가 생길 수 있다.

테스트도 어렵다.

분명 단순하게 동작해야 되는 설정이 왜 이렇게 어렵게 느껴지는 것일까?

뭔가 근본적으로 잘못된 것을 의미한다.

이런 문제점을 해결하는데 FRP는 도움이 될 수 있다.

1.11 FRP의 장점: 복잡도 다루기


FRP는 특별한 방법으로 복잡도를 낮춘다.

수학적인 특성으로 합성성이라 한다.

합성성은 소프트웨어 구성요소들을 예상치 못한 부수 작용 없이 서로 합성할 수 있게 해주는 특성이다.

1.12 FRP는 어떻게 동작하는가?


예시로 살펴보자

출발일과 복귀일을 정하는 UI가 있다.

출발일과 복귀일, 그리고 비즈니스 로직의 옳음 상태는 모두 사용자의 입력에 따라 동적으로 변한다.

이는 데이터의 흐름을 보여준다.

SDateField dep = new SDateField();
SDateField ret = new SDateField();

Cell<Boolean> valid = dep.date.lift(ret.date, (d, r) -> d.compareTo(r) <= 0);
SButton ok = new SButton("OK", valid);

FRP는 두 가지 기본적인 데이터 타입을 사용한다.

  • Cell은 시간에 따라 변화하는 값을 표현한다.
  • Stream은 이벤트의 흐름을 표현한다.

1.12.1 FRP프로그램의 생애 주기

FRP 시스템에서 이 동작은 실행중에 일어나며 다음과 같은 두 단계를 거친다.

1단계: 초기화 - 보통 프로그램 시작시 이뤄지며, FRP 코드 문장을 메모리상의 방향성 그래프로 변환한다.

2단계: 실행 - 프로그램을 실행하면서 상ㅇ자가 값을 넣고 크랭크 손잡이를 돌리면 FRP엔진이 결과를 만들어낸다.

FRP엔진의 주요 작업은 모든것이 메모리에 있는 방향성 그래프가 지정하는 의존 관계 순서대로 처리되게 만드는 것이다.

초기화 단계에서 다음과 같은 문장이 실행된다.

Cell<Boolean> valid = dep.date.lift(ret.date, (d, r) -> d.compareTo(r) <= 0);

이 코드는 위젯 사이의 관계만을 표현하며, 다른건 아무것도 하지 않는다.

초기화가 끝나면 실행에 들어간다.

FRP엔진이 관계를 유지하면서 valid를 최신 상태임을 보장하는 일을 담당할것이다.

1.13 패러다임 전환

FRP는 새로운 패러다임이다. FRP를 사용해서 기존 코드 기반을 점차 개선할 수 있지만 그 밑바탕에는 다른 사고 방식이 깔려있다.

1.13.1 패러다임

의식하지 않고 밑바탕에 깔려있는 의식

패러다임이 서로 다르면 얘기가 안됨

1.13.2 패러다임 전환

기존에 OOP 패러다임에서 개발해오던 사람은 그 패러다임에서 벗어나야 한다.

1.14 의존성 관점에서 생각하기


다음과 같은 예시에선 순서가 매우 중요하다.

  • 사일로 뚜껑을 연다.
  • 미사일을 발사한다.

순서가 뒤바뀐다면... 끔찍하다.

이렇듯 FRP에서는 순서에 의존적일때 사용하면 좋다.

이전에 살펴보았던 예약시스템에 FRP를 사용하지 않았을때의 예제코드를 살펴보자

public class BookingDialog {
    public BookingDialog() {
        JDateField startField = new JDateField();
        JDateField endField = new JDateField();

        this.ok = new JButton("OK");

        // 순서 중요!
        this.start = startField.getDate();
        this.end = endField.getDate();
        update();

        startField.addDateFieldListener(new DateFieldListener() {
            public void dataUpdated(Date date) {
                // 순서 중요!
                BookingDialog.this.start = date;
                BookingDialog.this.update();
            }
        });

        endField.addDateFieldListener(new DateFieldListener() {
            public void dataUpdated(Date date) {
                // 순서 중요!
                BookingDialog.this.end = date;
                BookingDialog.this.update();
            }
        });
    }

    private void update() {
        boolean valid = start.compareTo(end) <= 0;
        ok.setEnabled(valid);
    }
}

위의 코드에서 순서가 매우 중요하다.

값을 바꾸지 않았는데 update하면 올바른 값이 나오지 않는다.

스레드에서 시퀀스를 표현할 수 있다.

이벤트로도 의존 관계를 표현할 수 있다.

의존관계를 스레드로 표현하거나 이벤트를 시퀀스로 표현하는 데서 많은 문제가 생긴다.

FRP 엔진은 이런 관계를 잘 알고 있어서 의존 관계를 자동으로 결정한다.

덕분에 올바른 시퀀스를 보장할 수 있다.

FRP를 사용하지 않고 리스너 기반으로 이벤트 처리를 사용해도 의존관계를 표현할 순 있지만 신뢰할만한 시퀀스를 유지하기엔 너무 비용이 크다.

처리 순서는 코드가 이벤트를 언제 전파하느냐에 따라 달라지고, 어떤 리스너가 등록되어있느냐에 따라 다르다.

예츨불가능한 순서 문제가 생긴다.

FRP는 이런 측면의 프로그래밍을 덜어주며, 의존관계에만 관심을 쏟게 해준다.

1.15 선언적으로 생각하기: 이 프로그램은 무엇인가?


FRP는 기계 역역보다는 문제 영역에서 이야기한다.

시퀀스는 의존관계로부터 만들 수 있기 댸문에 시퀀스를 전혀 기술하지 않으면 코드를 더 적게 작성할 수 있다.

어떻게는 더 적으면서 어떤것 을 더 많이 사용한다.

⇒ 선언적 프로그래밍

조작적 정의 vs 개념적 정의

조작적 정의는 1에서 10까지 하나하나 순서를 기록하는 것이다.

개념적 정의는 해당하는것이 뭔지를 정의하는것이다.

요리법에 비유할 수 있다.

조작적 정의는 하나하나 다 나열하는것.

개념적 정의는 해당하는 개념이 무엇인지 서술하는 것

개념적 정의의 장점

  • 의존 관계를 직접 표현한다.
    그로부터 순서를 정할 수 있다.
  • 개념적인 모습에 더 가깝기 때문에 이해하기 쉽다.
  • 짧다.
  • 조합해서 새로운것을 만들기도 좋다 ⇒ 재사용성

프로그램은 입력을 출력으로 변환 하는 것이다.

1.16 FRP, 개념적 이해 vs 조작적 이해


1.16.1 FRP에 마음 열기

FRP는 반응형이라 불린다.

FRP 로직은 데이터의 흐름이다.

데이터는 로직안으로 들어와 스트림과 셀 사이를 흐른다.

출력 부분에서 데이터는 외부로 흘러나간다.

FRP 코드는 입력에 대한 반응이다.

FRP는 근본적으로 출력을 입력의 측면에서 선언적으로 기술하는것이다.

개념적인 수준에서 생각하기 위해 노력하자!

여러 요소간 상호작용의 메커니즘이 아니라 그들 사이의 관계 수준에서 머물라.

1.16.2 코드가 실행되면 어떤 일이 벌어지는가?

초기화 과정에서 푸시 기반의 FRP 시스템은 리스너의 네트워크를 만들어낸다.

실행단계에 사용자가 값을 변경하지 않으면 아무일도 발생하지 않는다.

  1. dep.date와 ret.date는 자신의 리스너에게 변경된 내용을 알린다.
  2. valid에 있는 update() 메서드가 호출된다. 그 안에서 비즈니스 규칙에 따라 최신값을 가지고 로직을 다시 계산한다. 작업이 끝나면 valid가 자신의 리스너를 호출한다.
  3. ok의 update()메서드가 호출된다. 버튼이 활성화 또는 비활성화 된다.

1.17 함수형 프로그래밍을 이벤트 기반 코드에 적용하기


FRP의 능력중 상당 부분은 FRP 셀과 스트림이 리스너/콜백에서 결코 기대할 수 없는 함수형 프로그래밍의 규칙을 따른다는 사실에 있다.

이벤트 기반의 코드를 함수형 프로그래밍으로 다룰 수 있다.

FRP는 함수형 프로그램이 이벤트 기반 로직을 위한 메타 언어 역할을 수행할 수 있게 해준다.

비즈니스 로직을 Rule이라는 클래스로 캡슐화해서 규칙을 개념처럼 다루려고 한다.

Rule r1 = new Rule((d, r) -> d.compareTo(r) <= 0);

새로운 규직 r2를 추가해보자

private static boolean unlucky(Calendar ct) {
    int day = ct.get(Calendar.DAY_OF_MONTH);
    return day == 4 || day == 14 || day == 24;
}

위 규칙을 기반으로 기존 규칙에 추가하면 다음과 같은 코드가 나오게 된다.

Rule r2 = new Rule((d, r) -> !unlucky(d) && !unlucky(r));

Rule r = r1.and(r2);

r1과 r2를 모두 만족시켜야 하는 r을 생성한다.

이러한 방법으로 규칙을 추가할 수 있다.

 

1.18 요약


  • 리스너와 콜백은 많은 문제가 있다.
  • FRP는 상태 기계와 리스너 또는 콜백을 대치한다.
  • FRP는 합성성이라는 수학적 특성을 통해 복잡도를 다룬다.
  • 시퀀스라는 관점보다 의존관계라는 관점에서 생각하는것이 더 낫다.
  • FRP 코드에는 방향성 그래프와 같은 구조가 들어있다. FRP 엔진은 그 그래프로부터 실행 순서를 만들어 낸다.
  • FRP는 선언적 프로그래밍 방식을 사용한다. 프로그램이 어떤 일을 하는가보다는 어떤것이 되어야 하는가에 관심을 갖는다.
  • FRP는 근본적인 어떤것이다. 그래서 FRP는 발명이 아닌 발견이다.
  • FRP에서는 함수형 프로그래밍을 이벤트 기반 로직을 작성하기 위한 메타 언어로 사용할 수 있다.

'개발 > Functional Programming' 카테고리의 다른 글

[함수형 반응형 프로그래밍] #0. Intro  (0) 2020.10.17