함수형 프로그래밍 완벽 가이드: 선언형 코드 작성의 실전 적용법

ClydeHan·2024년 9월 5일
1

함수형 프로그래밍 (Functional Programming)

Functional Programming Image

이미지 출처: modernescpp.com

📌 개요

함수형 프로그래밍은 선언형 프로그래밍의 한 종류로, 특정한 원칙과 개념에 중점을 둔 프로그래밍 스타일이다. 함수형 프로그래밍은 순수 함수(Pure Function), 함수의 일급 시민성(First-class Functions), 불변성(Immutability), 고차 함수(Higher-order Functions) 등의 특징을 가지고 있다.

결국, 함수형 프로그래밍은 선언형 프로그래밍의 철학을 따르면서, 상태 변화를 최소화하고, 순수 함수로 문제를 해결하는 방법론이다. 이 방법론은 코드가 더욱 간결하고 예측 가능하며, 유지보수가 용이하게 만들어준다.

즉, 함수형 프로그래밍은 어떻게 프로그래밍할 것인가에 대한 하나의 접근 방식으로, 선언형 프로그래밍의 철학을 구체화한 스타일이라고 볼 수 있다. 다양한 프로그래밍 언어에서 적용될 수 있으며, 함수형 프로그래밍을 사용하면 코드가 더욱 명확하고 안전해진다.


📌 등장 배경

함수형 프로그래밍은 수학적 함수의 개념을 프로그래밍에 적용하기 위해 탄생한 프로그래밍 패러다임이다. 이 개념은 초기 Lisp 언어에서 시작되었으며, 이후 Haskell, ML, Erlang과 같은 언어들이 함수형 프로그래밍의 철학을 발전시켰다. 함수형 프로그래밍은 상태 변화부작용을 최소화하여 코드를 더욱 예측 가능하고 안정적으로 만들려는 목적에서 출발했다.

초기에는 주로 학계에서 연구되었으나, 최근에는 병렬 처리분산 시스템의 중요성이 부각되면서 실무에서도 점점 더 널리 사용되고 있다. 함수형 프로그래밍은 이러한 환경에서 안정적이고 효율적인 코드를 작성하는 데 강력한 도구로 자리잡고 있다.


📌 특징

💡 순수 함수

순수 함수는 동일한 입력에 대해 항상 동일한 출력을 반환하며, 외부 상태에 영향을 미치지 않고, 외부 상태로부터도 영향을 받지 않는 함수를 말한다. 이는 함수형 프로그래밍의 기본 원리로, 부작용(Side Effect)을 없애고 코드를 더욱 예측 가능하게 만든다.

  • 실생활 예시

생각해보자. 커피를 만드는 기계를 가지고 있다고 가정하자. 이 기계는 같은 종류의 원두를 넣을 때마다 항상 같은 맛의 커피를 만들어 낸다. 여기서 중요한 점은, 이 기계가 커피를 만드는 과정에서 외부 환경에 영향을 받지 않고, 결과적으로 만들어진 커피가 외부의 상태에 영향을 미치지 않는다는 것이다.

이 기계는 항상 같은 입력(원두)을 받아 같은 출력을 제공(커피)하며, 외부의 어떤 것도 변경하지 않는다. 이는 순수 함수의 개념과 비슷하다.

  • 코드 예시

이제 코딩에서 순수 함수가 어떻게 작동하는지 보자. 예를 들어, 두 수를 더하는 간단한 순수 함수를 작성해보겠다.

// 순수 함수 예시
function add(x, y) {
  return x + y;
}

console.log(add(2, 3)); // 출력: 5
console.log(add(2, 3)); // 출력: 5 (항상 동일한 결과)

위의 add 함수는 순수 함수이다. 이 함수는 두 입력 xy를 받아 그들의 합을 반환한다. 이 함수의 특징은 동일한 입력값에 대해 항상 동일한 결과를 반환하며, 함수 외부의 상태에 영향을 미치지 않는다는 점이다.

  • 부작용 없는 코드

이제 반대로, 부작용이 있는 코드를 살펴보자. 아래 예시는 외부 상태를 변경하는 함수이다.

let total = 0;

function addToTotal(x) {
  total += x;
}

addToTotal(5);
console.log(total); // 출력: 5

addToTotal(10);
console.log(total); // 출력: 15 (total 값이 변경됨)

addToTotal 함수는 외부 변수 total을 변경한다. 이 함수는 부작용(Side Effect)이 있는 함수로, 외부 상태(total)를 변경하므로 순수 함수가 아니다. 이렇게 외부 상태를 변경하는 함수는 예측하기 어렵고, 코드의 유지보수가 복잡해진다.

  • 참조 투명성

