[기술] 함수형 프로그래밍

 


인사말


안녕하세요. 오랜만에 블로그에 글을 남깁니다. 오늘은 기술 관련 주제로, 최근에 흥미롭게 접해본 함수형 프로그래밍(Function Programming) 에 대해 이야기해보려 합니다.

객체지향 프로그래밍(OOP)은 어느 정도 개념적으로 익숙해졌지만, 직접 구현할 때 여전히 어려움을 느끼곤 합니다. 반면 함수형 프로그래밍은 처음 접했을 때 개념 자체가 낯설어 이해하는 데 다소 시간이 걸렸습니다.

이번 글에서는 함수형 프로그래밍을 공부하면서 이해하게 된 개념들과 그 과정을 정리해보았습니다. 입문자 관점에서 느낀 점들도 함께 담았으니, 가볍게 읽어주시면 좋겠습니다.



순수 함수 


함수형 프로그래밍에서의 함수는, 제가 느끼기에는 일종의 독립적인 존재처럼 느껴졌습니다. 외부의 상태나 환경에 의존하지 않고, 주어진 입력만으로 항상 일정한 출력을 낸다는 점에서 그렇습니다.

예를 들어 저는 평소에 코드를 작성할 때 힙 메모리를 많이 사용하는 편인데, 이는 어떤 변수를 만들어두고 그걸 함수에 넣어서 돌리는 방식에 익숙하기 때문입니다. 하지만 함수형 프로그래밍의 순수 함수 개념은 그런 구조와는 사뭇 다릅니다. 주로 스택 메모리 기반으로 작동하고, 외부 상태나 변수 없이 입력값에 따라 항상 동일한 결과를 반환하기 때문에 코드의 안정성과 예측 가능성을 높여줍니다. 무엇보다 코드의 단일화—즉, 복잡한 흐름 없이 명확하게 기능이 분리된 구조를 만들기에 매우 유용합니다.

예제를 통해 살펴보면 다음과 같습니다:


const x = Math.random();

// 순수함수의 개념
const functionXY = (x, y) => {
  return Math.pow(x, 2) + (2 * x * y) + Math.pow(y, 2);
};

const a = 2, b =3
console.log(functionXY(a, b))


이 함수는 입력 값 a, b만으로 결과를 도출하며, 외부 변수나 상태에 영향을 받지 않습니다. 즉, ab가 동일하다면 이 함수는 언제나 같은 값을 반환하게 됩니다.

이러한 특성 덕분에 순수 함수는 재사용성테스트 용이성 측면에서 큰 이점을 가집니다. 한 번 테스트를 통과한 순수 함수는 이후 코드 베이스에서 신뢰를 가지고 여러 곳에서 활용할 수 있고, 유지보수 시에도 예기치 않은 부작용(side effect)에 대한 걱정이 줄어듭니다.



불변성


함수형 프로그래밍을 공부하면서 가장 강하게 느꼈던 점은, **"변수는 함수를 거친다고 해서 바뀌어서는 안 된다"**는 원칙이었습니다. 즉, 어떤 값이 함수에 들어간다고 해서 그 값 자체가 변경되는 일은 없어야 한다는 것입니다.

예를 들어 C++ 같은 언어에서도 int, bool처럼 기본 타입은 스택 메모리에 저장되며, 함수 호출이 끝나면 소멸되는 구조입니다. 하지만 배열, 해시맵, 클래스와 같은 복합 자료형은 일반적으로 힙 메모리를 사용하게 되고, 이 경우 함수 호출 중에 해당 값이 의도치 않게 변경될 위험이 있습니다. 물론 C++에서도 복사를 하거나 const를 이용해 안전하게 관리할 수는 있지만, 기본적으로는 개발자가 그 점을 항상 신경 써야 합니다.

반면 함수형 프로그래밍은 애초에 값을 변경하지 않는 방식을 권장합니다. 값을 변경하는 대신 새로운 값을 만들어서 반환하는 방식으로 동작하기 때문에, 부작용(side effect)이 생길 여지가 줄어들고, 같은 값을 가지고 어디서든 안전하게 재사용할 수 있게 됩니다.

예제를 통해 살펴보겠습니다:

"use strict";

const Arr = Array.from({length : 10}, () => 0);

