규도자 개발 블로그

함수형 프로그래밍을 하면서 느낀 것들 본문

FP/Concept

함수형 프로그래밍을 하면서 느낀 것들

규도자 (gyudoza) 2022. 6. 13. 20:42

함수형 프로그래밍을 하면서 느낀 것들

함수형 프로그래밍과 함수형 프로그래밍을 위한 언어를 배우면서 느끼게 된 점이 참 많다. 공부해가면서 크고 작은 깨달음의 과정들이 있었고 또 깨달아가고 있는데 그것들을 지금 적어두지 않으면 경험상 곧 머지않아 당연하게 느끼고 머릿속에서 희석될 것이 뻔하기 때문에 의식적으로 남겨야할 것 같다는 생각이 들었다. 그런 의미로 기록한다.

 

 

1. Equal sign(=)에 대한 개념 회귀

프로그래밍을 배우지 않은 사람들은 절대 풀 수 없는 1차방정식 문제가 하나 있다.

x = x + 1

 

하지만 프로그래밍을 배운 사람이라면 x가 생략된 이전의 과정에서 이미 정수형으로 초기화된 상태이며, 그 초기화된 x에 1을 더해 새로이 또 x에 할당한 것이라는 걸 쉽게 유추할 수 있다. 그럼 보다 본질적인 질문을 해보려 한다.

 

과연 이것은 맞는 표현 방법일까?

 

이 질문에 대한 답은 쉽지 않다. 프로그래밍언어의 기반이 된 수학이라는 학문에 의거하면 이 표현방식은 단언컨대 틀리다. 그래서 실제로 R처럼 equal sign을 변수할당의 의미로 사용하고 있지 않는 프로그래밍 언어도 있다. R은 <-로 변수에 값을 할당한다. 어플리케이션 제작보다 공학적인 목적으로 많이 사용하는 R에서는 할당문이 <-인건 우연이 아닐 것이다.

 

그래서 함수형 언어에는 "할당(assign)"이라는 개념이 없다. 바인딩이 있을 뿐이다. 바인딩을 표현할만한 좋은 단어가 떠오르질 않으니 계속 바인딩이라고 표현하겠다. 아무튼 보통 c로부터 파생된 언어들의 equal sign이란 메모리에 값을 집어넣는 용도로 쓰인다는 점이다. 그 값이 리터럴이든, 주소값이든 말이다.

 

그래서 함수형에서는 할당이라는 단어가 없다. a = 1을 하면 1 = a가 된다.

