hexdrinker

함수형 프로그래밍

-5 min read

함수형 프로그래밍이란 개념은 프로그래밍 그 자체보다 앞서 등장했다. 무엇보다 함수라는 것은 수학에서 온 것이니까.

이 패러다임에서 핵심이 되는 기반은 람다 계산법(람다 대수, Lambda Calculus)로 알론조 처치(Alonzo Church)rk 1930년대에 발명했다.

💬

대수라는 용어는 대부분은 algebra에 대응되나 람다 대수는 calculus로 쓰고 있다.


calculus는 미적분학을 가리키는 말인데 그 어원은 '작은 돌', '조약돌'을 뜻하는 라틴어 calculus에서 왔다. 고대에는 조약돌을 사용하여 계산을 했는데 이것이 점점 확장되어서 셈을 하다, 계산을 하다라는 뜻으로 이어졌고 복잡한 수학적 계산법인 미적분학으로 자리잡게 된 것이다.


현대의 대수는 추상적인 구조(군, 환, 체)를 다루지만, 고전의 대수는 기호를 두고 일정한 규칙을 통해 구체적인 수의 계산과 방정식의 근을 구하는 것에 집중했다. 람다 대수에 왜 calculus라고 이름이 지어졌냐면 미분처럼 수나 기호를 체계적인 규칙에 따라 조작하여 결과를 도출하는 형식적인 논리 계산 체계이기 때문이다.


위와 같은 이유로 람다 대수라는 이름이 붙은 것이다. 오늘날 새로 번역된다면 람다 계산법이라 하지 않을까? 이미 그렇게 쓰고 있는 사람들도 있는 것 같지만..

정수를 제곱하기

아래는 25까지의 정수의 제곱을 출력하는 간단한 코드이다.

public class Squint {
  public static void main(String args[]) {
    for (int i = 0; i < 25; i++) {
      System.out.println(i * i);
    }
  }
}

LISP에서 파생한 클로저(Clojure)는 함수형 언어로, 클로저를 이용하면 위 코드를 다음과 같이 구현할 수 있다.

(println (take 25 (map (fn [x] (* x x)) (range))))

뭔가 복잡해 보이지만 println, take, map, range는 모두 함수다. LISP에서는 함수를 괄호 안에 넣는 방식으로 호출한다. 그것 알고 다시 바라보면 안쪽에서부터 함수를 호출하며 시작하며 읽게 될 것이다.

클로저와 자바의 극단적 차이가 보이는가?

자바는 가변 변수(mutable variable)를 사용하는 반면 클로저는 가변 변수가 하나도 없다. 변수가 중간에 변경될 여지가 사라지는 것이다.

불변성과 아키텍처

아키텍처를 고려할 때 이러한 내용이 왜 중요한가?

경합 조건, 데드락, 동시성 문제 모두 가변 변수에서 비롯된다. 다시 말해 동시성 어플리케이션에서 마주치는 모든 문제는 공유된 가변 상태가 없다면 원천적으로 발생하지 않는다.

이 때문에 아키텍처 설계에서 상태를 어떻게 정의하고, 누가 언제 변경할 수 있는지를 명확히 하는 것은 단순한 구현 상의 선택이 아니라 시스템의 안정성과 예측 가능성을 좌우하는 핵심 요소가 된다.

💬

이러한 문제들은 흔히 서버나 인프라 레벨에서만 발생하는 것으로 인식되곤 한다.
하지만 그렇다고 해서 웹 브라우저, 즉 클라이언트 사이드에서 가변 변수로 인한 문제가 존재하지 않는 것은 아니다.


자바스크립트는 싱글 스레드 언어이지만, 브라우저는 네트워크, 타이머, 렌더링 등을 멀티 스레드 환경에서 처리한다. 이 과정에서 비동기 작업과 이벤트 루프를 통해 서로 다른 시점에 실행되는 코드들이 하나의 가변 상태를 공유하게 되며, 이는 서버에서의 동시성 문제와 본질적으로 동일한 형태의 오류를 만들어낸다.

가변성의 분리

불변성과 타협하는 방법 중 하나는 서비스를 가변 컴포넌트와 불변 컴포넌트로 분리하는 일이다.

불변 컴포넌트에서는 순수하게 함수형 방식으로만 작업이 처리되며, 어떤 가변 변수도 사용되지 않는다. 불변 컴포넌트는 가변 컴포넌트와 적절하게 조합되어 쓰인다.

