2021.04.07

'깔끔한 코드 체계의 기초' 함수형 프로그래밍의 이해

Matthew Tyson | InfoWorld
함수형 프로그래밍은 초창기부터 존재한 소프트웨어 개발의 한 방식인데 최근 들어 새롭게 그 중요성이 부각되고 있다. 여기서는 함수형 프로그래밍의 기본 개념에 대해 알아보고 자바스크립트와 자바 예제를 살펴본다.
 
ⓒ Getty Images Bank
 

함수형 프로그래밍의 정의

함수는 코드 체계의 기초다. 함수는 모든 고차 프로그래밍 언어에 존재한다. 일반적으로 함수형 프로그래밍은 깔끔하고 유지보수가 용이한 소프트웨어를 만들기 위해 최대한 효과적으로 함수를 사용하는 것을 의미한다. 더 구체적인 의미로는 하나의 프로그래밍 패러다임으로 정의되는 일련의 코딩 접근 방식이다.

함수형 프로그래밍을 객체 지향 프로그래밍(OOP)과 절차적 프로그래밍의 반대 개념으로 정의하는 때도 있다. 그러나 각 접근 방법은 상호 배타적이지 않으며 대부분 시스템은 3가지 모두를 사용하므로 잘못된 생각이다.

함수형 프로그래밍은 특정 사례에서 명확한 이점을 제공하며 많은 언어와 프레임워크에서 폭넓게 사용되고 현재 소프트웨어 추세에서 두드러진다. 함수형 프로그래밍은 모든 개발자의 개념 및 구문 툴킷에 포함돼야 하는 유용하고 강력한 툴이다.
 

순수 함수

함수형 프로그래밍의 이상은 이른바 순수 함수(pure function)다. 순수 함수는 결과가 오로지 입력 매개변수에 의해서만 좌우되며 연산이 아무런 부작용을 일으키지 않는, 즉 반환 값 이외의 외부 영향이 없는 함수다.

순수 함수의 미는 구조적 단순함에 있다. 순수 함수는 인수와 반환 값(즉, API)으로 축약되므로 복잡성의 막다른 길이라고 할 수 있다. 순수 함수와 외부 시스템과의 유일한 상호작용은 정의된 API를 통해 이뤄진다.

이는 객체 메소드가 객체의 상태(객체 멤버)와 상호작용하도록 설계되는 OOP와 대비되며, 외부 상태가 함수 내에서 조작되는 경우가 많은 절차적 스타일 코드와도 대비된다. 그러나 리액트(React)의 useEffect 후크에서 볼 수 있듯이 실제 환경에서 함수는 더 넓은 컨텍스트와 상호작용해야 하는 경우가 많다.
 

불변성

함수형 프로그래밍 철학의 또 다른 특징은 함수 외부에서 데이터를 수정하지 않는다는 것이다. 실무에서 이는 함수에 대한 입력 인수를 수정하지 않는 것을 의미한다. 대신 함수의 반환 값은 수행된 작업을 반영해야 부작용을 피할 수 있다. 이를 통해 함수가 더 큰 시스템 내에서 동작할 때 함수의 영향을 추론하기가 더 쉬워진다.
 

일급 함수

순수 함수 이상 외에, 실제 함수형 프로그래밍 코딩을 좌우하는 요소는 일급 함수다. 일급 함수는 독립적으로 취급이 가능한 '물자체(thing in itself)' 함수다. 함수형 프로그래밍은 함수를 변수, 인수 및 반환 값으로 사용할 수 있는 언어적 지원을 적극적으로 활용해 매끄러운 코드를 생성한다.

일급 함수는 매우 유연하고 유용하므로 자바 및 C#과 같은 강력한 OOP 언어도 일급 함수를 지원하고 이를 계기로 자바 8에서 람다 식 지원이 추가됐다.

일급 함수를 기술하는 또 다른 방법은 데이터형 함수(function as data)다. 일급 함수는 다른 데이터처럼 변수에 할당할 수 있다는 의미다. let myFunc = function(){}라는 코드는 함수를 데이터로 사용한다.
 

고차 함수

함수를 인수로 받거나 함수를 반환하는 함수를 고차 함수라고 한다. 함수를 다루는 함수라고도 볼 수 있다.