Erlang/OTP 24 [erts-12.3.1] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit] [dtrace]
Interactive Elixir (1.13.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> a = 3
3
iex(2)> 3 = a
3

이게 가능한 것이다. 하지만 우리에게 익숙한 python에서 같은 동작을 시켜보면

Python 3.9.12 (main, Mar 26 2022, 15:51:15)
[Clang 13.1.6 (clang-1316.0.21.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> a = 3
>>> 3 = a
  File "<stdin>", line 1
    3 = a
    ^
SyntaxError: cannot assign to literal

맨 밑에 정확하게 나온다. 리터럴에 할당할 수 없습니다 (cannot assign to literal)

 

우리에게 익숙한 c계열의 언어에서는 a라는 메모리 공간에 3이 할당된 것이란 게 너무나도 명백하게 다가오지만
함수형에서는 a라는 메모리 공간에 3이 할당된 것이 아니라 a와 3이라는 값이 묶인(Binding) 것이라는 걸 알아야 한다. 현재 a는 3을 가리키고 있고, 3은 a를 가리키고 있는 것이다.

 

우리가 프로그래밍을 배우면서 equal sign에 대한 개념을 할당이라는 것으로 바꿨던 것처럼, 함수형을 배우면서 다시금 equal sign에 대한 개념을 수학에서 의미했던 그때로 돌아가는 것이 필요하다.

 

 

 

2. 명령형과 선언형. 그리고 리액티브 프로그래밍

함수형을 공부하면서 수없이도 마주치는 단어들이다. 그리고 또 자연스럽게 명령형 언어와 선언형 언어에 대해서 나오는 말이 있다.

명령형은 How에 집중하고, 선언형은 What에 집중한다.

 

솔직히 말하자면 난 함수형을 제대로 시작하기 전까진 이게 무슨 말인지 제대로 이해하지 못했었다. 하지만 함수형을 해가면서 정확하게 이해가 됐다. 거기에 리액티브 프로그래밍과 관련해서 찾아봤을 때 엑셀에 대한 비유가 있었는데 그것이 이 명령형과 선언형에 대한 이해로 이뤄지는 답이 되었다.

 

간단한 엑셀을 작성해보았다. bonus의 값을 읽어서 score에 더해 result의 값이 변경된다.

 

실제로 엑셀이라는 어플리케이션이 어떻게 작성돼있는지는 모른다. 하지만 명령형 혹은 선언형(리액티브 프로그래밍)으로 작성돼있다는 시나리오를 짜서 프로그래밍이 작동하는 과정을 설명해보자면 이렇다. 시작은 똑같이 bonus cell을 변경했다는 가정으로 한다.

 

명령형

bonus cell이 변경되었다.

  1. bonus cell을 참조하고 있는 셀들을 찾는다.
  2. 각 cell들에 변경된 bonus cell의 값을 보낸다.
  3. 변경된 bonus cell이 적용된 새로운 계산 결과를 각 cell들에 적용한다.

절차만 봐도 for문이 연상되는 형태이다. 그렇다면 선언형은 어떤식으로 작동할까.

 

선언형

bonus cell이 변경되었다.

  1. bonus cell이 변경됐다는 것을 알린다. (선언)
  2. bonus cell을 참조하고 있던 cell들은 각자 변경된 값에 맞춰 업데이트한다.

선언형은 어떻게 이것이 가능하느냐. 마법을 부리는 게 아니다. 간단하다. 그저 명령형 프로그래밍보다 좀 더 고차원적으로 추상화한 인터페이스를 적용해놓은 것이다.

 

좀 더 풀어서 설명해보면 이렇다.

  1. bonus cell을 참조하는 다른 cell(result cell)을 만드는 순간 bonus cell이라는 Stream이 하나 생긴다.
  2. 이 Stream은 변경이 일어났을 때 값이 변경됐다는 event를 뿌린다.
  3. 그러면 이 Stream을 참조하고 있는(Observing) cell들이 이 이벤트를 캐치하여 정해진 동작을 실행한다.
    이 시나리오에서는 값을 업데이트 하는 것이 동작이 된다.

이러한 과정들이 진행될 수 있게 고차원적으로 구현돼있는 상태에서 "선언"을 통해 이러한 일련의 과정들을 작동되게 하는 것, 그것이 선언형 프로그래밍이다.

 

천천히 읽어보면 알겠지만 선언형에 대한 설명에 리액티브 프로그래밍, 이벤트 드리븐 등에 대한 개념이 자연스레 녹아있는 것을 확인할 수 있다. 햇갈릴 필요 없다. 전부 다 일맥상통하는 부분이 있는 것 맞다. 그래서 내가 함수형을 배우면서 추상적으로, 혹은 막연하게 알고 있던 것들이 제대로 이해가 된 것이다.

 

 

 

3. 웹 어플리케이션 제작에 있어서 메모리 사용량과 속도는 더이상 최우선 고려사항이 아니다.

#include <stdio.h>
#include <stdlib.h>

void main()
{
    int* pPoint;
    pPoint = (int*)malloc(sizeof(int)*5);

    pPoint[0] = 25;
    pPoint[1] = 45;
    pPoint[2] = 50;
    pPoint[3] = 70;
    pPoint[4] = 99;

    int i = 0;
    for ( i = 0; i < 5; i++ )
        printf("pPoint[%d] : %d\n", i, pPoint[i]);

    free(pPoint);
}

c나 cpp를 해봤던 사람이라면 누구나 지옥같은 malloc과 free의 구렁텅이에서 허우적댔던 경험이 있을 것이다. c는 메모리라는 자원이 굉장히 귀했던 시대에 발명된 언어이다. 그리고 물론 지금도 메모리라는 자원이 귀중한 분야가 있다. 바로 몇날 며칠이고 주구장창 켜놓는 OS나 최소한의 사양으로 최적화를 해야하는 임베디드 분야이다.

 

당연스럽게도 이런 언어들은 저수준에서 직접 컴퓨터 자원에 접근할 수 있으니 속도가 빠르다. 하지만 웹 생태계를 주름잡고 있는 언어는 javascript이다. javascript는 못생기고 더럽고 비효율적인 막 만든 언어이다. 그래서 자꾸 그 단점들을 보완하기 위해 TypeScript라는 것에서부터 수많은 백엔드, 프론트엔드 프레임워크들이 난립하는 것이다.

 

왜 웹 어플리케이션에 있어서 더이상 메모리 사용량과 속도는 더이상 최우선 고려사항이 아닐까?

메모리 사용량은 하드웨어가 충분히 받쳐주고 클라우드 컴퓨팅 기술이 워낙에 발전했어서 크게 고려하지 않아도 된다. 그렇다면 속도는? 어플리케이션 속도가 아무리 빨라봤자 네트워크의 영향을 받는다. 자동차가 F1이어도 일반도로에서 몰면 결국엔 교통상황과 각종 환경상황에 영향을 받는 것처럼 말이다. 이것은 네트워크를 사용하는 어플리케이션들의 숙명이다.

 

웹 생태계에서 진짜로, 진실로서 속도가 중요했다면

이 프레임워크들의 이름이 모두에게 익숙해야했을 것이다. 이 순위는 techempower에서 진행한 각종 프레임워크들의 성능을 비교시험하여 점수를 부여한 것이다.(https://www.techempower.com/benchmarks/#section=data-r20&hw=ph&test=composite)

한 50위권은 넘어가야 우리에게 익숙한 Spring, node.js, FastAPI, nestJS, express등등이 보인다. 그리고 거의 매 라운드마다 바닥을 깔아주고 있는 django도 있다. 하지만 우린 위 프레임워크들로 만든 수많은 웹 어플리케이션이 존재한다는 것을 알고 있고, 또 계속해서 나오고 있고 우리가 사용도 하고 있다. 성능상으로 보면 c++로 만든 웹 프레임워크인 drogon(https://github.com/drogonframework/drogon)이 FastAPI라는 이름이 민망할 정도로 8배 Fast한데 왜 생태계는 나온지도 얼마 안 된 신생 프레임워크인 FastAPI가 압도적일까.

 

단순한 이유이다. 프레임워크의 사용자는 최종사용자, 예를들면 이커머스 플랫폼에서의 구매자가 아니라 그 프레임워크를 이용해 플랫폼을 개발하는 개발자이기 때문이다. 그리고 개발자들은 프레임워크를 선택할 때 속도보다는 생산성에 초점을 맞춘다. 시시때때로 바뀌고 추가되는 요구사항에 빠르게 대응하기 위해선 생산성이 가장 중요하기 때문이다. 그것이 비지니스적으로도 중요하기도 하고 말이다.

 

그리고 프레임워크레벨의 8배 성능 차이란 1s와 8s라는 죄악스러운 차이가 아닌 10ms와 80ms정도의, 사람이 느끼기에 미미한 수준에서의 차이다. 그리고 이정도의 성능차이는 네트워크환경이나 어플리케이션을 실행하는 수많은 조건, 예를 들면 데스크탑이냐 타블렛이냐 스마트폰이냐, 이와 더불어 크롬이냐 사파리나 파이어폭스냐 인터넷익스플로어냐, 와이파이냐 4G냐 유선이냐 등등 수많은 조건들에 따라서 어이없을 정도로 큰 차이를 보여준다.

 

아까 위에서 말했던 자동차와 도로의 비유를 또 빌려오자면, F1에서 1등한 차를 이용하면 뭐하나, 도로가 오프로드이고 시시때때로 병목구간이 발생하고 산사태가 일어나고 홍수가 일어나고 돌이 굴러오는데 말이다. 결국 용도와 환경에 맞는 기술선택이 필요하다.

 

 

 

4. 진짜 중요한 것

결국 웹 생태계에서 진짜 중요한 건 생산성이라는 것이다. 생산성이 좋은 언어나 프레임워크들이 개발자들의 선택을 받고 생태계가 형성이 되며 그것이 또 생산성의 향상을 불러오는 선순환구조가 만들어진다. 그렇다면 이 최초의 생산성은 어디에서 오는가, 수많은 요소들이 영향을 끼치겠지만 난 여기에 '납득할만한(acceptable)'이라는 키워드가 가장 크게 작용한다고 본다.

 

사실 acceptable이라는 건 경계가 모호하고 의미도 확실치 않은, 굉장히 자아가 희미한 그런 똑부러지지 않는 단어이다. 하지만 내가 그동안 각종 언어들이나 언어들의 탄생배경을 보면서 느꼈던 것들을 곱씹어보면 이것보다 더 적합한 단어를 찾기 힘들다. 그러니까 일종의 언어들의 계보를 따라가면서 전에 이렇게 했으니까 관성적으로 수용되는 점들이 많다는 점이다.

 

c에서 이렇게 했었으니까 이렇게 가자.
c++에서 이렇게 했었으니까 이렇게 가자.
java에서 이렇게 했었으니까 이렇게 가자.

 

이렇게 개발자들 사이에서만 공유되고 있는 그 어떤 이데아를 참조하는 듯 지엽적으로 acceptable한 결정사항들이 쌓이고 쌓여서 새로운 것들에 적용된다는 것이다. 나도 함수형 프로그래밍을 해보기 전까진 재할당이라는 개념이 없이 어떻게 프로그램을 작성하는지 감이 전혀 오질 않았다. 하지만 하고나서 알았다. 그냥 복제해서 새로 계산하고 또 만들면 된다. 거기에 프로그램의 문제를 야기하는 많은 부분들, 혹은 잠재적으로 위험성을 내포하고 있는 부분들이 오히려 할당이라는 개념때문에 생겼다는 것을 알았다. 멀리 갈 필요도 없었다. 다시 생각만 하려고 해도 머리가 지끈지끈한 c와 세마포어만 생각해봐도 그렇다. 다만 c는 os를 구축하고 있는 언어이기 때문에 초 경량화된 형태로 병렬성을 구현해야 하니 용인이 되지만 웹 어플리케이션에서는 어불성설이라고 본다. 이건 함수형의 기본 개념인 불변성과 복제를 이용해 간단하게 해결할 수가 있다. 그리고 이러한 시스템 프로그래밍 언어를 대체하기 위해 비교적 최근에 만들어진 Rust는 불변성 채택과 함께 Garbage collector 없이 소유권과 빌림이라는 개념을 통해서 메모리를 통제하고 있다. 왜냐. 아무리 생각해봐도 malloc과 free를 통해 모든 변수의 메모리 사용을 추적하고 통제하고 제어하는 건 생산성에 엄청난 악영향을 끼친다는 걸 알고 있기 때문이다.

 

 

하지만 그럼에도 불구하고... 가장 작은 단위도 함수, 모든 것들을 함수로 다루고 불변성과 순수함수라는 약속을 통해 이미 많은 문제들을 애초에 해결한 상태로 나왔지만 이미 기존의 프로그래밍 형태에 익숙한 현재 생태계에서는 이러한 개념들을 일부러라도 의식하지 않는 이상 아직은 not acceptable한 단계로 보인다.

 

사실 꽤 오랜 시간동안 함수형 프로그래밍 패러다임은 메인트렌드로서 수용되고 있지 못하다.

 

 

결국 돌고 돌아 결국 시장에서 선택받는 건 accpetable한, 기존의 개념을 알고 있는 사람들이 이어받아 섭취하기 쉽고 소화가 쉬운 것들이라는 점이다. 내가 맨 처음에 '납득할만한(acceptable)'이라는 키워드를 사용한 이유가 여기 있다.

 

 

해보면 해볼수록, 까보면 까볼수록, 알면 알수록 함수형 프로그래밍이라는 패러다임이 제시하는 방향이 맞다고 생각이 든다. 전에 썼던 글인 인디드(indeed)로 살펴본 프로그래밍 언어의 실력대비 연봉 테이블에서 함수형 프로그래밍 언어들이 연차대비 좋은 연봉을 받을 수 있는, 소위 "돈 버는 데 효율이 좋은 언어"라는 것도 통계적으로 확인이 된다. 하지만 위 구글 트렌드 그래프를 보면 알겠지만 아직 범용성이라는 측면에서는 의문부호가 많이 남는 게 사실이다. 그러니까 결국 함수형에 not acceptable한 생태계가 실제 함수형이 갖고 있는 생산성을 저평가하게 된 결과가 반영되어 생산성 이슈로 인해 선택되지 않은 것처럼 반영된다는 것이다.

 

 

 

결론

 

함수형 프로그래밍이 지향하는 바가 옳은가?

 

과연 옳은 방향의 프로그래밍 패러다임이란 무엇일까. 생산성이나 합리성, 수용성 등 다양한 요소들을 고려해봤을 때 보다 많은 부분들이 개선되는 방향이라면 옳은 방향이라고 할 수 있을 것 같다. 절차지향에서 객체지향으로 발전했듯이 말이다. 결론적으로 함수형으로 향하는 방향은 옳다고 생각한다. 하지만 함수형이 메인스트림이 될까? 거기에 대해서는 위에서도 말했듯이 잘 모르겠다. 잘 모르겠다가 아니고 오히려 내가 현업에 종사할 때까지는 안될 것 같다.

 함수형이 메인 패러다임이 되기 위해선 일단 보통 교과과정이라고 할 수 있는 c -> os -> java이러한 과정에 있어서 고민이 많이 필요한 것 같다. 실제로 거의 모든 os가 c로 작성돼있을 뿐더러, 실제 작성돼있는 수많은 앱이 c계열의 언어로 작성돼있고 거기에서 차용한 것들이 많아서 이것들에 대한 이해가 없이는 다른 언어들의 작동방식을 피상적으로만 이해할 수 있게 된다. 할당과 재할당이 너무나도 당연한 개념으로 자리잡고 있는 c를 배워놓고 갑자기 재할당이라는 개념이 없는 함수형을 배우면... 내가 그때로 다시 되돌아간다고 해도 굉장히 혼란스러울 것 같다. 결국 c -> os -> java라는 과정은 필연적이다. 그리고 또 이 언어의 계열이라는 것 또한 사람의 핏줄마냥 굉장히 강한 것이어서 라틴어계열의 언어에 익숙하다면 라틴어 계열의 언어들을 쉽게 배울 수 있는 것처럼 c에서 파생된 java, 그리고 java에서 파생된 kotlin등등으로 뻗어나갈 땐 굉장히 수월하지만 functional language들은 기반이 되는 개념부터가 다른 언어를 공부한다는 느낌이 많이 든다. 이러한 점들이 진입장벽으로 크게 작용하고 있을 것이다.

 함수형 프로그래밍이 용인되는 사람은 c에서 파생된 언어들을 사용하면서 이러저러한 문제들을 직접 부딪혀보고 고생해봤던 사람들일텐데 또 이 함수형 프로그래밍이라는 건 마주치지 않으려면 평생 마주칠 일이 없을 수도 있어서 의식적으로 찾아서 배워야 하며 또 그 과정에서 여태 배웠던 것들과는 많이 다른 개념들을 수용해야 한다. 나조차도 어쩌면 지금 이 타이밍에 이 회사를 다니고 있지 않았으면 평생 직접 사용해보지 않았을 수도 있다. 오히려 안해봤을 확률이 압도적으로 높다고 생각한다.

 

스택오버플로우 설문조사에서도 실제 함수형을 사용하고 있는 엔지니어들의 평균 연차가 높은 것도 이러한 현실들이 반영된 결과일 것이다. 한줄로 정리해보자면 현재 함수형 프로그래밍의 지위는 '방향은 옳지만 범용적으로 납득시키기는 어려운 상태'가 되는 것 같다. 물론 내 개인적인 생각이다.

 

Comments