현명한 아키텍트라면 가능한 많은 처리를 불변 컴포넌트로 옮겨야 하고, 가변 컴포넌트에서는 가능한 많은 코드를 빼내야 한다.

💬

리액트의 철학 중 하나는 컴포넌트와 hook은 순수해야한다는 것이다.
(내가 이 때까지 작성한 코드는 과연 순수했을까,,,,)


그렇다면 리액트는 왜 이토록 순수성에 집착할까? 그 출발점은 “UI를 어떻게 변경할 것인가”가 아니라, “UI 변경을 어떻게 관리할 것인가” 라는 질문에 있다. 리액트는 UI를 직접 조작하는 대상으로 보지 않고, 상태로부터 계산되는 결과로 취급한다. 이 관점에서 렌더링은 명령이 아니라 계산이며, 계산이기 때문에 반드시 순수해야 한다.


이러한 순수성은 사이드 이펙트를 줄이고 코드의 실행 결과를 예측 가능하게 만들며 디버깅과 테스트를 훨씬 단순하게 만든다. 이 철학은 선언적인 코드 스타일과도 이어진다. 명령형 코드는 어떻게(How) UI를 변경할지를 단계적으로 서술한다면 선언형 코드는 무엇(What)이 화면에 나타나야 하는지에 집중한다. UI를 직접 조작하는 대신, 현재 상태에 대한 결과를 선언함으로써 변경 과정은 리액트에게 위임하는 것이다.


이 구조는 함수형 프로그래밍의 사고방식과도 맞닿아 있다. 함수형 프로그래밍에서 프로그램을 값의 변환으로 바라보듯, 리액트 컴포넌트 역시 상태를 입력으로 받아 UI라는 값을 계산하는 함수로 동작한다. 리액트가 순수성을 요구하는 이유는 단순한 스타일의 문제가 아니라, UI를 계산 가능한 값으로 다루기 위한 근본적인 설계 선택인 셈이다.

결론

요약하면

  • 구조적 프로그래밍은 제어흐름의 직접적인 전환에 부과되는 규율
  • 객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 부과되는 규율
  • 함수형 프로그래밍은 변수 할당에 부과되는 규율

각 패러다임은 코드를 작성하는 방식의 형태를 한정시켜서 좀 더 좋은 아키텍처를 만들 수 있는 가이드라인을 제시한다.

반세기 넘게 프로그래밍 언어와 소프트웨어가 발전해왔지만 소프트웨어는 순차, 분기, 반복, 참조로 구성된다는 핵심은 여전하다.

💬

OOP와 FP는 양립 불가능한 개념이 아니며, 어느 한쪽이 우월하다고 보기도 어렵다. 두 패러다임은 지향하는 바가 분명히 다르고 그로 인해 자연스럽게 잘 어울리는 사용 영역 역시 달라진다.


OOP는 시스템을 거시적인 관점에서 바라볼 때 강점을 가진다. 애플리케이션의 전체적인 구조를 설계하고 컴포넌트와 모듈 분리하며 변화에 유연한 아키텍처를 만드는 과정에서 빛을 발한다. SOLID 원칙이나 디자인 패턴들과 같이 우리에게 익숙한 많은 방법론들은 객체 지향을 베이스에 두고 있는 이유도 여기에 있다.


반면, FP는 미시적인 관점에서 강력하다. 가변 변수를 최소화하여 사이드 이펙트를 줄이고, 함수들을 파이프라인처럼 조합함으로써 코드의 흐름을 명확히 드러낸다. 잘 만들어진 함수 이름 자체가 추상화의 단위가 되며, 로직은 예측 가능하고 안전해진다. 코드가 예뻐지는(?) 효과는 덤이다.


OOP는 변화에 유연한 구조를 제공하고 FP는 그 구조 안에서 안전하고 예측 가능한 로직을 담당한다. 이 둘은 상호간에 적절한 쓰임새가 있고 그것이 조화롭게 잘 사용될 때, 견고하면서 유연한 좋은 아키텍처의 서비스를 만들 수 있다.

위와 같은 생각을 갖고 있었는데 공부하면서 자료를 이리저리 찾다가 발견한 영상이 있다. 토스 FE 챕터에서 진행한 함수형 프로그래밍 관련 모닥불 영상인데 나의 의견과 거의 흡사한 이야기가 나왔다.

OOP와 FP에 대한 관점과 견해에 대한 이야기를 더 듣고싶다면 참고해도 좋을듯 하다.