자바스크립트와 자바에는 최근 몇 년 사이 개선된 함수 구문이 추가됐다. 자바는 화살표 연산자와 더블콜론 연산자를 추가했으며 자바스크립트는 화살표 연산자를 추가했다. 이러한 연산자는 함수를 더 쉽게 정의하고 사용할 수 있게 해준다(특히 인라인을 익명 함수로). 익명 함수는 참조 변수 없이 정의 및 사용되는 함수다.
 

함수형 프로그래밍 예: 컬렉션

함수형 프로그래밍이 가장 빛을 발하는 예는 컬렉션 작업이다. 컬렉션의 여러 항목에 걸쳐 대량의 기능을 적용할 수 있다는 것이 순수 함수의 개념과 잘 맞아떨어지기 때문이다. 예제 1을 보면 자바스크립트 map() 함수를 활용해서 배열의 문자를 대문자로 바꾼다.
 
예제 1. 자바스크립트에서 map()과 익명 함수 사용하기
let letters = ["a", "b", "c"];
console.info( letters.map((x) => x.toUpperCase()) ); // outputs ["A", "B", "C"]

이 구문의 미는 코드가 상당히 응축되어 있다는 것이다. 루프 및 배열 조작과 같은 부수적 작업이 필요 없다. 수행되는 작업에 관한 사고의 과정이 명확하게 표현돼 있다. 예제 2의 자바 화살표 연산자로도 동일한 결과를 달성할 수 있다.
 
예제 2. 자바의 map()과 익명 함수 사용하기
import java.util.*;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList;
//...
List lower = Arrays.asList("a","b","c");
System.out.println(lower.stream().map(s -> s.toUpperCase()).collect(toList())); // outputs ["A", "B", "C"]

예제 2는 자바 8의 스트림 라이브러리를 활용해 문자 목록을 대문자화하는 같은 작업을 수행한다. 핵심 화살표 연산자 구문이 자바스크립트와 거의 같고, 하는 일도 똑같이 인수를 받아 로직을 수행하고 값을 반환하는 함수를 만든다. 여기서 중요한 점은 정의된 함수 본문에 중괄호가 없는 경우 반환 값이 자동으로 제공된다는 것이다.

계속해서 예제 3에서 자바의 더블콜론 연산자를 보자. 이 연산자는 클래스의 메소드, 이 경우 String 클래스의 toUpperCase 메소드를 참조할 수 있게 해준다. 예제 3은 예제 2와 같은 일을 한다. 다양한 시나리오에서 다양한 구문을 활용할 수 있다.
 
예제 3. 자바 더블콜론 연산자
// ...
List upper = lower.stream().map(String::toUpperCase).collect(toList());

이들 3개 예제에서 모두 고차 함수가 사용되는 것을 볼 수 있다. 두 언어에서 모두 map() 함수는 함수를 인수로 받는다. 달리 말하면, 함수를 다른 함수로 전달하는 것을 함수형 인터페이스로 볼 수 있다(Array API 또는 다른 방법으로). 공급자 함수(매개변수 함수를 소비하는 함수)는 일반화된 로직에 대한 플러그인이다.

이는 OOP의 전략 패턴과 매우 비슷하지만(실제로 자바에서는 단일 메소드와의 인터페이스가 내부적으로 생성됨), 함수의 간결함이 응축된 컴포넌트 프로토콜로 이어진다. 다른 예로 Node.js를 위한 익스프레스(Express) 프레임워크에서 경로 핸들러를 정의하는 예제 4를 보자.
 
예제 4. 익스프레스의 함수형 경로 핸들러
var express = require('express');
var app = express();
app.get('/', function (req, res) {
 res.send('One Love!');
});

예제 4는 경로 매핑과 요청 및 응답 처리를 위해 필요한 것을 명확하게 정의할 수 있게 해준다는 면에서 함수형 프로그래밍의 좋은 예다. 다만 여기서는 함수 본문 내에서 응답 객체를 조작하는 것이 부작용에 해당한다고 볼 수도 있다.
 

커리형 함수