const SortArr = Arr.reduce((prev) => {
  const currentValue = prev.length > 0 ? prev.at(-1) : 0;
  prev.push(currentValue + 1);
  return prev;
}, [])

console.log(SortArr);


이 코드에서는 처음에 모든 값이 0으로 채워진 배열을 생성한 뒤, reduce를 사용해 1부터 10까지의 값을 차례로 누적해서 새로운 배열을 만들어냅니다. 이때 중요한 점은, 초기 배열 Arr은 전혀 변경되지 않고, 새로운 배열 SortArr가 만들어졌다는 것입니다.

이러한 방식이 바로 함수형 프로그래밍의 핵심입니다.

  • 입력을 바꾸지 않는다.

  • 결과만 새롭게 만든다.

  • 어디서 호출하든 동일한 결과를 낸다.

이런 구조 덕분에 코드는 더 예측 가능하고 안정적이 됩니다. 테스트도 쉬워지고, 여러 곳에서 재사용할 수 있는 함수 중심의 설계가 가능해지죠.



람다 함수


함수형 프로그래밍에서는 람다 함수(lambda function) 라는 개념이 핵심적인 역할을 합니다.
람다 함수는 간단히 말해 이름이 없는 함수, 혹은 익명 함수로, 주로 일시적인 계산이나 콜백 로직에서 자주 사용됩니다.

이 개념은 사실 수학에서 말하는 함수 개념과도 밀접하게 닮아 있습니다. 수학에서는 함수가 아래처럼 정의되죠:

  • 변수: x, y

  • 정의: f(x, y)

  • 출력: z = f(x, y)의 형태로 결과값 도출

이러한 구조는 함수형 프로그래밍에서도 그대로 반영됩니다. 즉, 어떤 입력값이 주어졌을 때 그에 따라 정해진 방식으로 결과값을 만들어내는 구조이며, 그 안에서 상태 변화 없이 순수하게 계산만 수행됩니다.

예제를 하나 살펴보면 다음과 같습니다:

"use strict";

const x = 10, y = 5

const fx = (x, y) => {
  return x + y;
}

fx(x, y);


이 함수 fxxy라는 입력값을 받아 더한 값을 반환합니다. 외부 상태나 변수에 영향을 받지 않으며, 같은 입력에 대해서는 항상 같은 출력을 보장합니다.
이는 함수형 프로그래밍의 중요한 특징 중 하나이며, 복잡한 프로그램 로직을 단순하고 명확하게 구성할 수 있도록 도와줍니다.

더 나아가, 이런 람다 함수는 다른 함수에 전달되거나, 함수에서 반환될 수도 있습니다. 이를 통해 함수 자체를 데이터처럼 다루는 고차 함수(Higher-Order Function) 설계가 가능해지고, 프로그램의 유연성과 추상화 수준이 훨씬 높아집니다.



일급 함수 && 고차 함수


객체지향 언어인 C++이나 Java로 함수형 프로그래밍을 구현하는 것은 꽤 까다로운 편입니다. 그 이유 중 하나는 바로 일급 함수(First-Class Function) 개념이 자연스럽게 지원되지 않기 때문입니다.

일급 함수란, 함수를 하나의 값처럼 취급하는 개념입니다. 즉, 함수를 변수에 할당하거나, 함수의 인자로 넘기거나, 함수에서 리턴값으로 반환할 수 있어야 한다는 의미죠. JavaScript는 이 일급 함수 개념을 자연스럽게 지원하기 때문에, 함수형 프로그래밍을 구현하기가 상대적으로 수월합니다.

예를 들어 아래 코드를 보겠습니다:


"use strict";

const functionPow = (f, x, y) => f(x, y) ** 2;

const sum = (x, y) => x + y;
const minus = (x, y) => x - y;

console.log(functionPow(sum, 3, 2));
console.log(functionPow(minus, 3, 2));


이처럼 functionPow함수를 인자로 받는 함수, 즉 고차 함수(Higher-Order Function) 입니다.
고차 함수는 함수를 인자로 받거나, 함수를 반환하는 함수를 말하며, 일급 함수가 가능해야만 이런 구조를 만들 수 있습니다.


고차 함수의 활용 – map, filter, reduce