참조 투명성(Referential Transparency)은 순수 함수의 또 다른 중요한 개념이다. 이는 같은 입력이 주어졌을 때 항상 동일한 출력을 제공한다는 의미이다. 즉, 함수 호출을 그 결과값으로 대체해도 프로그램의 동작에 아무런 영향을 미치지 않아야 한다.

console.log(add(2, 3)); // 출력: 5
console.log(5);         // 출력: 5 (위 함수 호출을 5로 대체해도 동일한 결과)

위 코드에서 add(2, 3)5로 대체해도 프로그램의 결과는 변하지 않는다. 이것이 참조 투명성의 예시이다.


💡 함수의 일급 시민성

함수의 일급 시민성(First-class Citizen)은 함수형 프로그래밍의 핵심 개념으로, 함수가 변수에 할당되거나, 인자로 전달되거나, 다른 함수에서 반환될 수 있는 능력을 의미한다. 이를 통해 고차 함수와 같은 강력한 기능을 구현할 수 있으며, 코드의 유연성과 재사용성을 높일 수 있다.

  • 고차 함수(Higher-order Function)

함수형 프로그래밍에서 고차 함수는 다른 함수를 인자로 받거나, 함수를 반환하는 함수를 의미한다. 이는 함수형 프로그래밍의 중요한 특성으로, 함수를 조합하거나 변형하는 데 사용된다.

  • 실생활 예시

실생활에서 어떤 개념이 일급 시민으로 취급된다는 것은 우리가 그것을 자유롭게 다룰 수 있다는 것을 의미한다. 예를 들어, 을 생각해보자. 책은 다음과 같은 다양한 방식으로 다뤄질 수 있다.

  • 책을 선물로 줄 수 있다. (함수를 인자로 전달)
  • 책을 대여할 수 있다. (함수를 변수에 할당)
  • 책을 판매할 수 있다. (함수를 반환)

이와 마찬가지로, 함수가 일급 시민으로 취급된다는 것은 우리가 함수를 다른 데이터 타입처럼 자유롭게 다룰 수 있다는 의미이다.

  • 코드 예시

이제 자바스크립트에서 함수의 일급 시민성을 보여주는 몇 가지 예시를 보겠다.

  • 함수가 변수에 할당될 수 있음
const greet = function(name) {
  return `Hello, ${name}!`;
};

console.log(greet("Alice")); // 출력: Hello, Alice!

여기서 greet라는 변수에 함수를 할당했다. 이 변수는 이제 함수처럼 동작하며, 나중에 호출할 수 있다.

  • 함수가 다른 함수의 인자로 전달될 수 있음
function logResult(fn, value) {
  console.log(fn(value));
}

logResult(greet, "Bob"); // 출력: Hello, Bob!

logResult 함수는 다른 함수(fn)를 인자로 받아, 그 함수를 실행하고 결과를 출력한다. 이 예시에서 greet 함수를 logResult의 인자로 전달하여, greet 함수가 실행되도록 했다.

  • 함수가 다른 함수에서 반환될 수 있음
function createMultiplier(multiplier) {
  return function(value) {
    return value * multiplier;
  };
}

const double = createMultiplier(2);
console.log(double(5)); // 출력: 10

이 예시에서 createMultiplier 함수는 함수를 반환한다. 이 반환된 함수는 특정 값을 인자로 받아 그 값을 multiplier로 곱한 결과를 반환한다. 여기서 doublecreateMultiplier(2) 호출의 결과로 생성된 함수이며, double(5)는 5에 2를 곱한 10을 반환한다.

  • 고차 함수의 예시

고차 함수는 함수형 프로그래밍에서 중요한 개념으로, 함수를 인자로 받거나 함수를 반환하는 함수를 의미한다.

function applyOperation(operation, x, y) {
  return operation(x, y);
}

function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

console.log(applyOperation(add, 3, 4)); // 출력: 7
console.log(applyOperation(multiply, 3, 4)); // 출력: 12

이 예시에서 applyOperation은 고차 함수이다. 이 함수는 다른 함수(operation)를 인자로 받아 그 함수를 실행한다. applyOperation(add, 3, 4)add(3, 4)의 결과를 반환하고, applyOperation(multiply, 3, 4)multiply(3, 4)의 결과를 반환한다.


💡 불변성(Immutability)