이제 함수를 반환하는 함수라는 함수형 프로그래밍 개념에 대해 생각해 보자. 함수를 반환하는 함수는 인수로서의 함수보다는 덜 사용된다. 예제 5에서 굵은 화살표(=>) 구문이 체인으로 연결된 일반적인 리액트 패턴의 예를 볼 수 있다.
 
예제 5. 리액트의 커리드 함수
handleChange = field => e => {
e.preventDefault();
// Handle event
}

이 코드의 목적은 field, 그다음 event를 받는 이벤트 핸들러를 만드는 것이다. 같은 handleChange를 적용해 필드를 조작할 수 있으므로 유용하다. 즉, 같은 핸들러를 여러 필드에 사용할 수 있다.

예제 5는 커리드 함수(curried function)의 예다. '커리드 함수'라는 이름은 의도는 좋지만 함수의 개념을 나타내지는 않으므로 혼란스럽다. 어떤 경우든 기본 개념은 함수를 반환하는 함수에서 이 함수에 대한 호출을 체인으로 연결할 수 있다는 것이며, 이 방법이 여러 인수가 있는 하나의 함수를 만드는 방법보다 더 유연하다는 것이다.

이러한 종류의 함수를 호출할 때 눈에 띄는 특징은 handleChange(field)(event)와 같이 “체인으로 연결된 괄호” 구문이다.
 

대규모 프로그래밍

앞서 살펴본 예제는 압축된 컨텍스트에서 함수형 프로그래밍의 실무적 이해를 도모하지만 함수형 프로그래밍은 전체적인 프로그래밍 관점에서 더 큰 이익이 되도록 고안됐다. 달리 말하면 함수형 프로그래밍은 더 깔끔하고 탄력적인 대규모 시스템을 대상으로 만들어졌다.

이 개념에 대한 예제를 제공하기는 어렵지만 한 가지 사례는 리액트의 함수형 컴포넌트 지지다. 리액트 팀은 컴포넌트의 더 간결한 함수형 스타일이 인터페이스 아키텍처의 규모가 커질수록 더 복합적인 이점을 제공한다는 점에 주목했다.

함수형 프로그래밍을 폭넓게 사용하는 또 다른 시스템은 리액티브엑스(ReactiveX)다. 리액티브엑스가 사용하는 종류의 이벤트 스트림을 기반으로 하는 대규모 시스템은 분리된 소프트웨어 컴포넌트 상호작용에 따른 혜택을 얻을 수 있다. 앵귤러(Angular)는 이 강력함을 인지하고 리액티브엑스(RxJS)를 전면적으로 도입했다.
 

변수 범위와 컨텍스트

마지막으로, 패러다임으로서 함수형 프로그래밍이 가진 문제라고 단정 지을 수는 없지만 함수형 프로그래밍을 할 때 주의를 기울여야 할 매우 중요한 한 가지는 변수 범위와 컨텍스트다.

자바스크립트에서 컨텍스트의 구체적인 의미는 ‘this 키워드가 무엇이 되느냐’이다. 자바스크립트 화살표 연산자의 경우 this는 둘러싼 컨텍스트를 나타낸다. 전통적인 구문으로 정의된 함수는 자체 컨텍스트를 받는다. DOM 객체의 이벤트 핸들러는 이 사실을 이용해서 this 키워드가 처리되는 대상 요소를 나타내도록 보장할 수 있다.

범위는 변수의 지평선, 즉 보이는 변수가 무엇인지를 나타낸다. 모든 자바스크립트 함수(두꺼운 화살표와 일반 모두), 그리고 자바의 화살표로 정의되는 익명 함수에서 범위는 둘러싼 함수 본문의 범위다. 이러한 함수를 클로저(closure)라고 지칭하는 이유가 여기에 있다. 이 용어는 함수가 함수를 포함하는 범위 내에 둘러싸여 있음을 의미한다.

이것은 기억해야 할 중요한 점이다. 이러한 익명 함수는 범위 내의 함수에 대한 완전한 액세스 권한을 갖는다. 안쪽 함수가 바깥쪽 함수의 변수에 반해서 작동할 수 있으며 이 경우 순수하지 않은 함수 부작용으로 볼 수도 있다. editor@itworld.co.kr