JavaScript는 고차 함수를 활용할 수 있는 메서드를 기본적으로 제공합니다. 대표적인 예가 map, filter, reduce, flatMap 등입니다. 이들은 배열을 조작할 때 매우 강력하면서도 직관적인 방식으로 데이터를 처리할 수 있게 해줍니다.

아래는 map을 활용한 간단한 예제입니다:


"use strict";

const pow = x => x ** 2;

const arr = [1, 2, 3];

const newArr = arr.map(pow);

console.log(newArr);


이 예제에서 map은 각 배열 요소에 대해 pow 함수를 적용하고, 그 결과로 새로운 배열을 반환합니다. 중요한 점은 기존 배열 arr은 변경되지 않고, 순수하게 새로운 결과만 만들어낸다는 것입니다. 이것이 함수형 프로그래밍이 지향하는 불변성과 순수성의 핵심입니다.


고차 함수를 이용하면 프로그램 로직을 더 가독성 있게, 더 안정적으로 작성할 수 있습니다. 무엇보다 함수 자체를 조합하면서 로직을 구성하기 때문에, 절차 중심의 코드보다 의도를 명확히 표현할 수 있고 디버깅이나 유지보수에서도 이점을 가질 수 있습니다.



커링 &&  합성함수


함수형 프로그래밍에는 다양한 개념이 있지만, 그 중에서도 커링(Currying)합성 함수(Function Composition) 는 함수 조합과 재사용성 측면에서 특히 유용한 기능입니다.


커링이란?

커링은 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수들의 연속적인 형태로 변환하는 기법입니다.
쉽게 말해, 원래는 f(x, y)처럼 한 번에 두 개의 인자를 받던 함수를, f(x)(y) 형태로 분해해서 사용하는 방식입니다.

JavaScript에서는 화살표 함수(arrow function)를 이용해 이 커링을 자연스럽게 구현할 수 있습니다:


"use strict";

const sum = x => y => x + y;

const sum100 = sum(100);

console.log(sum100(10));
console.log(sum100(20));


위 코드에서 sum은 두 인자를 나눠서 처리하는 커링 함수입니다.
sum100x = 100으로 고정된 상태이며, 이후에는 y만 넘기면 됩니다.
이러한 방식은 마치 객체지향의 템플릿처럼, 함수에 일부 값을 미리 설정해두고, 그 후 필요한 값만 받아서 유연하게 활용하는 구조라고 볼 수 있습니다.


합성 함수(Function Composition)


합성 함수는 여러 함수를 조합해 하나의 새로운 함수를 만드는 방식입니다. 예를 들어 f(g(x))처럼 한 함수의 결과를 다른 함수의 입력으로 사용하는 방식이죠.

JavaScript에서는 다음과 같이 간단한 합성 함수를 만들 수 있습니다:


"use strict";

const compose = (f, g) => x => f(g(x));

const double = x => x ** 2;
const square = x => x ** 3;

const six = compose(double, square);

console.log([1, 2, 3].map(x => six(x)));


이런 함수 조합은 작은 함수들을 쌓아 큰 기능을 만드는 함수형 프로그래밍의 핵심 철학과도 맞닿아 있습니다.
각 함수는 명확한 역할을 수행하며, 조합을 통해 더 복잡한 작업도 깔끔하게 표현할 수 있습니다.


정리

  • 커링은 다중 인자 함수를 단일 인자 함수들의 연속으로 바꿔서, 함수를 더 유연하게 사용할 수 있는 구조입니다.

  • 합성 함수는 여러 함수의 동작을 하나의 흐름으로 묶어, 간결하고 직관적인 로직을 구성할 수 있게 합니다.

이 두 개념을 활용하면, 가독성 높고 재사용성 좋은 함수형 코드를 작성할 수 있습니다.
함수형 프로그래밍을 처음 접할 때는 다소 낯설 수 있지만, 직접 구현해보면 생각보다 단순하면서도 강력한 구조라는 걸 체감하게 됩니다.


모나드 (monad)


함수형 프로그래밍을 공부하면서 가장 이해하기 어려웠던 개념 중 하나가 바로 모나드(Monad) 였습니다. 처음에는 단순한 함수 묶음인 줄 알았지만, 차차 Promise, 체이닝, 오류 처리, 그리고 명확한 타입 흐름까지 연결된다는 점에서 꽤 깊이 있는 개념이라는 걸 알게 되었습니다.


