Articles

함수형 프로그래밍의 설득력

자바와 하스켈의 차이는 어디에서 비롯되는가?

Table of Contents

이제 함수형 프로그래밍은 확실히 주류라고 할 만하다. 이미 많은 프로그래밍 언어가 함수형 프로그래밍의 핵심 개념을 차용했고, 그린랩스와 같은 회사들이 엔터프라이즈 규모에서 함수형 프로그래밍의 성공적인 사례를 만들어내고 있다. 함수형 프로그래밍에 관해 설명하는 도서나 자료도 쉽게 찾아볼 수 있다. 최근에는 프로그래밍 입문자들도 함수형 프로그래밍의 중요성을 인식하는 것 같다.

그러나 여전히 함수형 프로그래밍을 진지하게 생각하는 프로그래머는 많지 않다. 이제 막 함수형 프로그래밍이 무엇인지 알아보기 시작한 프로그래머는 스스로를 의심하곤 한다: ‘나는 매일 map, filter, reduce 함수를 사용하고 있으니 함수형 프로그래머인가?’. 자연스럽게 일급 함수를 다루고 리스트의 불변성을 보장하므로 맞는 말이지만, 프로그래머의 마음속에 있는 의심을 깨끗하게 씻어주지는 않는다. 마찬가지로 "여러분이 매일 사용하는 그 언어가 함수형 프로그래밍 언어이며, 우리는 모두 함수형 프로그래밍을 하고 있습니다"라는 말도 공허하다. 그것이 사실이고, 함수형 프로그래밍에 대한 심리적 장벽을 낮춰줄 수도 있겠지만, 사람들이 기대하는 함수형 프로그래밍은 그것이 아니기 때문이다.

그렇다고 펑터니, 모나드니 하는 개념은 소위 말하는 프로그래밍 언어 덕후들이나 파고드는 주제처럼 받아들여진다. 하스켈은 차라리 수학에 가까워 보인다. 함수형 프로그래밍의 각종 장점을 나열하며 이것이 당위인 것처럼 말해도 사람들에게는 전혀 설득력이 없다. 존 휴스(John Hughes)가 Why Functional Programming Matters[1]에서 언급했듯이, 여전히 함수형 프로그래머는 '고결해지기 위해 금욕적인 삶을 사는 중세 수도승’처럼 보인다. 휴스의 말대로 함수형 프로그래밍이 설득력을 얻기 위해서는 실질적인 이득을 증명해야 한다.

패러다임으로서의 함수형 프로그래밍

함수형 프로그래밍을 이해하기 위해 가장 먼저 하는 시도는 함수형 프로그래밍 언어라고 불리는 언어를 찾아보는 것이다. 무엇이 함수형 프로그래밍 언어인가? 프로그래머들 사이에서 주기적으로 논쟁거리가 되는 '순환 떡밥’이다. 하지만 이 논쟁은 함수형 프로그래밍에 대해 합의된 기준과 정의가 없어서 항상 소모적으로 흘러간다. 그런데 넓은 의미에서는 일급 함수(First-class function)를 지원한다면 함수형 프로그래밍 언어다. 어떤 언어에서 함수를 변수와 동일하게 다룰 수 있다면, 즉, 함수를 변수에 할당할 수 있고, 다른 함수의 인자로 함수를 전달할 수 있고, 함수가 함수를 반환할 수 있다면 그 언어는 일급 함수를 지원한다고 말할 수 있다. 흔히 함수형 프로그래밍의 핵심으로 언급되는 순수 함수나 불변성, 지연 평가, 참조투명성, 커링과 같은 것들은 언어 설계의 측면에서 봤을 때 일급 함수에 비하면 부차적이다.

이러한 정의에 따르면 일급 함수를 지원하는 자바스크립트, 파이썬, 자바는 모두 함수형 프로그래밍 언어라고 할 수 있다. 실제로 이들로 함수형 프로그래밍을 하는 것이 가능하다. 그런데 정말 자바가 하스켈과 같은 함수형 프로그래밍 언어라면 우리가 자바와 하스켈을 비교할 때 느끼는 그 미묘한 차이는 어디에서 오는 것일까? 일급함수에 대한 합의된 정의가 있음에도 불구하고 매번 무엇이 함수형 프로그래밍 언어인가에 대한 논쟁이 반복되는 이유는 이 미묘한 차이를 설명하기가 쉽지 않기 때문일지도 모른다.