2021.04.07

'깔끔한 코드 체계의 기초' 함수형 프로그래밍의 이해

Matthew Tyson | InfoWorld
함수형 프로그래밍은 초창기부터 존재한 소프트웨어 개발의 한 방식인데 최근 들어 새롭게 그 중요성이 부각되고 있다. 여기서는 함수형 프로그래밍의 기본 개념에 대해 알아보고 자바스크립트와 자바 예제를 살펴본다.
 
ⓒ Getty Images Bank
 

함수형 프로그래밍의 정의

함수는 코드 체계의 기초다. 함수는 모든 고차 프로그래밍 언어에 존재한다. 일반적으로 함수형 프로그래밍은 깔끔하고 유지보수가 용이한 소프트웨어를 만들기 위해 최대한 효과적으로 함수를 사용하는 것을 의미한다. 더 구체적인 의미로는 하나의 프로그래밍 패러다임으로 정의되는 일련의 코딩 접근 방식이다.

함수형 프로그래밍을 객체 지향 프로그래밍(OOP)과 절차적 프로그래밍의 반대 개념으로 정의하는 때도 있다. 그러나 각 접근 방법은 상호 배타적이지 않으며 대부분 시스템은 3가지 모두를 사용하므로 잘못된 생각이다.

함수형 프로그래밍은 특정 사례에서 명확한 이점을 제공하며 많은 언어와 프레임워크에서 폭넓게 사용되고 현재 소프트웨어 추세에서 두드러진다. 함수형 프로그래밍은 모든 개발자의 개념 및 구문 툴킷에 포함돼야 하는 유용하고 강력한 툴이다.
 

순수 함수

함수형 프로그래밍의 이상은 이른바 순수 함수(pure function)다. 순수 함수는 결과가 오로지 입력 매개변수에 의해서만 좌우되며 연산이 아무런 부작용을 일으키지 않는, 즉 반환 값 이외의 외부 영향이 없는 함수다.

순수 함수의 미는 구조적 단순함에 있다. 순수 함수는 인수와 반환 값(즉, API)으로 축약되므로 복잡성의 막다른 길이라고 할 수 있다. 순수 함수와 외부 시스템과의 유일한 상호작용은 정의된 API를 통해 이뤄진다.

이는 객체 메소드가 객체의 상태(객체 멤버)와 상호작용하도록 설계되는 OOP와 대비되며, 외부 상태가 함수 내에서 조작되는 경우가 많은 절차적 스타일 코드와도 대비된다. 그러나 리액트(React)의 useEffect 후크에서 볼 수 있듯이 실제 환경에서 함수는 더 넓은 컨텍스트와 상호작용해야 하는 경우가 많다.
 

불변성

함수형 프로그래밍 철학의 또 다른 특징은 함수 외부에서 데이터를 수정하지 않는다는 것이다. 실무에서 이는 함수에 대한 입력 인수를 수정하지 않는 것을 의미한다. 대신 함수의 반환 값은 수행된 작업을 반영해야 부작용을 피할 수 있다. 이를 통해 함수가 더 큰 시스템 내에서 동작할 때 함수의 영향을 추론하기가 더 쉬워진다.
 

일급 함수

순수 함수 이상 외에, 실제 함수형 프로그래밍 코딩을 좌우하는 요소는 일급 함수다. 일급 함수는 독립적으로 취급이 가능한 '물자체(thing in itself)' 함수다. 함수형 프로그래밍은 함수를 변수, 인수 및 반환 값으로 사용할 수 있는 언어적 지원을 적극적으로 활용해 매끄러운 코드를 생성한다.

일급 함수는 매우 유연하고 유용하므로 자바 및 C#과 같은 강력한 OOP 언어도 일급 함수를 지원하고 이를 계기로 자바 8에서 람다 식 지원이 추가됐다.

일급 함수를 기술하는 또 다른 방법은 데이터형 함수(function as data)다. 일급 함수는 다른 데이터처럼 변수에 할당할 수 있다는 의미다. let myFunc = function(){}라는 코드는 함수를 데이터로 사용한다.
 

고차 함수

함수를 인수로 받거나 함수를 반환하는 함수를 고차 함수라고 한다. 함수를 다루는 함수라고도 볼 수 있다.

