자바8의 변경사항 및 새로운 기능을 정리하다가 함수형 프로그래밍에 대해 간단하게 짚고 넘어갈 필요성을 느꼈다.
함수형에서 함수는 말 그대로 수학에서 사용되는 함수처럼 입력값을 받고 출력값을 반환하는 함수이다.
함수형 프로그래밍에서는 이 함수를 중심으로 함수를 조합하고 변환하여 결과를 도출해내는 프로그래밍 스타일이라고 할 수 있다.
함수를 객체로 취급해야하며 변수나 인자로 함수를 전달하고 함수를 반환하는 고차함수로도 사용될 수 있어야 한다.
(그래서 자바8에서 Supplier, Function 인터페이스가 추가되었나보다.)
불변성이란 말 그대로 변하지 않는다는 뜻인데, 변수에 값이 한번 할당되면 변수의 값이 변하지 않는다는 것을 의미한다.
이 변수(데이터)로 새로운 데이터를 만드는 함수의 입력값으로 사용되어 데이터의 안정성을 높여야 한다.
오직 입력되는 불변성을 가지는 데이터에만 의존하는 함수를 뜻한다.
외부 상태에 의존하지 않고, 같은 입력값에 같은 출력값을 보장할 수 있는 함수라고 할 수 있다.
때문에 병렬처리, 멀티쓰레드로 처리되기에 매우 적합하며 재사용성 또한 높아지기에 순수 함수 구현을 지향해야 한다.
함수를 값으로 다룰 수 있는 개념이다.
즉, 함수를 변수에 할당할 수 있고, 함수의 인자로 전달하거나, 함수 자체를 반환값으로 사용할 수 있다.
다시 말해 함수를 다른 데이터 타입과 똑같이 다룰 수 있다는 것이다.
함수 자체를 인자로 받거나 반환하는 함수이다.
때문에 함수를 다양하게 조합하고 활용하거나 추상화하여 사용하는데 유리하며 곧 특정 데이터를 유연하게 처리할 수 있다.
딱보면 거기서 거기인거 같아 햇갈리고 혼용되는거 같다.
또한 어떠한 경우에는 일급 함수이면서 고차 함수일 수도 있어서 더욱 햇갈리는거 같다.
일급 함수는 함수를 반환값으로, 고차 함수는 함수를 반환하는..(???)내가 찾아보고 생각하는 바로는 관점의 차이가 아닐까 싶다.
(속시원하게 차이점을 설명해주는 내용을 찾을 수 없음;;)일급 함수는 함수 자체를 값으로 다룬다는 것에 초점을 맞춰 설명하고 있다.
그래서 함수를 인자값으로, 변수값으로, 반환값으로 활용한다면 일급 함수라고 할 수 있다.반면 고차 함수는 함수 자체를 인자로 받거나 반환한다는 것에 초점을 두고 설명한다.
그럼 함수 자체를 값으로 다루면서 함수를 반환하면? 일급 함수이면서 고차 함수이다.
그럼에도 약간의 차이를 보이는데 위에서 설명한대로
함수를 값으로 다루기만 한다면 일급 함수, 함수를 반환하기만 한다면 고차 함수이다.
일급 함수인 예시)
void firstClassExample() {
// 함수 자체를 값으로 다루기에 변수에 대입한다.
Function<Integer, Integer> plusOne = v -> v + 1;
Function<Integer, Integer> plusTwo = v -> v + 2;
System.out.println(plusOne.apply(10) + plusTwo.apply(10)); // (10+1) + (10+2) = 23
}
고차 함수인 예시)
// 같은 입력값에 같은 출력값을 보장할 수 있는 순수 함수(Pure Function)이기도 하다.
Function<Integer, Integer> plusFive(int number) {
return v -> v + 5; // 인자(v)에 5를 더해주는 '함수'를 반환한다.
}
일급 함수이면서, 고차 함수인 예시)
// 입력값 Integer, 반환값은 Function
Function<Integer, Function<Integer, Integer>> plus5AndReturnTimesFunc = number -> {
// 위 고차함수 예시인 plusFive 메소드를 값처럼 다룸
Function<Integer, Integer> plus5Func = plusFive(number);
// 입력값을 plus5Func의 결과와 곱하는 함수 반환
return v -> v * plus5Func.apply(number);
};
// 7 + 5의 결과값인 12를 곱해주는 함수 반환
Function<Integer, Integer> timesFunc = plus5AndReturnTimesFunc.apply(7);
System.out.println(timesFunc.apply(12)); // 10을 곱해 120을 출력
마지막 예시에서 plusFive
메소드를 plus5Func 변수에 할당함으로서 일급 함수의 특징을 가지며, 입력값에 plusFive
의 결과인 12를 곱해주는 함수를 반환함으로서 고차 함수의 특징을 가진다.
즉 일급 함수이면서, 동시에 고차 함수라고 할 수 있다.
정리하자면,
일급 함수는 함수 자체를 값처럼 다룬다는 것으로, 함수를 동적으로 활용할 수 있게 해주는 함수라고 하면 될 것 같다.고차 함수는 함수 자체를 인자로 받거나 반환한다는 것으로, 함수를 다양한 형태로 조합하거나 추상화하는 함수라고 정리할 수 있지 않을까...
익명 함수(Anonymous Function)라고도 불린다.
위 고차 함수와 같이 사용하여 효율적으로, 간결하게 표현하는데 자주 사용된다.
예를 들어 자바에서 Stream API의 중간연산 메소드들을 활용할때 가독성 높은 코드를 작성할 수 있다.
람다 함수는 Closure 라는 개념을 가지고 있는데 해당 람다 함수 외부의 값을 활용할 수 있는 개념이다.
이 개념때문에 유용한 함수를 작성할 수 있으니 아래 서술할 부작용에 유의해야한다.
람다 함수에서 함수 외부의 값을 참조하고, 나중에 람다 함수에서 그 외부의 값을 그대로 사용한다는 람다 함수의 특성 중 하나이다.
위 일급 함수이면서, 고차 함수인 예시 중에 return v -> v * plus5Func.apply(number);
부분에서 외부에서 선언된 number(값 : 7)는 클로저라고 할 수 있다.
함수가 실행되면 함수 외부의 값이나 상태를 변경하거나 함수 외부, 또는 다른 함수의 내부와의 상호작용을 의미한다.
예를 들어, 위의 '일급 함수이면서, 고차 함수인 예시'에서 number
의 값인 12가 변경된다면 부작용이라고 볼 수 있다.
number
가 12인 것은 plus5AndReturnTimesFunc.apply(7);
에서 예측할 수 있지만 외부의 영향 등으로 값이 변경된다면 예측하기 어려워 진다.
이는 곧 예상치 못한 동작을 유발하거나 테스트, 디버깅을 어렵게 만들 수 있다.
또한 함수 외부와 상호작용 한다는 것은 병렬처리에도 여러움이 있을 가능성이 크다.
때문에 함수형 프로그래밍에서는 함수가 독립적이고 예측 가능하게 동작하도록 구현하는 것을 지향해야한다.
위에서 서술한 기본 개념을 생각해보면 함수형 프로그래밍의 특징과 지향하는 점들은 부작용과 버그 방지, 테스트 등에서 많은 장점을 가진다.
순수 함수의 개념에서 가지는 특성 중 하나인 독립적인 동작과, 고차함수와 일급함수의 특성을 활용하여 로직의 테스트 및 디버깅이 쉬워져 함수단위에서 높은 테스트 커버리지를 가질 수 있다.
이 외에도 몇가지 장점이 있다.
데이터가 불변성을 가진다는건 공유자원으로 사용되기 쉽다.
즉, 멀티쓰레드 환경에서 공유자원에 대해 Race Condition 을 피할 수 있다.
함수형 프로그래밍에서는 데이터의 불변성을 유지하기 위해 상태를 변경하는 대신 데이터를 복사하여 새로운 데이터를 생성하는 방식을 지향해야 한다.
이를 통해 여러 개의 스레드나 프로세스에서 동시에 작업을 수행하더라도 원본 데이터가 변경되지 않고 각각의 작업이 독립적으로 처리되어 데이터의 일관성을 유지함과 동시에 병렬성과 분산성을 높일 수 있다.
위 고차 함수와 일급 함수의 설명처럼 함수형 프로그래밍은 작은 함수들을 조합하여 더 큰 함수나 모듈을 만들어내는 방식으로 코드를 작성한다.
각각 특정 기능을 하는 작은 함수들로 더 큰 함수로 구성하게 되면 코드의 구조와 로직을 쉽게 파악되며 유지보수에도 유리하다.
이렇게 작게 함수를 나누고 조합하는 것을 컴포지션(Compostion)이라고 한다.
또한 오직 입력에만 의존하는 순수 함수 개념으로 함수들을 독립적으로 재사용 가능하기에, 해당 함수를 다양한 로직에서 재사용이 가능한데, 이를 통해 코드의 중복을 피하고 높은 생산성을 기대할 수 있다.
함수 합성 (Compostion) 이란?
위에서 언급한 작게 함수를 나누고 조합하는 것을 컴포지션이라고 한다.
크게 Pipeline과 Chain 방식이 있다.기본적으로 함수들을 연속적으로 호출하여 입력값을 순차적으로 처리하는 방식이다.
A ➡️ B ➡️ C 의 순서로 함수가 실행된다고 하면,
A함수의 출력은 B의 입력, B의 출력은 C의 입력되어 처리되는 방식이다. ex.) C(B(A(x)))함수형 프로그래밍이에서 이런 함수 합성을 통해 구현된 함수를 합성 함수(Compose Function)라고도 한다.
자바에서 연속해서 메소드를 호출하는 메소드 체이닝이 대표적인 예라고 할 수 있다.
자바에서 Stream API 등에서 메소드 체이닝을 구현하는 것을 함수형 프로그래밍 관점에서 Pipeline 방식이라고 볼 수 있다.Pipeline 말고도 Curry 라고 불리는 방식도 있다.
Curry(또는 Currying)은 각각의 함수에서 하나의 인자를 가지는 함수를 생성하면서, 함수의 인자를 하나씩 하나씩 적용하는 것이다.
하지만 자바에서는 타입을 추론하지 못하기에 직접적으로 이 방식을 지원하지않는다.
(자바는 8부터 함수형 프로그래밍을 지원해도 태생이 객체지향이라서??)
자바에서의 Currying 방식 참고 : http://egloos.zum.com/ryukato/v/1160506
(함수형 프로그래밍을 지원하기 시작한) 자바8부터 지원하는 Stream API를 사용하여 데이터 조작하는 방법 및 병렬 및 동시성 처리 참고 : https://velog.io/@cv_/Java8-부터-사용가능한-문법과-기능#stream-api
또는 CompletableFuture
객체를 활용하여 비동기적인 작업을 조합하고 연결하거나 결과를 합성, 에러 처리 등의 함수형 프로그래밍을 지원한다.
참고 : https://anythingis.tistory.com/119#CompletableFuture-1
순수 함수 개념과 불변성을 강조하는 만큼 특정 데이터와 타입에 대해 알아놔야할 필요가 있다.
예시로, 자바에서는 Immutable Collections, 열거형 클래스, Atomic 타입 등이 있다.
(때문에 함수형 프로그래밍은 객체지향 프로그래밍과는 다른 새로운 개념과 용어를 익혀야 하는 불편함이 있는것 같다)
이를 통해 멀티쓰레드 환경에서 안전하게 병렬 처리가 되도록 구현해야 한다.
명령형 프로그래밍이 보다 오랜 역사와 넓은 생태계를 가지고 있기 때문에, 함수형 프로그래밍에 비해 라이브러리와 생태계가 상대적으로 제한적이며 직접 구현해야되는 경우가 비교적 많을 수 있다.
(Reactor(WebFlux), Cyclops, Vavr 등이 있긴 한거 같은데??)
위 Curry 방식을 직접적으로 지원하지 않는것부터 특정 프로젝트를 위한 불변성과 순수 함수의 개념을 포함한 라이브러리나 프레임워크가 부족할 수 있다고 한다.