이때 느껴지는 미묘한 차이에는 하스켈에 대한 오해도 한몫하겠지만, 나는 두 언어가 지향하는 패러다임이 가장 큰 차이라고 생각한다. 프로그래밍 패러다임이 토머스 쿤(Thomas Kuhn)이 말한 과학적 패러다임과 완전히 같다고 할 수는 없겠지만, 시대에 따라 프로그래머 커뮤니티가 일반적으로 지향하는 공통된 이론이나 법칙, 믿음, 가치가 있다는 점에서 개념을 끌어올 수 있을 것이다. 패러다임으로서 함수형 프로그래밍을 말하기 위해서는 함수형 프로그래밍 자체가 무엇인지 설명하는 것으로 충분치 않다. 프로그래밍 패러다임이 거쳐온 역사적 맥락을 살펴볼 필요가 있다.

현재 우리가 '함수’라고 부르는 프로그래밍 요소의 시초는 서브루틴(Subroutine)이다. 서브루틴은 그저 프로그램의 실행 흐름을 메인에서 서브로 점프시키는 용도였다. 이때 메인에 값을 반환하는 서브루틴을 함수라고 불렀다. 나중에는 함수와 서브루틴의 구분이 사라지고 모든 서브루틴을 함수라고 부르게 되었다. 이 함수가 수학에서의 함수와는 다르다는 점에 유의해야 한다. 함수 f(x)f(x)는 아무런 값을 의미하지 않거나(void), 인자 xx 자체를 변경하거나(call-by-reference), 함수 외부에 있는 임의의 변수를 변경할 수 있다.

실행 흐름을 다른 서브루틴으로 이동시키는 goto 구문은 실행 흐름을 마구 점프시키는 문제를 일으켰다. 이러한 문제의식을 바탕으로 구조적 프로그래밍 패러다임이 제시되었다. 다익스트라(Edsger Dijkstra)는 Go To Statement Considered Harmful[2]에서 goto 구문이 프로그램의 모듈화를 방해하며, 모든 프로그램은 순차, 분기, 반복이라는 세 가지 논리 구조만으로 표현할 수 있다고 주장했다. 오늘날 많은 프로그래머가 처음 C언어를 배울 때 goto 구문을 사용하지 말라는 경고를 받곤 하는데, 모두 구조적 프로그래밍 패러다임에서 비롯된 것이다. 대부분의 현대적 프로그래밍 언어에 goto와 같은 구문이 없는 이유도 구조적 프로그래밍 패러다임의 영향이라고 할 수 있다.

소프트웨어가 복잡해짐에 따라 프로그램의 모듈화는 더욱 중요한 문제가 되었다. 객체지향 프로그래밍 패러다임은 프로그램을 객체의 집합으로 보고, 그 객체 간의 상호작용으로 프로그램이 동작한다고 해석한다. 순수 객체지향 프로그래밍 언어를 표방하는 스몰토크(Smalltalk)는 오늘날 우리가 말하는 객체지향 프로그래밍 패러다임의 뼈대를 구축했다. 그리고 자바는 객체지향 프로그래밍의 상업적 성공을 추동했다. 지금도 기업, 정부, 학교 등 모든 곳에서 범용적으로 자바가 쓰이며, 이에 따라 자바가 추구하는 객체지향 프로그래밍 패러다임도 모든 곳에서 자연스럽게 받아들여졌다. 시간이 흐르면서 객체지향 프로그래밍 패러다임은 많은 수정과 변화를 거쳤지만, 지금도 객체지향 프로그래밍의 특성을 나열하는 것만으로 어느 정도 객체지향 프로그래밍 패러다임을 묘사할 수는 있다. 다만 일반적으로 객체지향 프로그래밍 패러다임의 핵심 특성으로 꼽는 캡슐화, 상속, 다형성, 메시지 패싱 등이 정말 핵심 특성인가에 대한 의견은 분분하다. 이들은 사실 객체지향 프로그래밍만의 전유물이 아니기 때문이다. 함수형 프로그래밍 언어에 대한 합의된 기준이 없어서 매번 논쟁거리가 되듯이, 객체지향 프로그래밍도 마찬가지인 것 같다.