불변성은 함수형 프로그래밍의 핵심 원리 중 하나로, 데이터가 한 번 생성되면 변경되지 않는 성질을 말한다. 데이터의 불변성을 유지하면 상태 변화로 인한 버그를 방지하고, 프로그램의 예측 가능성을 높일 수 있다.

  • 불변 데이터 구조: 함수형 프로그래밍에서는 데이터 구조가 불변성을 가지며, 이는 데이터의 복사와 수정이 더 안전하고 예측 가능하게 이루어질 수 있음을 의미한다.

  • 실생활 예시

계약서를 작성했다고 가정하자. 이 계약서의 내용을 수정해야 할 경우, 원본 계약서를 직접 수정하는 대신, 원본을 그대로 보관하고 복사본을 만들어 그 복사본에서 수정 작업을 한다고 생각해보자. 이렇게 하면 원본 계약서는 언제나 그대로 유지되며, 원본이 손상되거나 의도치 않게 변경될 위험이 없다. 이것이 바로 불변성의 개념이다.

  • 불변성을 지키지 않는 경우의 코드 예시
let person = { name: "Alice", age: 25 };

person.age = 26;  // 원본 객체가 직접 변경됨

console.log(person);  // 출력: { name: "Alice", age: 26 }

이 코드에서는 person 객체의 age 속성을 직접 변경했다. 이로 인해 원본 객체가 변경되며, 다른 곳에서 이 객체를 참조할 때 예측하지 못한 문제가 발생할 수 있다.

  • 불변성을 지키는 경우의 코드 예시
let person = { name: "Alice", age: 25 };

// 새로운 객체를 생성하여 변경 사항 반영
let updatedPerson = { ...person, age: 26 };

console.log(person);        // 출력: { name: "Alice", age: 25 }
console.log(updatedPerson); // 출력: { name: "Alice", age: 26 }

이 코드에서는 person 객체를 변경하지 않고, 대신 복사본을 만들어 age 속성을 수정한 후, updatedPerson이라는 새로운 객체를 생성했다. 이 방법을 통해 원본 객체의 불변성을 유지하면서 필요한 변경 사항을 적용할 수 있다.

  • 불변 데이터 구조의 예시

함수형 프로그래밍에서는 배열이나 객체 같은 데이터 구조가 불변성을 가지도록 관리된다.

const numbers = [1, 2, 3];

// 기존 배열을 변경하지 않고, 새로운 배열을 생성
const newNumbers = [...numbers, 4];

console.log(numbers);    // 출력: [1, 2, 3]
console.log(newNumbers); // 출력: [1, 2, 3, 4]

이 예시에서 numbers 배열은 불변성을 유지하며, 새로운 newNumbers 배열이 생성된다. 이로 인해 원본 배열을 변경하지 않고도 데이터를 추가할 수 있다.


💡 지연 평가(Lazy Evaluation)

지연 평가는 필요할 때까지 계산을 미루는 기법으로, 불필요한 계산을 피하고 성능을 최적화하는 데 사용된다. 이는 특히 대용량 데이터 처리에서 유리하다.

  • 지연 리스트(Lazy Lists): Haskell과 같은 함수형 언어는 지연 평가를 통해 무한 리스트와 같은 구조를 다룰 수 있으며, 필요한 만큼만 계산을 수행한다.

💡 함수 조합성(Composability)

함수형 프로그래밍에서는 작은 함수들을 조합하여 복잡한 동작을 구현할 수 있다. 이는 코드의 재사용성을 높이고, 모듈화된 코드를 작성하는 데 유용하다.

  • 함수 합성(Composition): 함수 합성은 두 개 이상의 함수를 결합하여 새로운 함수를 만드는 과정으로, 코드의 명확성과 간결성을 높인다.

📌 대표적인 함수형 프로그래밍 언어 및 프레임워크

💡 Haskell

순수 함수형 프로그래밍 언어로, 순수 함수와 불변성의 개념을 철저히 따른다. Haskell은 함수형 프로그래밍의 모든 핵심 개념을 깊이 있게 탐구할 수 있는 언어이다.

💡 Lisp

함수형 프로그래밍의 철학을 따르는 대표적인 언어 중 하나로, 함수의 일급 시민성을 강하게 지원한다. Lisp은 또한 매크로 시스템을 통해 고도의 확장성을 제공한다.

💡 Scala

함수형과 객체 지향 프로그래밍을 모두 지원하는 하이브리드 언어로, 함수형 프로그래밍의 개념을 적극적으로 채택했다. Scala는 JVM(Java Virtual Machine)에서 실행되며, Java와의 호환성을 제공한다.

💡 Erlang

동시성 처리를 위해 설계된 함수형 프로그래밍 언어로, Telecom 시스템에서 널리 사용된다. Erlang은 메시지 기반의 동시성 모델을 제공하며, 고가용성 시스템에 적합하다.

💡 F#

