시프티 팀

Engineering: Functional Programming

2021-12-27

Author | 구장회

Front-End Engineer


Introduction

함수형 프로그래밍 패러다임은 명령형 프로그래밍에 비해서 소프트웨어의 크기가 커짐에 따라 필연적으로 증가하는 코드의 복잡성을 줄이고, 유지보수가 비교적 용이하여 주목받게 되었고, 함수형 언어 중에 대중적으로 잘 알려진 언어인 ML, Clojure, Scala, Haskell 이외에도 C++, C#, Kotlin Python, Go, Java 언어 등에서도 함수형 프로그래밍을 지원하거나 관련 feature 들이 구현되어 있습니다.

시프티 개발팀은 함수형 프로그래밍을 통해 제품을 개발하고 있으며, 15만+ 기업의 니즈에 맞는 솔루션을 빠르고 안정적으로 개발하고 있습니다.

이 글에서는 함수형 프로그래밍이 다른 패러다임에 비해 어떤 이점이 있는지 key feature 들과 함께 설명하겠습니다.


Programming Paradigm

// https://en.wikipedia.org/wiki/Programming_paradigm
Programming paradigms are a way to classify programming languages based on their features. Languages can be classified into multiple paradigms.


위 정의처럼 프로그래밍 패러다임이란 소프트웨어 구조에 대해 몇 가지 원칙을 기반으로 하는 사고방식/패턴으로, 프로그래밍 언어에 따라 특정 패러다임에 밀접한 관련이 있기도 하고, 여러 패러다임의 코드를 작성할 수도 있습니다.