이 모든 패러다임에서 함수는 특별 취급을 받았다. 다른 값과 달리 함수는 변수에 할당할 수 없고, 다른 함수에 인자로 전달할 수도 없고, 함수가 함수를 반환하는 것도 불가능했다. 함수가 수학에서의 함수와 다르게 동작하는 것도 여전했다. 따라서 함수를 호출했을 때 어떤 일이 일어날지 쉽게 예측할 수가 없었다. 함수형 프로그래밍 패러다임은 프로그램 자체를 입력과 출력이 있는 일종의 함수라는 관점으로 해석하고, 하나의 프로그램은 다양한 함수의 합성으로 구성된다고 말한다. 이런 식으로 프로그램을 해석함으로써 소프트웨어를 수학적으로 엄밀하게 정의하고, 증명할 수 있게 되었다. 이를 위해서는 이전과 달리 함수를 특별 취급하지 않도록 '정상화’할 필요가 있었다. 우리가 함수형 프로그래밍의 특성이라고 부르는 요소들이 모두 그것으로부터 출발한다.

다시 자바와 하스켈의 차이가 어디에서 비롯되는지에 대한 질문으로 돌아가자. 프로그래밍 언어의 정체성을 구분하는 가장 큰 기준 중 하나는 언어가 프로그래머에게 가하는 제약이다. 자바와 하스켈 모두 함수형 프로그래밍이 가능하지만, 그 제약에 차이가 있다. 자바는 순수 함수를 강제하지 않는다. 변수나 객체의 상태를 변경하는 데도 제약이 없다. 반면 하스켈에는 강한 제약이 있다. 하스켈로 객체지향 프로그래밍도 할 수 있지만, 그 역시 함수형 프로그래밍의 대원칙을 위배하지 않도록 고도로 추상화된 인터페이스 위에서만 가능하다.

패러다임은 그 언어를 만들고, 사용하는 커뮤니티를 비롯한 생태계를 지배한다. 자바에도 함수형 프로그래밍 요소가 많이 도입됐지만, 여전히 자바 생태계의 지배적 패러다임은 객체지향 프로그래밍이다. 자바의 표준 라이브러리나 외부 패키지는 객체지향 프로그래밍의 원칙을 따르며, 자바를 사용하는 기업은 신입 프로그래머에게 객체지향 프로그래밍의 원칙을 교육한다. 지난 수십 년간 자바로 작성된 거대한 레거시 코드는 객체지향 프로그래밍 패러다임을 충실히 반영하고 있다. 결국 커뮤니티 구성원들이 따르는 패러다임과 그 패러다임 안에서 옳다고 여겨지는 제약에 따라 언어가 구별되는 것이다. 그래서 일상에서 어떤 언어를 함수형 프로그래밍 언어라고 지칭할 때는 단순히 그 언어가 일급 함수를 지원하는 경우가 아니라, 그 언어의 생태계가 함수형 프로그래밍 패러다임을 지향하는 경우가 대부분이다.

여러 프로그래밍 패러다임을 지원하는 언어를 다중 패러다임 언어라고 부르기도 한다. 가령 스칼라에는 절차지향 프로그래밍의 제약과 함수형 프로그래밍의 제약이 공존한다. 여러 패러다임의 장점만 모았다니 너무나 혹하지만, 거의 모든 현대적 프로그래밍 언어가 여러 종류의 프로그래밍 패러다임을 채택하고 있기 때문에 다중 패러다임 언어라는 용어 자체에는 큰 의미가 없다. 요즘에는 어떤 언어가 다중 패러다임 언어라고 말할 때는 둘 중 하나인 것 같다: 첫 번째는 언어가 함수형 프로그래밍을 잘 지원한다는 의미일 때, 두 번째는 함수형 프로그래밍 언어가 대중성을 어필할 때.

