자바 8, 9, 10에 대한 책을 읽으면서 함수형 프로그래밍에 대해 알게 된 것과 느낀 것을 정리하려고 합니다. 개인적인 참고 용도와 저처럼 초보 개발자들을 위해 적겠습니다.😀
함수형 프로그래밍이란 무엇일까?
자바에서는 stream
, map
, filter
등 많은 것들이 떠오르겠지만 위키피디아에서는 아래와 같이 정의한다.
함수형 프로그래밍(函數型 프로그래밍, 영어: functional programming)은 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다
나머지 단어는 알겠는데 “수학적 함수의 계산”이란 무엇일까? 명령형 프로그래밍에서 다루는 함수는 수학에서 정의하는 함수와 다르다. 수학적 함수는 매번 같은 입력이 주어질 때마다 같은 출력이 나오지만 명령형 프로그래밍은 아래와 같이 상태의 값을 꿔 부작용(side effect)이 생겨 매번 출력이 다르게 나올 수 있다.
1
2
3
4
5
6
7
8
9
10
class Main() {
private int total = 0;
public static void main(String[] args) {
System.out.println(add(1)); // 1
System.out.println(add(1)); // 2
}
private add(int num) { // total의 상태를 바꾸고 있고 그에 따라 결과가 매번 바뀜
total += num;
return total;
}
반면 아래와 같은 함수는 수학적 함수라고 볼 수 있다.
1
2
3
4
5
6
7
8
9
class Main() {
private int total = 0;
public static void main(String[] args) {
System.out.println(add(1, 1)); // 2
}
private add(int num1, int num2) { // 상태를 바꾸지 않음
return num1 + num2;
}
}
수학적 함수를 사용하면 자연스럽게 상태와 가변 데이터를 멀리하는 코드가 나올 수 밖에 없다. 이렇게 함수형 프로그래밍은 상태와 가변 데이터를 멀리하게 끔 수학적 함수로 코드를 짜는 프로그래밍 종류 중 하나다. 그러면 함수형 프로그래밍을 통해 얻는 이점은 무엇일까?
코드 이해가 쉽다 (강력한 추상화)
함수형 프로그래밍은 부작용이 없는 것이 보장되어 결과만 생각하며 코드를 읽을 수 있고 또한 내부 로직을 타고 들어가지 않아도 함수 이름으로 결과를 유추할 수 있다.
병렬 프로그래밍을 짜기가 쉽다
가변 데이터를 건드리지 않으므로 동시성 프로그래밍에서 생각할 것이 줄어든다. 쓰레드가 한 자원을 공유해서 쓰면서 상태를 바꾸면 예상치 못한 버그가 나올 수 있다. 하지만 상태를 바꾸지 않는 함수형 프로그래밍에서는 여러 스레드가 해당 데이터에 동시에 접근하더라도 데이터가 변경되지 않는 것을 보장하기 때문에 레이스 컨디션에 대한 버그는 나올 수 없다.
쇼트 서킷과 최적화
함수형 프로그래밍이기 보다는 자바의 Stream
과 관련 메서드에 관한 장점이지만 어차피 자바를 공부하는 것이기 때문에 얘기한다😙 Stream
으로 Collection
을 처리하면 명령형 프로그래밍으로는 귀찮은 여러가지 최적화를 해준다.
Lazy Evaluation
함수형 프로그래밍은 어떻게 할지(How)보다 무엇을 할지(What)을 정의한다. 그래서 Stream
을 사용할 때 중간연산으로 무엇을 할지를 정의하고 마지막 최종 연산으로 계산을 한다. 이렇게 게으르게 계산을 해서 얻는 최적화가 있다.
Short Curcuit
쇼트 서킷은 처리하지 않아도 되는 것은 처리하지 않는 최적화다. 예를 들어 조건문에서 &&
앞에가 False
이면 뒷 문장은 실행하지 않고 ||
앞에가 True
이면 뒷 문장은 실행하지 않는다. Stream
에서도 비슷한 최적화가 적용된다.
1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(1,2,3,4,5,6,7,8,9,10);
list.stream().map(i -> {
System.out.println(i) // 5번만 실행됨
return i;
}).limit(5).toList();
}
}
위 코드에서 요소를 5개까지 제한하는 limit
함수 때문에 5 이후에 값은 볼 필요가 없으므로 5까지 출력이되고 나머지 요소는 출력되지 않는다.
중간연산의 동시적으로 실행
파이프라인으로 연결된 함수들이 동시에 실행이된다.
1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(1,2,3,4,5,6,7,8,9,10);
list.stream().map(i -> {
System.out.println("1번째 map의 " + i);
return i;
}).map(i -> {
System.out.println("2번째 map의 " + i);
return i;
}).toList();
}
}
위 코드를 실행해보면 1번째 map과 2번째 map이 번갈아가며 실행되는 것을 알 수 있다.
책을 읽으면서 함수형 프로그래밍이 멀티쓰레드 환경에서 CPU자원을 잘로활용하려면 필수적이라고 느꼈습니다. 현재 많은 컴퓨터가 멀티 코어를 지원하고 직접회로의 한계 때문에 개별 코어의 성능을 높이기 보다는 코어수를 늘리는 방향으로 진화하고 있습니다. 따라서 멀티 코어를 잘 활용할 수 있어야 비지니스 비용을 아낄 수 있을 거 같습니다.🤑 하지만 명령형 프로그래밍에 익숙한 저는 함수형 프로그래밍으로 생각하는게 어렵네요… 다음 번엔 Stream
을 사용해서 병렬 프로그래밍하는 방법을 정리하겠습니다!