자바스크립트와 자바에는 최근 몇 년 사이 개선된 함수 구문이 추가됐다. 자바는 화살표 연산자와 더블콜론 연산자를 추가했으며 자바스크립트는 화살표 연산자를 추가했다. 이러한 연산자는 함수를 더 쉽게 정의하고 사용할 수 있게 해준다(특히 인라인을 익명 함수로). 익명 함수는 참조 변수 없이 정의 및 사용되는 함수다.
 

함수형 프로그래밍 예: 컬렉션

함수형 프로그래밍이 가장 빛을 발하는 예는 컬렉션 작업이다. 컬렉션의 여러 항목에 걸쳐 대량의 기능을 적용할 수 있다는 것이 순수 함수의 개념과 잘 맞아떨어지기 때문이다. 예제 1을 보면 자바스크립트 map() 함수를 활용해서 배열의 문자를 대문자로 바꾼다.
 
예제 1. 자바스크립트에서 map()과 익명 함수 사용하기
let letters = ["a", "b", "c"];
console.info( letters.map((x) => x.toUpperCase()) ); // outputs ["A", "B", "C"]

이 구문의 미는 코드가 상당히 응축되어 있다는 것이다. 루프 및 배열 조작과 같은 부수적 작업이 필요 없다. 수행되는 작업에 관한 사고의 과정이 명확하게 표현돼 있다. 예제 2의 자바 화살표 연산자로도 동일한 결과를 달성할 수 있다.
 
예제 2. 자바의 map()과 익명 함수 사용하기
import java.util.*;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList;
//...
List lower = Arrays.asList("a","b","c");
System.out.println(lower.stream().map(s -> s.toUpperCase()).collect(toList())); // outputs ["A", "B", "C"]

예제 2는 자바 8의 스트림 라이브러리를 활용해 문자 목록을 대문자화하는 같은 작업을 수행한다. 핵심 화살표 연산자 구문이 자바스크립트와 거의 같고, 하는 일도 똑같이 인수를 받아 로직을 수행하고 값을 반환하는 함수를 만든다. 여기서 중요한 점은 정의된 함수 본문에 중괄호가 없는 경우 반환 값이 자동으로 제공된다는 것이다.

계속해서 예제 3에서 자바의 더블콜론 연산자를 보자. 이 연산자는 클래스의 메소드, 이 경우 String 클래스의 toUpperCase 메소드를 참조할 수 있게 해준다. 예제 3은 예제 2와 같은 일을 한다. 다양한 시나리오에서 다양한 구문을 활용할 수 있다.
 
예제 3. 자바 더블콜론 연산자
// ...
List upper = lower.stream().map(String::toUpperCase).collect(toList());

이들 3개 예제에서 모두 고차 함수가 사용되는 것을 볼 수 있다. 두 언어에서 모두 map() 함수는 함수를 인수로 받는다. 달리 말하면, 함수를 다른 함수로 전달하는 것을 함수형 인터페이스로 볼 수 있다(Array API 또는 다른 방법으로). 공급자 함수(매개변수 함수를 소비하는 함수)는 일반화된 로직에 대한 플러그인이다.

이는 OOP의 전략 패턴과 매우 비슷하지만(실제로 자바에서는 단일 메소드와의 인터페이스가 내부적으로 생성됨), 함수의 간결함이 응축된 컴포넌트 프로토콜로 이어진다. 다른 예로 Node.js를 위한 익스프레스(Express) 프레임워크에서 경로 핸들러를 정의하는 예제 4를 보자.
 
예제 4. 익스프레스의 함수형 경로 핸들러
var express = require('express');
var app = express();
app.get('/', function (req, res) {
 res.send('One Love!');
});

예제 4는 경로 매핑과 요청 및 응답 처리를 위해 필요한 것을 명확하게 정의할 수 있게 해준다는 면에서 함수형 프로그래밍의 좋은 예다. 다만 여기서는 함수 본문 내에서 응답 객체를 조작하는 것이 부작용에 해당한다고 볼 수도 있다.
 

커리형 함수