함수형 프로그래밍의 실질적 이득

함수형 프로그래밍을 진지하게 생각하지 않더라도, 적어도 모나드가 무엇인지 궁금해하는 프로그래머는 상당히 많다. 그것을 이해하는 순간, 마치 '고급 프로그래머’로 거듭날 수 있다는 신화가 있다. 그만큼 모나드에 대해 설명하는 글도 엄청나게 많다. 하지만 무슨 의미가 있을까? 함수형 프로그래밍 패러다임에서 모나드는 너무나도 사랑스러운 개념이지만, 함수형 프로그래밍보다 모나드를 먼저 접한 프로그래머에게 모나드는 공포스러운 개념에 가깝다. 심지어 모나드가 엔도펑터 카테고리에 속한 모노이드에 불과하다[3]는 사실을 이해한 뒤에도 다른 프로그래머에게 함수형 프로그래밍의 현실적 이익을 설득하기는 쉽지 않다. 함수형 프로그래밍을 기웃거리는 프로그래머에게 모나드의 유용함을 설득하기 위해 상자에 값을 넣고 빼는 등 '초등학생도 이해할 수 있는 수준’의 예시를 들며 펑터, 모노이드, 모나드를 차례로 설명하는 것은 좋은 접근 방법이 아닐 수도 있다.

이때 독자에게는 프로그래밍 입문자가 객체지향 프로그래밍을 공부할 때 겪는 혼란과 완전히 같은 문제가 일어난다. 대규모 애플리케이션을 작성한 경험이 없는 프로그래머에게는 클래스니, 캡슐화니, SOLID니 하는 개념들이 모두 불필요해 보인다. 심지어 그 개념을 이해한 뒤에도 "그래서 뭐?"라는 질문에 대한 답을 얻지는 못한다. 함수형 프로그래밍의 수학적으로 엄밀한 개념은 정말 멋지고 중요하지만, 객체지향 프로그래밍의 방대한 원칙과 복잡성에 매몰되어 스스로를 자책하고 있는 프로그래머에게 그런 것은 크게 매력적으로 다가오지 않는다. 객체지향 프로그래밍 패러다임이 지배하는 생태계에서, 함수형 프로그래밍 패러다임을 통해 자연스레 달성하게 되는 그것은 프로그래머가 로버트 마틴(Robert Martin)의 클린코드[4] 원칙을 충실히 따름으로써 달성할 수 있는 것이기도 하기 때문이다. 각종 전술적인 개발 원칙과 디자인 패턴을 일일히 외우지 않아도 된다는 그 말은, 이미 객체지향 프로그래밍 패러다임에 따라 사고하는 프로그래머와 그러한 패러다임으로 점철된 코드베이스 위에서 그다지 설득력을 얻지 못한다.

패러다임의 변화를 마주한 프로그래머의 입장에서, 새로운 패러다임이 현재의 문제에 직접적인 해결책을 제시하지 않는 이상 그 패러다임을 받아들일 이유는 별로 없다. 그러나 결국 둘 중 하나다. 기존 패러다임에서 온갖 문제를 직접 경험하다가 환멸을 느껴 새로운 패러다임을 받아들이거나, 자신이 속한 생태계의 지배 패러다임이 변화하여 어쩔 수 없이 새로운 패러다임을 받아들여야 하는 것이다. 그 많은 모나드 튜토리얼과 "함수형 프로그래밍이란?"으로 시작하는 인터넷 글들, 함수형 프로그래밍을 지향하는 프로그래머들은 패러다임의 변화와 대중화를 위해 갖은 노력을 해왔다. 이제 함수형 프로그래밍은 대다수의 프로그래머가 어쩔 수 없이 받아들여야 하는 패러다임이 되었다. 어쩌면 높은 점유율을 가진 주류 언어의 설계에 함수형 프로그래밍 패러다임이 반영된 시점에 이미 그랬을지도 모르겠다. 쿤의 과학적 패러다임과 달리 프로그래밍 패러다임의 변화는 기존 패러다임에 대해 한편으로는 파괴적이기도, 한편으로는 상호보완적이기도 하다. 함수형 프로그래밍 패러다임은 객체지향 프로그래밍 패러다임이 쌓아 올린 성과를 완전히 폐허로 만들지 않는다. "은총알은 없다"[5]라는 오래된 소프트웨어 공학 격언은 다행스럽게도(?) 파괴적 혁명이 없을 것임을 예견한다.