모나드는 무엇일까?

간단히 말하면, 모나드는 값을 감싸고(map), 계속해서 함수를 적용하며(flatMap), 흐름을 이어가는 구조입니다.
우리가 함수형 방식으로 여러 연산을 체이닝할 때, 중간 상태를 명확하게 관리하고, 흐름을 깨지 않으면서 로직을 이어나가는 데 큰 도움이 됩니다.

이 개념은 JavaScript의 Promise와 매우 유사한 구조를 가집니다.


"use strict";

class Monad {

  constructor(value) {
    this.value = value;
  }

  static Of(value) {
    return new Monad(value);
  }

  map(fn) {
    return Monad.Of(fn(this.value));
  }

  flatMap(fn) {
    return fn(this.value);
  }

  getValue() {
    return this.value;
  }
}

const x = 10;
const y = 5;
const monad = new Monad(x);
const result = monad
.flatMap(value => {
  return new Monad(value + y);
})
.map(value => {
  return value ** 2;
})
.getValue();
console.log(result);


  • map: 일반적인 값 변환. 내부 값을 꺼내지 않고 연산을 수행한 후 다시 감쌉니다.

  • flatMap: 내부에서 또 다른 Monad를 반환할 때 사용. 중첩 방지를 위해 .flatMap()이 필요합니다.


왜 모나드가 중요한가?

함수형 프로그래밍을 하다 보면 함수 조합, 중간 상태 전달, 조건에 따른 분기, 오류 처리 등의 상황을 자주 마주하게 됩니다.
이럴 때 모나드는 명확한 흐름 제어를 가능하게 해주고, 각 연산의 맥락을 유지하면서 연속적인 처리를 지원합니다.

또한, 다음과 같은 이점이 있습니다:

  • 가독성이 좋고 의도가 명확한 코드를 작성할 수 있음

  • 복잡한 분기와 예외 처리를 감싸서 안정적인 로직 구성 가능

  • Promise, Option, Either 등 다양한 패턴과 쉽게 연결 가능


정리

모나드는 단순한 개념 같지만, 실제로는 함수형 프로그래밍의 핵심 철학을 가장 잘 담고 있는 구조입니다.
값을 감싸고, 체이닝을 통해 흐름을 유지하며, 언제든 필요한 시점에 결과를 꺼내 쓸 수 있는 구조화된 컨테이너라고 할 수 있습니다.

JavaScript에서는 이를 직접 구현해볼 수도 있고, PromiseArray, Maybe, Either처럼 이미 구조화된 객체들을 통해도 체험해볼 수 있습니다.
함수형 프로그래밍을 조금 더 깊이 이해하고 싶다면, 모나드 개념을 꼭 한번 구현해보고 체감해보는 것을 추천드립니다.


마무리


오늘은 함수형 프로그래밍의 기본 개념들을 중심으로 정리해보았습니다.
객체지향 프로그래밍과 달리, 함수형 프로그래밍은 단위 테스트에 용이하고, 변수를 안전하게 관리할 수 있으며, 메모리 관리 측면에서도 안정적인 구조를 갖고 있다는 점에서 매우 매력적인 패러다임입니다.

특히 순수 함수, 불변성, 고차 함수, 커링, 모나드와 같은 개념들은 처음에는 조금 낯설게 느껴질 수 있지만, 실제로 적용해보면 코드의 가독성, 재사용성, 예측 가능성을 크게 향상시켜줍니다.

무엇보다 중요한 것은, 객체지향과 함수형 프로그래밍은 서로 배타적인 것이 아니라 상호 보완적인 개념이라는 점입니다. 상황에 따라 두 가지 패러다임을 적절히 융합해 사용한다면, 더 견고하고 유연한 소프트웨어를 만들 수 있을 거라 생각합니다.

오랜만에 작성한 포스팅이었지만, 끝까지 읽어주셔서 감사합니다.
여러분이 하시는 일에도 좋은 결과가 함께하길 바랍니다. 화이팅!











개발자 김동완

개발, 비전공, 백엔드, 풀스택, 프로그래밍

댓글 쓰기

다음 이전