이제 함수를 반환하는 함수라는 함수형 프로그래밍 개념에 대해 생각해 보자. 함수를 반환하는 함수는 인수로서의 함수보다는 덜 사용된다. 예제 5에서 굵은 화살표(=>) 구문이 체인으로 연결된 일반적인 리액트 패턴의 예를 볼 수 있다.
 
예제 5. 리액트의 커리드 함수
handleChange = field => e => {
e.preventDefault();
// Handle event
}

이 코드의 목적은 field, 그다음 event를 받는 이벤트 핸들러를 만드는 것이다. 같은 handleChange를 적용해 필드를 조작할 수 있으므로 유용하다. 즉, 같은 핸들러를 여러 필드에 사용할 수 있다.

예제 5는 커리드 함수(curried function)의 예다. '커리드 함수'라는 이름은 의도는 좋지만 함수의 개념을 나타내지는 않으므로 혼란스럽다. 어떤 경우든 기본 개념은 함수를 반환하는 함수에서 이 함수에 대한 호출을 체인으로 연결할 수 있다는 것이며, 이 방법이 여러 인수가 있는 하나의 함수를 만드는 방법보다 더 유연하다는 것이다.

이러한 종류의 함수를 호출할 때 눈에 띄는 특징은 handleChange(field)(event)와 같이 “체인으로 연결된 괄호” 구문이다.
 

대규모 프로그래밍

앞서 살펴본 예제는 압축된 컨텍스트에서 함수형 프로그래밍의 실무적 이해를 도모하지만 함수형 프로그래밍은 전체적인 프로그래밍 관점에서 더 큰 이익이 되도록 고안됐다. 달리 말하면 함수형 프로그래밍은 더 깔끔하고 탄력적인 대규모 시스템을 대상으로 만들어졌다.

이 개념에 대한 예제를 제공하기는 어렵지만 한 가지 사례는 리액트의 함수형 컴포넌트 지지다. 리액트 팀은 컴포넌트의 더 간결한 함수형 스타일이 인터페이스 아키텍처의 규모가 커질수록 더 복합적인 이점을 제공한다는 점에 주목했다.

함수형 프로그래밍을 폭넓게 사용하는 또 다른 시스템은 리액티브엑스(ReactiveX)다. 리액티브엑스가 사용하는 종류의 이벤트 스트림을 기반으로 하는 대규모 시스템은 분리된 소프트웨어 컴포넌트 상호작용에 따른 혜택을 얻을 수 있다. 앵귤러(Angular)는 이 강력함을 인지하고 리액티브엑스(RxJS)를 전면적으로 도입했다.
 

변수 범위와 컨텍스트

마지막으로, 패러다임으로서 함수형 프로그래밍이 가진 문제라고 단정 지을 수는 없지만 함수형 프로그래밍을 할 때 주의를 기울여야 할 매우 중요한 한 가지는 변수 범위와 컨텍스트다.

자바스크립트에서 컨텍스트의 구체적인 의미는 ‘this 키워드가 무엇이 되느냐’이다. 자바스크립트 화살표 연산자의 경우 this는 둘러싼 컨텍스트를 나타낸다. 전통적인 구문으로 정의된 함수는 자체 컨텍스트를 받는다. DOM 객체의 이벤트 핸들러는 이 사실을 이용해서 this 키워드가 처리되는 대상 요소를 나타내도록 보장할 수 있다.

범위는 변수의 지평선, 즉 보이는 변수가 무엇인지를 나타낸다. 모든 자바스크립트 함수(두꺼운 화살표와 일반 모두), 그리고 자바의 화살표로 정의되는 익명 함수에서 범위는 둘러싼 함수 본문의 범위다. 이러한 함수를 클로저(closure)라고 지칭하는 이유가 여기에 있다. 이 용어는 함수가 함수를 포함하는 범위 내에 둘러싸여 있음을 의미한다.

이것은 기억해야 할 중요한 점이다. 이러한 익명 함수는 범위 내의 함수에 대한 완전한 액세스 권한을 갖는다. 안쪽 함수가 바깥쪽 함수의 변수에 반해서 작동할 수 있으며 이 경우 순수하지 않은 함수 부작용으로 볼 수도 있다. editor@itworld.co.kr

X