내가 함수형 프로그래밍에 가장 큰 매력을 느낀 첫 번째 계기는 프로덕션 환경에서 아무 곳에서나 터지는 예외로 인해 큰 서비스 장애를 겪은 뒤였다. 프로그래밍 언어 차원에서 함수를 호출하는 쪽에서 해당 함수가 던지는 예외를 처리하도록 강제할 방법이 전혀 없었다. 프로그래머의 실수로 예외 처리를 하지 않으면 우리의 코드 베이스가 던진 예외로 인해서든, 외부 패키지가 던진 예외로 인해서든 API가 언제든 예상치 못하게 실패할 수 있었다. 그런 상황에서 러스트의 Result 타입은 대단히 매력적이었다. 하스켈이나 ML, 스칼라 등 다른 언어에서 Either라고 부르는 대수적 데이터 타입으로부터 영향을 받은 이 타입은 함수가 정상적으로 값을 반환할 수 있을 때는 Ok 타입에 값을 담아 반환하고, 그렇지 않을 때는 Err 타입에 에러를 담아 반환하여 에러 핸들링을 자연스럽게 강제하는 효과가 있다.

enum Result<T, E> {
    Ok(T),
    Err(E),
}
fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b != 0 {
        Ok(a / b)
    } else {
        Err("Cannot divide by zero".to_string())
    }
}

fn main() {
    println!("{:?}", divide(8, 2)); // Ok(4)
    println!("{:?}", divide(8, 0)); // Err(...)
}

비슷하게 Option 타입이 있다. 토니 호어(Tony Hoare)가 null을 고안한 과거에 대해 십억 달러짜리 실수[6]라고 회고했듯이, null은 오랜 시간 프로그래머를 고통에 빠뜨렸다. 다른 함수형 프로그래밍 언어에서 보통 Maybe라고 불리는 이 타입을 사용하면 코드에서 null을 완전히 없애는 것이 가능하다. 값이 존재하는 경우에는 Some 타입에 값을 담아 표현하고, 값이 존재하지 않는 경우에는 None 타입으로 표현하면 된다.

enum Option<T> {
    Some(T),
    None,
}
fn divide(a: i32, b: i32) -> Option<i32> {
    if b != 0 {
        Some(a / b)
    } else {
        None
    }
}

fn main() {
    println!("{:?}", divide(8, 2)); // Some(4)
    println!("{:?}", divide(8, 0)); // None
}

두 번째는 클린코드를 읽은 뒤였다. 클린코드가 406 페이지에 걸쳐 제시하는 갖가지 원칙 중 많은 것들을 함수형 프로그래밍 패러다임의 대원칙을 따르면 자연스럽게 달성할 수 있거나, 아예 신경 쓰지 않아도 된다. 코드를 작성할 때 굳이 클린코드 원칙을 떠올리지 않아도, 함수형 프로그래밍 언어의 견고한 타입 시스템이 클린코드를 강제하기 때문이다. 같은 맥락에서, 쌓여 있는 풀 리퀘스트를 리뷰할 때도 함수형 프로그래밍 패러다임이 절실했다. 부수 효과를 일으키지 않는 순수 함수만으로 구성된 코드는 참조 투명하다. 참조 투명한 코드는 실행 흐름을 추적하기 쉽고, 읽기도 쉽다. 이때 '참조 투명하다’라는 말은 프로그램을 변경하지 않고 표현식을 값으로 치환해도 그 의미가 같다는 뜻이다. 아래와 같이 두 변수를 더하는 함수 add를 보자.