.NET 플랫폼을 위한 함수형 프로그래밍 언어로, F#은 함수형 프로그래밍의 강력한 특성과 객체 지향 프로그래밍의 유연성을 결합했다.


📌 장단점

💡 장점

  • 예측 가능성: 순수 함수와 불변성 덕분에 함수형 프로그래밍에서 작성된 코드는 매우 예측 가능하다. 이는 디버깅과 테스트를 쉽게 만들어 주며, 코드의 신뢰성을 높인다.
  • 병렬 처리에 강함: 함수형 프로그래밍은 상태 변화가 없기 때문에 병렬 처리에 매우 적합하다. 이는 동시성 문제를 최소화하고, 보다 안전하게 병렬 프로그램을 작성할 수 있게 한다.
  • 코드 재사용성: 함수 조합성 덕분에 코드의 재사용성이 높다. 작은 함수들을 결합하여 새로운 기능을 구현할 수 있으며, 코드의 모듈화와 유지보수에 유리하다.
  • 디버깅과 테스트가 용이함: 함수형 프로그래밍은 함수 단위로 코드를 분리하고, 순수 함수의 특성 덕분에 디버깅과 테스트가 상대적으로 쉽다. 이는 복잡한 애플리케이션에서도 각 함수의 동작을 독립적으로 검증할 수 있게 해준다.

💡 단점

  • 학습 곡선: 함수형 프로그래밍의 개념과 철학은 명령형 프로그래밍에 익숙한 개발자에게 생소할 수 있으며, 이로 인해 학습 곡선이 가파를 수 있다.
  • 성능 문제: 일부 경우에는 함수형 프로그래밍이 명령형 프로그래밍보다 성능이 낮을 수 있다. 예를 들어, 불변성으로 인해 데이터 구조를 복사하는 데 추가적인 메모리와 시간이 필요할 수 있다.
  • 디버깅 어려움: 지연 평가와 같은 개념은 디버깅을 어렵게 만들 수 있다. 계산이 지연됨에 따라 프로그램의 실행 순서를 추적하기 어려울 수 있다.
  • 라이브러리와 툴링 부족: 함수형 프로그래밍 언어는 명령형 프로그래밍 언어에 비해 상대적으로 생태계가 작을 수 있다. 이는 특정 문제를 해결하기 위한 라이브러리나 툴이 부족할 수 있음을 의미한다.

📌 함수형 프로그래밍의 철학과 원리

함수형 프로그래밍의 철학은 수학적 함수를 기반으로 한다. 이는 프로그램을 상태 변화와 부작용 없이 구성할 수 있게 하며, 이를 통해 코드의 신뢰성과 안정성을 높인다. 함수형 프로그래밍에서는 프로그램이 상태 변화 없이 순수 함수들로 구성되며, 이로 인해 프로그램의 동작을 예측하고 이해하기 쉽다. 이러한 철학은 프로그램의 복잡성을 줄이고, 보다 안정적이고 유지보수가 쉬운 소프트웨어를 개발하는 데 기여한다.


선언형 프로그래밍과는 구체적으로 어떻게 다른 것인가?

📌 차이점 정리

  1. 선언형 프로그래밍무엇을 할 것인지에 초점을 맞춘다. 목표를 명시하지만, 그 목표를 어떻게 달성할지는 언어나 시스템이 알아서 처리한다.
  2. 함수형 프로그래밍은 선언형 프로그래밍의 한 종류다. 순수 함수와 함수의 조합을 사용하여 상태 변화 없이 프로그램을 구성하는 방식이다.

📌 쉽게 설명하자면

  • 선언형 프로그래밍은 "결과를 설명하는 방식"이다. 예를 들어, 피자를 주문할 때 "피자 하나 주세요"라고 말하는 것이 선언형 프로그래밍이다. 피자를 어떻게 만드는지는 관심 없다. 결과만 중요하다.
  • 함수형 프로그래밍은 이 선언형 프로그래밍의 한 방법으로, "레고 블록" 같은 함수를 사용해 프로그램을 만든다. 각 레고 블록(함수)은 독립적이고, 다른 블록(함수)과 결합하여 더 큰 구조를 만든다. 중요한 것은, 이 블록들이 서로 간섭하지 않고 독립적으로 작동한다는 점이다.

📌 결론

그래서 함수형 프로그래밍은 선언형 프로그래밍의 한 종류지만, 그 자체로는 더 구체적이고 특정한 방법을 의미한다. "무엇을" 할지 설명하는 것(선언형 프로그래밍)과, 그 설명을 함수로 풀어내는 것(함수형 프로그래밍)이 핵심이다.


참고문헌

0개의 댓글