보편적인 패러다임의 분류를 보겠습니다.

  • 명령형(imperative) 프로그래밍 : 무엇(What)을 할 것인지 나타내기보다 어떻게 할 건지(How)를 설명하는 방식
    • 절차지향(procedural) 프로그래밍 : 수행되어야 할 순차적인 처리 과정을 포함하는 방식 (C, C++)
    • 객체지향(object-oriented) 프로그래밍 : 객체들의 집합으로 프로그램의 상호작용을 표현 (C++, Java, C#)
  • 선언형(declarative) 프로그래밍 : 어떻게 할건지(How)를 나타내기보다 무엇(What)을 할 건지를 설명하는 방식
    • 함수형(funtional) 프로그래밍 : 순수 함수를 조합하고 소프트웨어를 만드는 방식 (Clojure, Lisp, Haskell)
    • 반응형(reactive) 프로그래밍 : 비동기 데이터 스트림을 다루는 기법



Functional Programming Paradigm

// 정의 : https://en.wikipedia.org/wiki/Functional_programming
In computer science, functional programming is a programming paradigm where programs are constructed by applying and composing functions.

It is a declarative programming paradigm in which function definitions are trees of expressions that map values to other values, rather than a sequence of imperative statements which update the running state of the program.


함수형 프로그래밍 패러다임은 선언형 프로그래밍 패러다임의 하나로서, 순수함수(pure function)와 불변값(immutable value)을 통해 소프트웨어를 작성하는 방식입니다.

아래 함수형 프로그래밍의 key concepts 을 통해 더 자세히 설명하겠습니다.

  • First-class and higher-order functions
  • Pure functions
  • Recursion
  • Immutability



First-class and Higher-order Functions

함수형 프로그래밍에서 1급 함수는 1급 시민(first class citizen)의 개념을 충족하는 함수를 의미합니다. 개발자분들에게는 익숙한 개념인 1급 시민이란 아래 조건을 충족하는 개념으로 간단한 예시는 다음과 같습니다.

  • 변수나 데이터 구조 안에 담을 수 있다.
  • 파라미터로 전달 할 수 있다.
  • 반환 값으로 사용할 수 있다.

// 변수에 assign
var add = function(a, b) {
 return a + b;
}

// parameter로 전달
var add2 = function(func) {
 return func();
}

// return 값으로 사용
function hello() {
 return function() {
  console.log(“Hello!”);
 }
}


위와 같이 함수를 자료형처럼 사용할 수 있다면, 어떤 장점이 있을까요?

눈치를 채셨겠지만 고차함수(High-order function)로써 함수를 인자로 받거나, 결과로 반환하여 중첩된(composable) 형태로 함수를 사용할 수 있습니다.
고차함수의 개념은 함수형 프로그래밍을 지원하는 언어에서 기본적인 특징으로 자바스크립트의 callback을 예로 들면, 함수를 인자로 전달하여 나머지 연산이 완료된 후 실행될 수 있도록 하여 비동기적인 흐름을 가능하게 합니다.

Node.js 애플리케이션에서 Ajax를 이용하여 서버로부터 데이터의 수신을 기다리지 않고 바로 다른 작업을 실행함에 따라 웹의 속도가 빠르게 반응할 수 있는것도 고차함수의 개념이 사용되었기 때문입니다.

(참고)
함수를 1급 시민의 개념으로 사용할 수 없는 프로그래밍 언어에서도 고차함수를 사용할 수는 있습니다.
(C 언어의 경우 Function Pointers)




Pure Functions

함수형 프로그래밍은 정의에 따라, 순수함수를 작성하여 프로그램을 작성하도록 합니다.
함수가 pure 하다는 것은 어떤 의미일까요?

// pure의 사전적 정의
not mixed or adulterated with any other substance or material.


"pure"의 사전적 정의처럼 pure function이란 아래 조건을 만족하는 함수를 의미합니다.

  • 참조투명성(Referential transparency) : 함수가 외부의 어떤 mutable state에도 의존적이지 않고 함수의 출력이 오로지 입력으로 결정됩니다. 이는 같은 input에 대해서 같은 output이 항상 보장됨을 의미합니다.
  • 부수 효과 없음(Side-effect free) : 아래의 동작들이 발생하지 않아 예측 가능함을 의미합니다.
    • 변수의 re-assign
    • 자료 구조를 제자리에서 수정함
    • 객체의 필드 값을 설정함
    • 예외나 오류가 발생하며 실행이 중단됨
    • 콘솔 또는 파일 I/O가 발생함


이해를 돕기위해 예시를 보시겠습니다.

let ageRequirement = 20;
// Impure function: 함수 외부의 mutable한 ageRequirement값에 따라 output이 달라질 수 있음
function canVote(age) {
 return age >= heightRequirement;
}

// Impure function: output을 예측할 수 없음
function getRandom() {
 return Math.ramdom();
}

// Pure function: 외부에 독립적으로, input에 의해서만 output이 결정
function multiply(a, b) {
 return a * b;
}


위와 같이 순수함수를 사용하면 디버깅, 유지보수, 재사용이 쉬우며, thread-safe한 코드를 작성할 수 있습니다. 하지만 순수함수만을 사용하여 프로그램을 작성할 수 있을까요?

간단한 프로그램은 가능할 수 있으나, 실제 application에서는 memory I/O, Ajax request, void function 등 본질적으로 impure 한 함수를 사용할 수밖에 없으니 함수의 80% 정도는 순수함수로 사용하는 방법이 권장된다고 합니다. (80/20 rule)


Recursion

// 정의: https://en.wikipedia.org/wiki/Recursion_(computer_science)
recursion is a method of solving a problem where the solution depends on solutions to smaller instances of the same problem.


함수형 프로그래밍에서는 iteration을 주로 재귀함수(recursion)를 통해 구현합니다.

피보나치수열을 예시로 보시겠습니다.

// for-loop로 구현된 피보나치 수
function fibonacci(n){
 let arr = [0, 1];
 for (let i = 2; i < n + 1; i++){
  arr.push(arr[i - 2] + arr[i -1])
 }
return arr[n]
}

// recursion으로 구현된 피보나치 수
function fibonacci(n) {
 if(n < 2)
  return n;
 return fibonacci(num-1) + fibonacci(num - 2);
}


예시에서 보시다시피 for-loop 구현에서 변수 i 가 iteration마다 re-assign 되고 있습니다.
(이는 pure function이 아니죠!) 재귀함수 구현에서는 이 부분을 제거해서 pure function으로서 가독성이 나아진 것을 확인하실 수 있습니다.

이처럼 재귀함수는 코드를 간결하고 명확하게 서술할 수 있게 해 주지만, 반복되는 함수호출에서 스택을 많이 잡아먹을 수 있어 imperative loop에 비해서 expensive 할 수 있기때문에 주의해서 사용해야 합니다.

(참고)
브라우저에서는 tail-call이라는 stack optimization을 제공하고, 보편적으로는 이미 계산된 intermediate result를 재사용하는 메모이제이션 기법이 많이 쓰입니다.




Immutablility

scala idiom:
Make your variables immutable, unless there’s a good reason not to.


Immutability (불변성)이란 함수형 프로그래밍에서 예측할 수 있는 코드를 작성하는 핵심 개념으로 변수의 경우 값, 객체의 경우 레퍼런스가 생성 이후 변경(mutate)될 수 없는 특성입니다.
(자바에서의 final, 스칼라에서 val 키워드를 예로 들 수 있습니다.)

이 개념을 자료구조에 적용한 것이 불변성의 자료구조 (immutable or persistent data structure)입니다. 즉, 변수/객체를 변경하는 대신 새로운 인스턴스를 생성함으로써 의도하지 않은 변경을 방지할 수 있어 error prone 한 코드를 작성할 수 있습니다.

기존 인스턴스를 변경하지 않고 새로운 인스턴스를 생성하기 때문에 비효율적일 수 있으나, 반대로 불변성으로 인해 기존의 인스턴스를 재사용 가능하다는 장점도 존재합니다.

자바스크립트의 경우에 const 키워드로 생성된 변수는 값의 재할당시 런타임 에러가 발생하기 때문에 불변성이 보장되고, object는 mutable 하므로 Object.assign, Object.freeze를 사용하여 불변의 객체를 만들 수는 있으나 큰 object의 경우는 성능상 이슈가 있어서 일반적으로는 immutable한 자료구조를 사용하는 것이 권장되는 방법입니다.

아래는 Facebook에서 제공하는 Immutable Collections 라이브러리인 Immutable.js의 예시입니다.

const { Map } = require(‘immutable’);

const originalMap = Map({ a: 1, b: 2, c: 3 });
const updatedMap = originalMap.set(‘b’, 1000);

console.log(originalMap.get(‘b’)); // 2
console.log(updatedMap.get(‘b’)); // 1000

set 함수는 새로운 Map instance를 updatedMap에 할당하기 때문에 기존 originalMap은 mutate 되지 않습니다.


Functional VS Imperative programming paradigm


이제 함수형 프로그래밍 패러다임과 명령형 프로그래밍의 차이점을 성능과 유지보수 측면에서 짚어보겠습니다.

• 성능 측면

함수형 프로그래밍에서는 불변(immutable) data structure를 사용함으로써 CPU와 메모리 사용에 있어 명령형 프로그래밍보다 덜 효과적인 것으로 알려져 있습니다.

그러나 CPU와 메모리의 극적인 발전, 함수형 프로그래밍 언어 컴파일러들의 놀라운 발전 덕분에 함수형 프로그래밍이 필연적으로 가지는 오버헤드가 줄어들었고, Lazy evaluation을 통해 추가적인 성능 개선의 이점도 존재합니다.

• 유지보수 측면

프로그램의 유지보수 측면에서 두 패러다임의 가장 큰 차이점은, 함수형 프로그래밍은 순수함수를 사용해서 side effect를 회피한다는 점입니다.

side effect란 호출된 함수가 전역변수의 값을 변경하는 등 밖의 scope에서 관찰할 수 있는 프로그램/코드의 상태 변경을 의미하는데요. 코드를 작성하는데 side effect가 자주 발생한다면, 작성하는 코드 이외의 scope를 신경써야 하므로 유지보수, 테스트와 디버깅이 쉬운 코드를 작성하기 어렵게 됩니다.

이를 위해서 함수형 프로그래밍을 지원하는 언어들은 변수의 값의 처음 할당된 후에 변경이 되지 않도록 하여 어떤 순간에 변수를 참조하더라도 항상 같은 값을 참조하는 참조투명성(referential transparency)을 제공합니다.



Conclusion


프로그래밍 패러다임들이 모두 mutually exclusive 하지 않기 때문에 실제 응용 프로그램 개발에서는 '은총알은 없다'는 프레디 브룩스의 말처럼 언제라도 다른 패러다임을 적용할 수 있는 시야를 기르고 지식을 갈고 닦아야 할 것 같습니다.


추천 블로그 글