✏️[모던 자바 인 액션, 전문가를 위한 자바 8, 9, 10 기법 가이드] 스터디 관련 책 내용을 정리한 글입니다.

📌 이 장의 내용
- 왜 함수형 프로그래밍을 사용하는가?
- 함수형 프로그래밍은 어떻게 정의하는가?
- 선언형 프로그래밍과 참조 투명성
- 함수형 스타일의 자바 구현 가이드라인
- 반복과 재귀
이 장에서는 함수형 프로그래밍이란 무엇인지 설명하고, 함수형 프로그래밍의 개념과 관련 용어를 살펴본 후, 함수형 프로그래밍이 제공하는 이점인 부작용, 불변성, 선언형 프로그래밍, 참조 투명성, 자바 8에서 제공하는 기능 등을 설명합니다. 그리고 고차원 함수, 커링, 영속 데이터 구조체, 게으른 리스트, 패턴 패칭, 콤비네이터 등 함수형 프로그래밍의 기법을 자세히 살펴봅니다.
18.1 시스템 구현과 유지보수
이 절에서는 대규모 소프트웨어 시스템 업그레이드 관리 요청에 대해 노련한 자바 개발자들의 경험과 자바 8의 스트림 기능을 소개하며, 상태 없는 동작을 유지하고 시스템 구조를 이해하기 쉽게 클래스 계층으로 구성하면 유지보수가 용이해진다는 것을 강조합니다. 그리고 코드 크래시 디버깅 문제와 함께 이 문제를 해결할 수 있는 함수형 프로그래밍의 부작용 없음과 불변성 개념에 대해 설명합니다.
18.1.1 공유된 가변 데이터
변수가 예상하지 못한 값을 갖는 이유는 시스템의 여러 메서드에서 공유된 가변 데이터 구조를 읽고 갱신하기 때문이며, 공유 가변 데이터 구조를 사용하면 프로그램 전체에서 데이터 갱신 사실을 추적하기가 어려워지기 때문에 유지보수가 어렵습니다. 예를 들어, 리스트를 참조하는 여러 클래스가 있을 때, 리스트의 소유자는 어느 클래스인지, 한 클래스가 리스트를 갱신하면 다른 클래스는 해당 사실을 알고 있는지, 리스트 갱신 사실을 추적하는 것이 좋은지 아니면 안전하게 리스트 사본을 만드는 것이 나은지 등의 문제가 발생할 수 있습니다.
자료구조를 바꾸지 않는 시스템에서 순수 메서드 또는 부작용 없는 메서드를 사용하면 유지보수가 용이해집니다. 순수 메서드는 자신을 포함하는 클래스와 다른 객체의 상태를 바꾸지 않으며 return문을 통해서만 결과를 반환합니다. 반면에 부작용은 함수 내에 포함하지 못한 기능으로, 외부 상태를 변경하거나 전역 변수를 참조하는 것 등이 있습니다. 다음은 부작용의 예입니다.
- 자료구조를 고치거나 필드에 값을 할당(setter 메서드 같은 생성자 이외의 초기화 동작)
- 예외 발생
- 파일에 쓰기 등의 I/O 동작 수행
불변 객체를 사용하면 부작용을 없앨 수 있고, 이는 스레드 안전성과 멀티코어 병렬성을 제공합니다. 부작용 없는 시스템은 독립적인 부분을 이해하기 쉽게 만들어주며, 함수형 프로그래밍에서 이 개념이 유래되었습니다. 이를 위해 먼저 선언형 프로그래밍의 개념을 살펴볼 필요가 있습니다.
18.1.2 선언형 프로그래밍
프로그램으로 시스템을 구현하는 방식은 두 가지로 구분할 수 있습니다. 하나는 작업을 어떻게 수행할 것인지에 집중하는 명령형 프로그래밍 방식이며, 이는 고전적인 객체지향 프로그래밍에서 사용됩니다. 명령형 프로그래밍은 할당, 조건문, 분기문, 루프 등과 같은 명령어를 사용하며, 저수준 언어와 비슷합니다.
Transaction mostExpensive = transactions.get(0);
if (mostExpensive == null)
throw new IllegalArgumentException("Empty list of transactions")
for (Transaction t : transactions.subList(1, transactions.size())) {
if (t.getValue() > mostExpensive.getValue()) {
mostExpensive = t;
}
}
프로그램으로 시스템을 구현하는 두 번째 방식은 '어떻게'가 아닌 '무엇을'에 집중하는 방식입니다. 이는 스트림 API를 사용하여 작업을 처리할 때 사용됩니다.
Optional<Transaction> mostExpensive = transactions.stream()
.max(comparing(Transaction::getValue());
내부 반복 방식은 질의문을 어떻게 구현하느냐에 따라 결정됩니다. 내부 반복 프로그래밍은 문제를 어떻게 해결할지에 대해 직접적으로 명시합니다. 반면, 선언형 프로그래밍은 무엇을 해결할지에 대해 명시하고, 시스템이 목표를 달성하는 방법을 규칙으로 정하는 방식입니다. 이는 문제 자체가 코드로 명확하게 드러난다는 장점을 가집니다.
18.1.3 왜 함수형 프로그래밍인가?
함수형 프로그래밍은 선언형 프로그래밍을 따르며 부작용이 없는 계산을 지향하는 프로그래밍 방식입니다. 람다 표현식과 스트림 등의 기능을 이용해 코드를 자연스럽고 읽고 쓰기 쉽게 구현할 수 있습니다. 함수형 프로그래밍을 자바 8과 연결하면 부작용이 없는 복잡하고 어려운 기능을 수행하는 프로그램을 구현할 수 있습니다.
18.2 함수형 프로그래밍이란 무엇인가?
함수형 프로그래밍은 부작용이 없는 수학적 함수를 사용하는 프로그래밍 방식입니다. 함수는 여러 입력을 받아서 여러 출력을 생성하는 블랙박스이며, 항상 같은 결과를 반환해야 합니다. 함수형 프로그래밍은 순수 함수형 프로그래밍과 함수형 프로그래밍으로 나뉘는데, 전자는 함수와 if-then-else 등의 수학적 표현만 사용하는 방식이며, 후자는 시스템의 다른 부분에 영향을 미치지 않는다면 내부적으로는 함수형이 아닌 기능도 사용할 수 있는 방식입니다. 내부적으로 부작용이 발생하지만 호출자에게는 영향을 미치지 않으면 함수형 프로그래밍으로 간주됩니다. 함수형 프로그래밍은 코드가 간결하고 예측 가능하며, 병렬 처리와 같은 최적화 기법을 적용하기에 용이합니다.
18.2.1 함수형 자바
자바에서는 완벽한 순수 함수형 프로그래밍을 구현하기 어렵지만 부작용을 일으키지 않는 함수나 메서드를 이용하여 함수형 프로그래밍을 구현할 수 있습니다. 함수나 메서드에서는 지역 변수만을 변경하고, 참조하는 객체는 불변 객체여야 합니다. 함수형이라고 말할 수 있는 함수나 메서드는 어떤 예외도 일으키지 않아야 하며, 입력값에 따라 정확하게 하나의 결과를 반환해야 합니다. Optional <T>를 <T> 사용하여 예외 없이 함수를 표현할 수 있습니다. 라이브러리 함수를 사용할 때는 부작용을 감출 수 있는 상황에서만 사용해야 하며, 주석이나 마커 어노테이션을 사용하여 부작용이 있는 메서드를 명시해 주는 것이 좋습니다. 디버깅 정보를 출력하는 것은 함수형의 규칙에 위배되지만, 로그 출력을 제외하고는 함수형 프로그래밍의 장점을 유지할 수 있습니다.
18.2.2 참조 투명성
참조적 투명성은 같은 인수로 함수를 호출할 때 항상 같은 결과를 반환하는 것을 의미합니다. 따라서 부작용을 감춰야 한다는 제약은 참조적 투명성 개념으로 귀결됩니다. 함수형 코드에서는 final 변수를 변경하지 않는 연산과 같이 항상 같은 결과를 생성하는 연산을 함수형으로 구현할 수 있습니다. List를 반환하는 메서드는 두 번 호출하면 결과가 같아도 서로 다른 메모리 공간에 생성된 리스트를 참조할 수 있으므로 참조적으로 투명한 메서드가 아닐 수 있습니다. 하지만 함수형 코드에서는 일반적으로 이런 함수를 참조적으로 투명한 것으로 간주합니다.
18.2.3 객체지향 프로그래밍과 함수형 프로그래밍
이 절에서는 함수형 프로그래밍과 익스트림 객체지향 프로그래밍을 비교하고, 자바 8에서는 이 두 가지 프로그래밍 형식을 혼합하는 경향이 있다는 것을 설명합니다. 또한 하드웨어 변경과 데이터 조작에 대한 프로그래머의 기대치로 인해 자바 소프트웨어 엔지니어의 프로그래밍 형식이 좀 더 함수형으로 다가갈 것이라고 예측하고 있으며, 이 책은 이러한 변화에 대응하는 방법을 설명합니다. 프로그래밍 형식을 스펙트럼으로 표현하여, 객체지향 프로그래밍과 함수형 프로그래밍이 서로 어디에 위치하는지 설명하고, 자바 프로그래머가 이 두 가지 프로그래밍 형식을 혼합하여 사용할 수 있음을 보여주며, 이를 통해 모듈성이 좋고 멀티코어 프로세서의 적합한 프로그램을 구현하는 데 도움을 주는 함수형 프로그래밍의 기능을 소개하는 것이 이 절과 19장의 목표라고 요약할 수 있습니다.
18.3 재귀와 반복
순수 함수형 프로그래밍 언어에서는 반복문을 사용하지 않는 이유는 변화를 피하기 위해서입니다. 일반적으로 반복문은 코드에 변화를 자연스럽게 스며들게 하기 때문입니다. 함수형 프로그래밍에서는 지역 변수는 자유롭게 갱신할 수 있지만, 루프의 조건문을 갱신해야 할 경우 안전하게 사용할 수 없습니다. 다음은 자바의 Iterator로 for (Apple a: apples) { }라는 for-each 루프를 표현한 코드입니다.
Iterator<Apple> it = apples.iterator();
while (it.hasNext()) {
Apple apple = it.next();
// ...
}
for-each 루프를 사용하는 검색 알고리즘에서는 루프 내부에서 apple 변수의 값을 변경하는 것이 안전하지 않습니다. 이는 변화를 확인할 수 없는 상황에서는 예기치 않은 결과를 초래할 수 있기 때문입니다. 따라서 이러한 상황에서는 명시적인 while 루프를 사용하는 것이 더 안전합니다. 하지만 다음 코드처럼 for-each 루프를 사용하는 검색 알고리즘은 문제가 될 수 있습니다.
public void searchForGold(List<String> l, Stats stats) {
for (String s : l) {
if ("gold".equals(s)) {
stats.incrementFor("gold");
}
}
}
for-each 루프를 사용하는 검색 알고리즘은 상충하는 부작용을 발생시킬 수 있기 때문에 하스켈 같은 순수 함수형 프로그래밍 언어에서는 부작용 연산을 제거합니다. 이를 해결하기 위해서는 재귀를 이용하여 구현할 수 있으며, 이를 통해 변화가 일어나지 않도록 할 수 있습니다. 다음은 팩토리얼 함수로, 반복과 재귀 방식으로 해결할 수 있는 고전적 문제입니다. 여기서 입력은 1보다 크다고 가정합니다.
// 반복 방식의 팩토리얼
static int factorialIterative(int n) {
int r = 1;
for (int i = 1; i <= n; i++) {
r *= i;
}
return r;
}
// 재귀 방식의 팩토리얼
static long factorialRecursive(long n) {
return n == 1 ? 1 : n * factorialRecursive(n - 1);
}
첫 번째 예제는 일반적인 루프를 사용한 코드로 매 반복마다 변수 r과 i가 갱신되며, 두 번째 예제는 재귀 방식의 코드로 문제를 해결합니다. 하지만 이 책을 학습한 독자라면 자바 8 스트림을 이용해 더 간단하게 팩토리얼을 정의할 수 있다는 사실을 알고 있을 것입니다.
static long factorialStreams(long n) {
return LongStream.rangeClosed(1, n)
.reduce(1, (long a, long b) -> a * b);
}
함수형 프로그래밍에서는 무조건 재귀가 좋다는 주장에 주의해야 한다는 것이 중요하다. 이는 재귀 함수를 호출할 때마다 메모리 사용량이 증가하므로, 큰 입력값을 사용하면 StackOverflowError가 발생할 수 있기 때문이다. 하지만 함수형 언어에서는 꼬리 호출 최적화를 제공하여 이 문제를 해결할 수 있다는 것이다.
static long factorialTailRecursive(long n) {
return factorialHelper(1, n);
}
static long factorialHelper(long acc, long n) {
return n == 1 ? acc : factorialHelper(acc * n, n - 1);
}
일반 재귀와 달리, 꼬리 재귀에서는 컴파일러가 하나의 스택 프레임을 재활용할 가능성이 생긴다. 꼬리 재귀에서는 중간 결과(팩토리얼의 부분결과)를 함수의 인수로 직접 전달한다. 이와 달리 일반 재귀는 중간 결과를 각각의 스택 프레임으로 저장해야 한다.
아래 두 그림은 팩토리얼의 재귀 정의와 꼬리 재귀 정의의 차이를 보여줍니다.


자바에서는 최적화된 꼬리 재귀를 지원하지 않지만, 순수 함수형을 유지하면서도 유용성과 효율성을 얻을 수 있습니다. 또한 자바 8에서는 반복을 스트림으로 대체하여 변화를 피하고, 재귀를 이용하면 더 간결하고 부작용이 없는 알고리즘을 만들 수 있습니다. 이러한 함수형 프로그래밍 기법은 기존의 자바 버전에서도 적용할 수 있습니다. 다음 장에서는 자바 8에 도입된 일급 함수를 이용한 가능성을 살펴봅니다.
'📚 서적 및 학습자료 > 모던 자바 인 액션[2회독]' 카테고리의 다른 글
[모던 자바 인 액션] 20장 OOP와 FP의 조화 : 자바와 스칼라 비교 (0) | 2023.04.02 |
---|---|
[모던 자바 인 액션] 19장 함수형 프로그래밍 기법 (0) | 2023.04.02 |
[모던 자바 인 액션] 17장 리액티브 프로그래밍 (0) | 2023.04.02 |
[모던 자바 인 액션] 16장 CompletableFuture : 안정적 비동기 프로그래밍 (0) | 2023.04.02 |
[모던 자바 인 액션] 15장 CompletableFuture와 리액티브 프로그래밍 컨셉의 기초 (0) | 2023.03.29 |
✏️[모던 자바 인 액션, 전문가를 위한 자바 8, 9, 10 기법 가이드] 스터디 관련 책 내용을 정리한 글입니다.

📌 이 장의 내용
- 왜 함수형 프로그래밍을 사용하는가?
- 함수형 프로그래밍은 어떻게 정의하는가?
- 선언형 프로그래밍과 참조 투명성
- 함수형 스타일의 자바 구현 가이드라인
- 반복과 재귀
이 장에서는 함수형 프로그래밍이란 무엇인지 설명하고, 함수형 프로그래밍의 개념과 관련 용어를 살펴본 후, 함수형 프로그래밍이 제공하는 이점인 부작용, 불변성, 선언형 프로그래밍, 참조 투명성, 자바 8에서 제공하는 기능 등을 설명합니다. 그리고 고차원 함수, 커링, 영속 데이터 구조체, 게으른 리스트, 패턴 패칭, 콤비네이터 등 함수형 프로그래밍의 기법을 자세히 살펴봅니다.
18.1 시스템 구현과 유지보수
이 절에서는 대규모 소프트웨어 시스템 업그레이드 관리 요청에 대해 노련한 자바 개발자들의 경험과 자바 8의 스트림 기능을 소개하며, 상태 없는 동작을 유지하고 시스템 구조를 이해하기 쉽게 클래스 계층으로 구성하면 유지보수가 용이해진다는 것을 강조합니다. 그리고 코드 크래시 디버깅 문제와 함께 이 문제를 해결할 수 있는 함수형 프로그래밍의 부작용 없음과 불변성 개념에 대해 설명합니다.
18.1.1 공유된 가변 데이터
변수가 예상하지 못한 값을 갖는 이유는 시스템의 여러 메서드에서 공유된 가변 데이터 구조를 읽고 갱신하기 때문이며, 공유 가변 데이터 구조를 사용하면 프로그램 전체에서 데이터 갱신 사실을 추적하기가 어려워지기 때문에 유지보수가 어렵습니다. 예를 들어, 리스트를 참조하는 여러 클래스가 있을 때, 리스트의 소유자는 어느 클래스인지, 한 클래스가 리스트를 갱신하면 다른 클래스는 해당 사실을 알고 있는지, 리스트 갱신 사실을 추적하는 것이 좋은지 아니면 안전하게 리스트 사본을 만드는 것이 나은지 등의 문제가 발생할 수 있습니다.
자료구조를 바꾸지 않는 시스템에서 순수 메서드 또는 부작용 없는 메서드를 사용하면 유지보수가 용이해집니다. 순수 메서드는 자신을 포함하는 클래스와 다른 객체의 상태를 바꾸지 않으며 return문을 통해서만 결과를 반환합니다. 반면에 부작용은 함수 내에 포함하지 못한 기능으로, 외부 상태를 변경하거나 전역 변수를 참조하는 것 등이 있습니다. 다음은 부작용의 예입니다.
- 자료구조를 고치거나 필드에 값을 할당(setter 메서드 같은 생성자 이외의 초기화 동작)
- 예외 발생
- 파일에 쓰기 등의 I/O 동작 수행
불변 객체를 사용하면 부작용을 없앨 수 있고, 이는 스레드 안전성과 멀티코어 병렬성을 제공합니다. 부작용 없는 시스템은 독립적인 부분을 이해하기 쉽게 만들어주며, 함수형 프로그래밍에서 이 개념이 유래되었습니다. 이를 위해 먼저 선언형 프로그래밍의 개념을 살펴볼 필요가 있습니다.
18.1.2 선언형 프로그래밍
프로그램으로 시스템을 구현하는 방식은 두 가지로 구분할 수 있습니다. 하나는 작업을 어떻게 수행할 것인지에 집중하는 명령형 프로그래밍 방식이며, 이는 고전적인 객체지향 프로그래밍에서 사용됩니다. 명령형 프로그래밍은 할당, 조건문, 분기문, 루프 등과 같은 명령어를 사용하며, 저수준 언어와 비슷합니다.
Transaction mostExpensive = transactions.get(0);
if (mostExpensive == null)
throw new IllegalArgumentException("Empty list of transactions")
for (Transaction t : transactions.subList(1, transactions.size())) {
if (t.getValue() > mostExpensive.getValue()) {
mostExpensive = t;
}
}
프로그램으로 시스템을 구현하는 두 번째 방식은 '어떻게'가 아닌 '무엇을'에 집중하는 방식입니다. 이는 스트림 API를 사용하여 작업을 처리할 때 사용됩니다.
Optional<Transaction> mostExpensive = transactions.stream()
.max(comparing(Transaction::getValue());
내부 반복 방식은 질의문을 어떻게 구현하느냐에 따라 결정됩니다. 내부 반복 프로그래밍은 문제를 어떻게 해결할지에 대해 직접적으로 명시합니다. 반면, 선언형 프로그래밍은 무엇을 해결할지에 대해 명시하고, 시스템이 목표를 달성하는 방법을 규칙으로 정하는 방식입니다. 이는 문제 자체가 코드로 명확하게 드러난다는 장점을 가집니다.
18.1.3 왜 함수형 프로그래밍인가?
함수형 프로그래밍은 선언형 프로그래밍을 따르며 부작용이 없는 계산을 지향하는 프로그래밍 방식입니다. 람다 표현식과 스트림 등의 기능을 이용해 코드를 자연스럽고 읽고 쓰기 쉽게 구현할 수 있습니다. 함수형 프로그래밍을 자바 8과 연결하면 부작용이 없는 복잡하고 어려운 기능을 수행하는 프로그램을 구현할 수 있습니다.
18.2 함수형 프로그래밍이란 무엇인가?
함수형 프로그래밍은 부작용이 없는 수학적 함수를 사용하는 프로그래밍 방식입니다. 함수는 여러 입력을 받아서 여러 출력을 생성하는 블랙박스이며, 항상 같은 결과를 반환해야 합니다. 함수형 프로그래밍은 순수 함수형 프로그래밍과 함수형 프로그래밍으로 나뉘는데, 전자는 함수와 if-then-else 등의 수학적 표현만 사용하는 방식이며, 후자는 시스템의 다른 부분에 영향을 미치지 않는다면 내부적으로는 함수형이 아닌 기능도 사용할 수 있는 방식입니다. 내부적으로 부작용이 발생하지만 호출자에게는 영향을 미치지 않으면 함수형 프로그래밍으로 간주됩니다. 함수형 프로그래밍은 코드가 간결하고 예측 가능하며, 병렬 처리와 같은 최적화 기법을 적용하기에 용이합니다.
18.2.1 함수형 자바
자바에서는 완벽한 순수 함수형 프로그래밍을 구현하기 어렵지만 부작용을 일으키지 않는 함수나 메서드를 이용하여 함수형 프로그래밍을 구현할 수 있습니다. 함수나 메서드에서는 지역 변수만을 변경하고, 참조하는 객체는 불변 객체여야 합니다. 함수형이라고 말할 수 있는 함수나 메서드는 어떤 예외도 일으키지 않아야 하며, 입력값에 따라 정확하게 하나의 결과를 반환해야 합니다. Optional <T>를 <T> 사용하여 예외 없이 함수를 표현할 수 있습니다. 라이브러리 함수를 사용할 때는 부작용을 감출 수 있는 상황에서만 사용해야 하며, 주석이나 마커 어노테이션을 사용하여 부작용이 있는 메서드를 명시해 주는 것이 좋습니다. 디버깅 정보를 출력하는 것은 함수형의 규칙에 위배되지만, 로그 출력을 제외하고는 함수형 프로그래밍의 장점을 유지할 수 있습니다.
18.2.2 참조 투명성
참조적 투명성은 같은 인수로 함수를 호출할 때 항상 같은 결과를 반환하는 것을 의미합니다. 따라서 부작용을 감춰야 한다는 제약은 참조적 투명성 개념으로 귀결됩니다. 함수형 코드에서는 final 변수를 변경하지 않는 연산과 같이 항상 같은 결과를 생성하는 연산을 함수형으로 구현할 수 있습니다. List를 반환하는 메서드는 두 번 호출하면 결과가 같아도 서로 다른 메모리 공간에 생성된 리스트를 참조할 수 있으므로 참조적으로 투명한 메서드가 아닐 수 있습니다. 하지만 함수형 코드에서는 일반적으로 이런 함수를 참조적으로 투명한 것으로 간주합니다.
18.2.3 객체지향 프로그래밍과 함수형 프로그래밍
이 절에서는 함수형 프로그래밍과 익스트림 객체지향 프로그래밍을 비교하고, 자바 8에서는 이 두 가지 프로그래밍 형식을 혼합하는 경향이 있다는 것을 설명합니다. 또한 하드웨어 변경과 데이터 조작에 대한 프로그래머의 기대치로 인해 자바 소프트웨어 엔지니어의 프로그래밍 형식이 좀 더 함수형으로 다가갈 것이라고 예측하고 있으며, 이 책은 이러한 변화에 대응하는 방법을 설명합니다. 프로그래밍 형식을 스펙트럼으로 표현하여, 객체지향 프로그래밍과 함수형 프로그래밍이 서로 어디에 위치하는지 설명하고, 자바 프로그래머가 이 두 가지 프로그래밍 형식을 혼합하여 사용할 수 있음을 보여주며, 이를 통해 모듈성이 좋고 멀티코어 프로세서의 적합한 프로그램을 구현하는 데 도움을 주는 함수형 프로그래밍의 기능을 소개하는 것이 이 절과 19장의 목표라고 요약할 수 있습니다.
18.3 재귀와 반복
순수 함수형 프로그래밍 언어에서는 반복문을 사용하지 않는 이유는 변화를 피하기 위해서입니다. 일반적으로 반복문은 코드에 변화를 자연스럽게 스며들게 하기 때문입니다. 함수형 프로그래밍에서는 지역 변수는 자유롭게 갱신할 수 있지만, 루프의 조건문을 갱신해야 할 경우 안전하게 사용할 수 없습니다. 다음은 자바의 Iterator로 for (Apple a: apples) { }라는 for-each 루프를 표현한 코드입니다.
Iterator<Apple> it = apples.iterator();
while (it.hasNext()) {
Apple apple = it.next();
// ...
}
for-each 루프를 사용하는 검색 알고리즘에서는 루프 내부에서 apple 변수의 값을 변경하는 것이 안전하지 않습니다. 이는 변화를 확인할 수 없는 상황에서는 예기치 않은 결과를 초래할 수 있기 때문입니다. 따라서 이러한 상황에서는 명시적인 while 루프를 사용하는 것이 더 안전합니다. 하지만 다음 코드처럼 for-each 루프를 사용하는 검색 알고리즘은 문제가 될 수 있습니다.
public void searchForGold(List<String> l, Stats stats) {
for (String s : l) {
if ("gold".equals(s)) {
stats.incrementFor("gold");
}
}
}
for-each 루프를 사용하는 검색 알고리즘은 상충하는 부작용을 발생시킬 수 있기 때문에 하스켈 같은 순수 함수형 프로그래밍 언어에서는 부작용 연산을 제거합니다. 이를 해결하기 위해서는 재귀를 이용하여 구현할 수 있으며, 이를 통해 변화가 일어나지 않도록 할 수 있습니다. 다음은 팩토리얼 함수로, 반복과 재귀 방식으로 해결할 수 있는 고전적 문제입니다. 여기서 입력은 1보다 크다고 가정합니다.
// 반복 방식의 팩토리얼
static int factorialIterative(int n) {
int r = 1;
for (int i = 1; i <= n; i++) {
r *= i;
}
return r;
}
// 재귀 방식의 팩토리얼
static long factorialRecursive(long n) {
return n == 1 ? 1 : n * factorialRecursive(n - 1);
}
첫 번째 예제는 일반적인 루프를 사용한 코드로 매 반복마다 변수 r과 i가 갱신되며, 두 번째 예제는 재귀 방식의 코드로 문제를 해결합니다. 하지만 이 책을 학습한 독자라면 자바 8 스트림을 이용해 더 간단하게 팩토리얼을 정의할 수 있다는 사실을 알고 있을 것입니다.
static long factorialStreams(long n) {
return LongStream.rangeClosed(1, n)
.reduce(1, (long a, long b) -> a * b);
}
함수형 프로그래밍에서는 무조건 재귀가 좋다는 주장에 주의해야 한다는 것이 중요하다. 이는 재귀 함수를 호출할 때마다 메모리 사용량이 증가하므로, 큰 입력값을 사용하면 StackOverflowError가 발생할 수 있기 때문이다. 하지만 함수형 언어에서는 꼬리 호출 최적화를 제공하여 이 문제를 해결할 수 있다는 것이다.
static long factorialTailRecursive(long n) {
return factorialHelper(1, n);
}
static long factorialHelper(long acc, long n) {
return n == 1 ? acc : factorialHelper(acc * n, n - 1);
}
일반 재귀와 달리, 꼬리 재귀에서는 컴파일러가 하나의 스택 프레임을 재활용할 가능성이 생긴다. 꼬리 재귀에서는 중간 결과(팩토리얼의 부분결과)를 함수의 인수로 직접 전달한다. 이와 달리 일반 재귀는 중간 결과를 각각의 스택 프레임으로 저장해야 한다.
아래 두 그림은 팩토리얼의 재귀 정의와 꼬리 재귀 정의의 차이를 보여줍니다.


자바에서는 최적화된 꼬리 재귀를 지원하지 않지만, 순수 함수형을 유지하면서도 유용성과 효율성을 얻을 수 있습니다. 또한 자바 8에서는 반복을 스트림으로 대체하여 변화를 피하고, 재귀를 이용하면 더 간결하고 부작용이 없는 알고리즘을 만들 수 있습니다. 이러한 함수형 프로그래밍 기법은 기존의 자바 버전에서도 적용할 수 있습니다. 다음 장에서는 자바 8에 도입된 일급 함수를 이용한 가능성을 살펴봅니다.
'📚 서적 및 학습자료 > 모던 자바 인 액션[2회독]' 카테고리의 다른 글
[모던 자바 인 액션] 20장 OOP와 FP의 조화 : 자바와 스칼라 비교 (0) | 2023.04.02 |
---|---|
[모던 자바 인 액션] 19장 함수형 프로그래밍 기법 (0) | 2023.04.02 |
[모던 자바 인 액션] 17장 리액티브 프로그래밍 (0) | 2023.04.02 |
[모던 자바 인 액션] 16장 CompletableFuture : 안정적 비동기 프로그래밍 (0) | 2023.04.02 |
[모던 자바 인 액션] 15장 CompletableFuture와 리액티브 프로그래밍 컨셉의 기초 (0) | 2023.03.29 |