많은 분이 B2R2 프로젝트를 보면서 왜 F#인가 궁금해합니다. 어찌 보면, F#이라는 언어가 프로젝트의 대중화에 걸림돌이 되는 것 같기도 합니다. 왜 잘 알려지지도 않은 언어인 데다가 사용하기도 불편해 보이는 함수형 언어를 써야만 했던 것일까요? 앞으로 두세 차례 포스팅을 통해 왜 우리가 F#을 썼는지, 우리의 비전은 무엇인지에 대해 이야기해보려 합니다.
B2R2는 닷넷(.NET) 라이브러리
정확히 말하면 F#은 B2R2의 개발언어이지, F#을 써야만 B2R2를 쓸 수 있는 것은 아닙니다. B2R2는 닷넷(.NET) 환경이 지원하는 어떤 언어로도 사용 가능한 라이브러리입니다. 따라서 F#은 필수가 아닌 권장 사항일 뿐이죠. 또한, 닷넷은 운영체제와 관계없이 동작하는 플랫폼입니다. 그러므로, B2R2는 기본적으로 어떤 환경에서든, 어떤 언어를 사용하든 관계없이 동작하는 것을 목표로 만들어진 바이너리 분석 플랫폼 입니다.

값 중심의 프로그래밍
그럼 왜 함수형 언어인 F#을 활용해 B2R2를 개발했을까요? B2R2는 주석을 제외한 순수 코드만 10만 줄에 육박하는 프로젝트입니다—단, 공개된 부분만 보면 약 8만 줄 정도 됩니다 [1]. 이렇게 크고 복잡한 소프트웨어 시스템을 만들다 보면 당연히 코드를 유지 보수하기가 어려워지고, 버그도 많이 생겨납니다.
하지만, F#과 같은 값 중심의 언어를 활용하면 이러한 걱정을 상당 부분 절감할 수 있는데, 오늘은 그 이유에 대해서 알아보도록 하겠습니다. 값 중심의 언어라는 표현이 아마 많이 생소할 수 있지만, 값 중심의 언어라는 것은 함수형 언어의 또 다른 말입니다. 모든 연산이 값을 중심으로 이루어진다고 해서 값중심 언어, 또는 값중심 프로그래밍이라 이야기합니다. 이 말은 이미 2000년대 초반 이광근 교수님께서 국내에 소개하신 개념이기도 합니다만 [2], 저는 다른 각도에서 값중심 프로그래밍의 가치를 설명해보고자 합니다.
우리가 프로그래밍 언어에서 정의하는 순수한 함수들은 수학에서 말하는 함수와 다를 바가 없습니다. 우리가 수학에서 정의역이라 부르는 집합 X는 입력 타입에 해당하고, 공역이라 부르는 집합 Y가 함수의 리턴 타입이 됩니다. 그리고 함수는 두 타입 사이의 대응 관계를 정의합니다. 함수의 정의에 따라, 순수한 함수의 경우에는 함수의 입력이 정해지면 그 리턴(출력)값은 항상 고정적으로 결정됩니다.
세상에 모든 함수가 다 순수하기만 하다면 좋겠지만, 안타깝게도 소프트웨어에서 접하는 함수는 많은 경우 불순합니다. 게다가 대부분의 프로그래밍 언어에서는 불순한 함수를 정의하는 것이 아주 자연스럽게 가능합니다. 예를 들어, 임의의 숫자를 입력으로 받되, 입력된 숫자가 1이면 함수가 호출된 횟수에 따라 1부터 계속 증가하는 값을 반환하는 함수를 생각해봅시다. 또한, 그 함수는 입력이 1이 아닌 경우에는, 항상 0을 반환한다고 가정해봅시다. 이 함수는 같은 입력(숫자 1)에 대해 항상 다른 값을 반환하므로 불순합니다. 이러한 함수를 Python을 이용해 구현한다면 아래와 같을 겁니다.
cnt = 0
def func(x):
global cnt
cnt += 1
if x == 1:
return cnt
else:
return 0
여기에는 몇 가지 주목할 점이 있습니다. 먼저, 이 함수를 정의하기 위해 함수 밖에 전역변수인 cnt를 활용했다는 점입니다. 또한, 해당 전역변수는 4번 라인에서 1씩 증가하고 있습니다. 즉, 매번 호출할 때마다 새로운 값이 할당되는 것이죠. 변수는 말 그대로 “변할 수 있는 수”입니다. 그러니까 새로운 값이 할당될 수 있습니다.
그러면 이제 이 함수의 구현을 다른 각도에서 생각해봅시다. 전역변수를 사용하지 않고 구현할 수 있을까요? 아니, 아예 변수라는 것을 쓰지 않은 채로 상수 만을 가지고 한다면? 다시 말해 불변의 값만으로 불순한 함수를 구현할 수 있을까요?
당연히 당장 답이 떠오르지 않을 것입니다. 실제로 불순한 함수를 구현하기 위해서는 변수와 같이 상태를 변화할 수 있는 무언가가 필요하기 때문입니다. 이를 반대로 생각해보면, 불변하는 “상수”만을 가지고 함수를 짜면, 그 함수는 순수할 수밖에 없습니다. 이러한 값의 불변성은 함수형 프로그래밍(또는 값중심 프로그래밍)을 구성하는 핵심 재료가 됩니다.
값중심 프로그래밍의 가치
함수형 프로그래밍이란 순수한 함수를 중심으로 이루어지기 때문에 함수형으로 만들어진 코드는 불변의 값을 중심으로 구성됩니다. 그렇기에 우리는 함수형 프로그래밍을 값중심 프로그래밍이라고도 부릅니다. 값중심 프로그래밍에서는 한번 생성된 값은 영원히 변하지 않습니다. 물론, 그 값이 아예 사라지는 것(메모리상에서 해제되는 것)은 가능하지만, 값이 변화하는 일은 없습니다.
그러면 불변의 값이 갖는 의미는 무엇일까? 우리말에 “값”이라는 단어는 어떠한 수치를 의미하는 것 외에도, 중요성이나 의의를 나타내는 말로 사용되곤 합니다. 예를 들어, 값이 있는 삶을 살았다고 하면, 그만큼 의미 있고 중요한 삶을 살았다는 이야기죠. 또한, 우리가 무언가를 비교할 때는 값을 비교하는 경우가 많습니다. 두 개의 값을 비교해서 높고 낮음에 따라 중요한 것과 중요하지 않은 것을 나누게 되죠.
그런데 가만히 생각해보면, 값을 비교한다는 말에는 비교의 대상이 되는 값이 변화하지 않는다는 가정이 숨어있습니다. 우리가 키를 재서 170cm라는 값을 얻게 되면, 그 값은 변하지 않는 사실을 나타냅니다. 설사 키가 더 자라나서 171cm가 되더라도 처음에 쟀던 170cm라는 값이 갖는 의미가 변하지는 않죠. 그래서 우리는 170cm라는 값을 다른 값과 비교하고 그것을 통해 키의 높고 낮음을 비교할 수 있는 것입니다. 170cm라는 값이 171cm보다 작다는 것은 항상 성립하는 진실이며, 이 진실은 변하지 않죠. 하룻밤을 자고 일어났더니 170cm가 171cm보다 커지는 일은 없다는 겁니다.
그래서 값이라는 것은 불변이라는 말을 앞에 붙이지 않더라도 그 자체로 변하지 않는 사실(fact)을 의미합니다. 또한 변수나 객체라는 것은 값을 담는 그릇일 뿐입니다. 그릇에 여러 값을 담을 수는 있지만, 그릇에 담겼던 값 자체가 변하지는 않습니다. 문제는 대부분의 현존 프로그래밍 언어는 그릇 중심의 언어이기 때문에, 진실이 무엇인지 알기가 어렵다는 겁니다. 왜냐면 그릇에 담길 수 있는 진실이 계속 바뀔 수 있으므로, 내가 값(진실)을 그릇에 담고 있는지, 또는 담을 수 있는지를 미리 알아내기가 어렵다는 겁니다.
반면 값중심 프로그래밍에서는 불변하는 진실을 중심으로 코딩을 합니다. 따라서 값중심 프로그래밍이라는 말은 불변성 중심의 프로그래밍이라는 말과도 크게 다르지 않습니다. 함수형 프로그래밍, 값중심 프로그래밍, 불변성 중심의 프로그래밍은 사실상 모두 같은 말이며, 그 핵심 가치는 바로 값의 불변성에 있습니다. 그렇다면 좀 더 구체적으로, 값의 불변성이 갖는 의미에 대해서 알아봅시다.
값은 공유된다
첫째, 값은 공유될 수 있습니다. 170이라는 값은 세상 어디에 가더라도 170을 나타냅니다. 누구에게 170을 말하거나 공유해도 전혀 문제가 되지 않죠. 마찬가지로 값은 프로그램 어디서 사용되더라도 전혀 문제가 없습니다. 함수의 인자로 전달이 되거나 반환 값으로 받거나 하더라도 아무 문제가 없죠. 그런데 만약 값을 담는 그릇을 공유한다면 어떻게 될까요? 그릇의 내용물이 바뀔 수도 있고, 새로운 것이 첨가될 수도 있고, 내용물이 완전히 사라질지도 모릅니다. 따라서 그 그릇을 다시 돌려받더라도 그 내용물을 하나하나 확인하기 전까지는 안심할 수 없겠죠. 따라서 그릇은 값보다 공유가 어렵습니다.
값은 예측 가능하다
둘째, 값을 활용한 연산은 항상 그 결과를 예측할 수 있습니다. 함수의 관점에서 보면 입력값이 그릇이면 그 내용물에 따라서 해당 함수의 출력값이 변할 수밖에 없습니다. 따라서 함수의 동작을 예측하기가 어려울 수밖에 없습니다; 함수가 불순하므로. 하지만 함수가 값을 이용해 구현되어있고, 입력되는 인자도 값이면 그 함수의 동작은 항상 예측 가능한 범위 내에 있게 됩니다.
값은 설명이 편리하다
셋째, 값은 설명이 편리하고 그것을 설명하는 언어에 종속되지 않습니다. 우리가 일(1) 이라는 숫자를 영어로 one이라고 부른다고 그 값의 의미가 변화하지 않듯이, 값은 항상 고유한 그 “값”을 갖습니다. 프로그래밍 언어에서도 마찬가지로 사용되는 언어가 Python이건 F#이건 값을 나타내는 데에는 그 표현이 다를 뿐이지 해당 진실은 변화하지 않습니다. 따라서 값은 프로그램에서 프로그램으로, 하나의 언어에서 다른 언어로 전달이 쉽습니다. 반면 그릇을 전달이 굉장히 어려운데, 예를 들어 Python으로 만들어진 객체를 C++로 전달하기란 매우 어렵습니다. 객체에 달린 수많은 메서드(method)를 모두 다시 구현해야 할 뿐 아니라 언어에서 표현하는 상속 관계나 기타 여러 가지 사항을 새로운 언어로 표현해야만 가능하기 때문입니다. 이것은 마치 우리말에서 “국물이 시원하다.”라는 말을 영어의 “The soup is cool.”이라는 말로는 직역할 수 없는 것과 마찬가지라 하겠습니다. 값이 아닌 것을 설명하는 것은 어렵고, 다른 언어로 그것을 표현하기도 어렵습니다.
값은 조립가능하다
마지막으로, 값은 여럿이 모여도 값을 이루며, 쉽게 조립 가능합니다. 예를 들어 1과 2를 모아 하나의 튜플(tuple)을 만들면 (1, 2)라는 새로운 값이 됩니다. 튜플 값은 불변하는 성질을 가지며, 그 자체를 설명하는 데에 어려움이 없습니다. 숫자가 세 개, 네 개, 혹은 그 이상이 모여도 마찬가지이며, 숫자가 아닌 다른 값이 어떠한 형태로 모여있더라도 우리는 그것을 값으로 생각할 수 있습니다. 단, 여기서 튜플은 그릇이 아니어야만 합니다. 만약 튜플을 값이 아닌 그릇으로 구현한다면, 그릇에 담긴 내용물을 언제든지 바꿀 수 있기 때문입니다. 예를 들어, (1, 2)라는 튜플을 만들고 나면, 첫 번째 원소를 3으로 치환하여 (3, 2)로 변화시킬 수 없어야 합니다. 불변성이 보장된다는 전제하에 이렇게 여러 개의 값을 다른 하나의 값으로 묶을 수 있는 능력은 값중심 언어(즉, 함수형 언어)의 핵심 재료 중 하나입니다.
마치며
지금까지 값의 불변성과 값중심 프로그래밍이 갖는 가치에 관해 이야기해보았습니다. 이러한 값중심 프로그래밍의 장점은 크고 복잡한 소프트웨어를 만들 때 더욱 극대화됩니다. 특히 코드가 간결해지고 설명이 편리해지는 것이 가장 큰 장점이라 하겠습니다. 최대한 어려운 용어를 배제한 채 의미를 설명하는 데에 초점을 두었지만, 직접 해보지 않는 한 값중심 프로그래밍의 가치를 깨닫기는 어렵다고 생각합니다. 하지만, 이 포스팅이 값중심 프로그래밍을 접하는 사람들에게 좋은 밑거름이 되기를 희망하며 글을 마칩니다.
[1] B2R2, https://github.com/B2R2-org/B2R2
[2] 값중심의 프로그래밍, 이광근, http://kwangkeunyi.snu.ac.kr/~kwang/paper/maso/1.html
차상길 교수는 카네기멜론 대학교에서 2015년에 박사학위를 취득하였으며, 2020년 3월부터 사이버보안연구센터장으로 역임중이다. 현재 주 연구 분야는 소프트웨어 보안 및 프로그램 분석이며, 최근에는 차세대 바이너리 플랫폼을 만드는 연구에 매진하고 있다.
그동안 ‘함수형 프로그래밍’ 관련 글을 보면 단순히 “~는~다”라는 식의 설명만 있어서 이해가 어려웠는데, 처음으로 이 글을 읽고 해당 프로그래밍 패러다임의 철학 자체를 이해할 수 있었던 것 같습니다! 정말 좋은 글입니다 다음 글도 기대되네요 ^^
다른 함수형 언어로, ocaml 같은 것도 있는데 어떤 이유로 F#을 사용하셨나요?
답변이 많이 늦어져서 죄송합니다. F#은 OCaml보다 크고 복잡한 시스템을 만들기에 더 적합한 실용적인 언어이기 때문입니다. 가장 먼저 멀티코어 지원이 안된다는 점이 있겠고, 패키지 매니징 시스템(opam 등)이 구식이고, 지원 라이브러리도 한계가 있고, 등등 다양한 이유가 있을 것 같습니다.