add a b = a + b
main = print (add 1 2) -- 3

아래와 같이 (add 1 2)를 그 값인 3으로 치환해도 프로그램의 의미가 변하지 않으므로 함수 add는 참조 투명하다. add는 외부의 영향을 받지 않고 같은 인자에 대해 항상 같은 결과를 반환하며, 덕분에 프로그래머는 쉽게 함수의 결과를 예측할 수 있다.

main = print 3 -- 3

어떻게 보면 함수형 프로그래밍의 핵심에는 그다지 특별한 것이 없어 보인다. 그저 함수를 아주 평범하게 다룰 수 있어야 한다는 대원칙이 있을 뿐이다. 하지만 그 간단한 원칙이 만들어 내는 효과는 거대하다. 함수형 프로그래밍 패러다임을 이해하는 데 도움이 된 자료 몇 개를 추천하며 글을 마친다.

  • 폴 키우사노, 루나르 비아르드나손, “스칼라로 배우는 함수형 프로그래밍”, 류광 역, 제이펍, 2015: 함수형 프로그래밍의 개념과 적용을 차근차근 익힐 수 있는 모범적인 교재. 스칼라가 생소하더라도 크게 방해되지는 않는다. 코틀린에 익숙하다면 조재용, 우명인, “코틀린으로 배우는 함수형 프로그래밍”, 인사이트, 2019도 좋다. 내용이 간결하고, 연습 문제를 풀지 않아도 다음 내용을 이해하는 데 지장이 없다는 점에서 조금 더 편하게 읽을 수 있다.
  • 홍재민, 류석영, “Introduction to Programming Languages”: 카이스트 ‘프로그래밍 언어’ 과목의 교과서. 2021년 처음 공개된 이후로 꾸준히 개정되고 있다. 프로그래밍 언어론을 다루는 책이지만, 함수형 프로그래밍 패러다임을 이해하는 데 큰 도움을 준다. 프로그래밍 언어에 대한 관점과 사고방식을 완전히 뒤바꿀 수 있고, 끝까지 읽은 뒤에는 코드 한 줄 한 줄이 완결된 수식으로 보이는 놀라운 경험을 할 수 있다.
  • Brent Yorgey, “CIS194”, 2013: 펜실베니아 대학교 ‘Introduction to Haskell’ 과목의 강의 자료. 하스켈 공식 홈페이지에서는 2013년 봄 학기 자료를 추천하는데, 학기마다 내용이 조금씩 다르다. (가장 최근 자료는 2016년 가을 학기) 하스켈은 개념이 생소할 뿐이지 문법은 정말 단순하고 직관적이라 배우기 어렵지 않다. 함수형 프로그래밍을 공부하기 위해 굳이 하스켈을 알아야 하나 싶기도 하지만, 외부 세계에 영향을 받을 수밖에 없는 소프트웨어가 어떻게 그 '순수함’을 지킬 수 있는지 배우는 데는 하스켈이 좋은 교재가 된다고 생각한다.

  1. John Hughes, “Why Functional Programming Matters”, 1990. (한국어) ↩︎

  2. Edgar Dijkstra, “Go To Statement Considered Harmful”, 1968. ↩︎

  3. 제임스 아이리(James Iry)가 'A Brief, Incomplete, and Mostly Wrong History of Programming Languages’에서 언급. 종종 모나드의 난해함을 상징하는 농담처럼 인용된다. ↩︎

  4. 로버트 C. 마틴, “클린 코드”, 이해영, 박재호 역, 인사이트, 2013. ↩︎

  5. Frederick Brooks, “No Silver Bullet - Essence and Accidents of Software Engineering”, 1986. ↩︎

  6. Tony Hoare, “Null References: The Billion Dollar Mistake”, 2009. ↩︎

아치 리눅스로 15년차 넷북 되살리기

Eee PC 1000HE 위에 올린 아치 리눅스 32

철도 시간표가 유닉스 시간이 되기까지

시간과 컴퓨터 공학

Articles