
함수형 프로그래밍은 프로그래밍 패러다임의 한 종류입니다.
명령형, 객체지향 패러다임과는 다르게 상태를 바꾸지 않고 함수 합성을 통해 문제를 푼다는 점이 핵심입니다.
상태를 바꾸지 않는다는 의미가 와닿지 않으니, 다른 프로그래밍 패러다임의 예시와 함께 함수형 프로그래밍의 특징을 확인해봅시다.
대표적인 언어로는 C, Python이 있습니다.
nums = [1, 2, 3, 4, 5]
evens = []
for n in nums:
if n % 2 == 0:
evens.append(n * n)
위 파이썬 코드는 배열의 상태를 직접 변경하며, 세부적인 처리 과정을 개발자가 모두 제어합니다.
절차적 프로그래밍은 명령형 프로그래밍의 하위 개념입니다.
큰 문제를 절차 단위로 나눠서 해결한다는 특징이 있습니다.
대표 언어로는 C, Pascal이 있습니다.
int square_even(int n) {
if (n % 2 == 0) return n * n;
return -1;
}
절차를 만들어 코드를 모듈화했지만, 여전히 상태 변경 중심입니다.
객체지향 프로그래밍은 캡슐화, 상속, 다형성을 통해 복잡성을 관리합니다.
대표 언어로는 Java, C++, Python이 있습니다.
class Number {
int value;
Number(int v) { this.value = v; }
boolean isEven() { return value % 2 == 0; }
int square() { return value * value; }
}
데이터를 객체로 감싸 상태와 행위를 함께 관리하지만, 여전히 상태 변화를 기반으로 동작합니다.
상태를 변경하지 않고 순수 함수와 함수 합성으로 문제를 해결합니다.
데이터는 불변하고 선언적 스타일로 무엇을 할지만 표현합니다.
대표 언어로는 Haskell, Scala, Clojure이 있으며 JavaScript, Python, Java Stream에서 부분적으로 지원합니다.
List<Integer> evens = nums.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.toList();
상태를 변경하지 않고 짝수를 골라 제곱한다 라는 의도만 표현합니다.
상태와 부작용이 줄어 코드가 예측 가능해지고, 데이터 흐름이 명확해져 병렬 처리나 테스트에도 유리하다는 특징이 있습니다.
프로그래밍 패러다임은 서로 경쟁하는 개념이 아니라, 각각의 장단점을 가지고 있고 상황에 따라 보완적으로 쓰일 수 있는 도구 에 가깝습니다.
함수형 프로그래밍 역시 마찬가지로, 명령형·절차적·객체지향 패러다임과 함께 실무에서 공존하고 있습니다.
함수형 프로그래밍은 상태 변화와 부작용을 최소화하고, 함수 중심으로 문제를 해결하는 패러다임입니다.
함수형 프로그래밍의 주요 특징을 알아봅시다.
예제 코드는 익숙한 Java로 작성되었습니다
예제 코드
int square(int n) {
return n * n; // 입력 n에 대해 항상 같은 결과
}
순수 함수의 특징
예제 코드
List<Integer> nums = List.of(1, 2, 3); // 불변 리스트
불변성의 특징
여러 스레드가 같은 데이터를 동시에 읽고 쓸 때, 동기화 문제가 발생할 수 있습니다.
불변 객체는 상태가 변하지 않기 때문에, 락이나 동기화 처리 없이 여러 스레드가 동시에 접근 가능합니다.
따라서, 상태를 직접 바꾸지 않는다는 특징은 병렬 처리에서 Race Condition을 자연스럽게 방지할 수 있습니다.
불변성을 유지하면 이전 상태를 유지할 수 있어 디버깅과 테스트가 쉬워지고, 데이터 변환이 안전하다는 장점이 있습니다.
하지만 매 번 새로운 값을 생성하므로 메모리 사용량이 증가한다는 단점이 있습니다.
그럼에도 불구하고 함수형 프로그래밍이 사용되는 이유는, 현대 JVM과 GC는 이런 패턴에 최적화되어 있어, 성능 문제가 크지 않는 경우가 많습니다.
함수형 프로그래밍에서 함수는 1급 객체로 취급됩니다.
그리고 함수형 프로그래밍에서 사용되는 함수는 모두 순수 함수만을 사용합니다.
즉, 함수를 변수에 담거나, 인자로 전달하거나, 반환값으로 사용할 수 있는 값처럼 다룰 수 있습니다.
1급 객체란 다음과 같은 특징을 가진 객체를 의미합니다.
함수형 프로그래밍에서 위와 같은 특징 때문에 고차 함수라는 특징이 가능해집니다.
고차 함수는 함수를 인자로 받거나, 함수를 반환하는 함수를 말합니다.
예제 코드
// 함수 자체를 변수에 담기 (1급 객체)
Function<Integer, Integer> square = x -> x * x;
// 함수에 전달 (고차 함수)
List<Integer> squared = nums.stream().map(square).toList();
코드를 부연 설명하자면 square 는 함수를 값처럼 다루는 1급 객체입니다.
map 은 이 1급 객체를 받아 각 리스트 요소에 적용하는 고차 함수입니다.
고차 함수의 특징
함수 합성은 작은 단위의 함수를 결합해 새로운 함수를 만드는 것을 의미합니다.
FP에서는 복잡한 로직도 여러 순수 함수를 조합해 쉽게 구현할 수 있습니다.
예제 코드
Function<Integer, Integer> add2 = x -> x + 2;
Function<Integer, Integer> squareThenAdd2 = add2.compose(square);
함수 합성의 특징
선언적 스타일은 무엇을 할지(What)만 정의하고, 어떻게 할지(How)는 숨기는 방식입니다.
FP에서는 데이터 처리와 로직을 선언적으로 표현할 수 있어 코드가 간결하고 읽기 쉽습니다.
예제 코드
List<Integer> evens = nums.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.toList();
선언적 스타일의 특징
지연 평가는 필요할 때까지 계산을 미루는 방식입니다.
이를 통해 불필요한 연산을 줄이고, 무한 시퀀스 처리도 가능합니다.
예제 코드
-- Haskell 예시: 무한 리스트
take 5 [1..] -- [1,2,3,4,5]
지연 평가의 특징
함수형 프로그래밍은 코드의 가독성과 재사용성을 높이고, 부작용을 줄이며 병렬 처리에도 안전한 장점이 있습니다.
하지만 학습 곡선이 비교적 가파르고, 기존 명령형·객체지향 중심 사고에서 벗어나야 하기 때문에 처음에는 낯설 수 있습니다.
제가 함수형 프로그래밍을 배우려는 가장 큰 이유는, 기존의 명령형이나 객체지향 방식으로 작성한 코드가 복잡하고 가독성이 떨어지는 경우를 자주 느꼈기 때문입니다.
그렇기 때문에 데이터 흐름이 보다 명확하고, 재사용이 용이한 코드를 작성할 수 있는 방법을 찾고자 함수형 프로그래밍을 공부하고 있으며, 코딩테스트나 다른 개발 활동을 하면서 점차 적용해보려고 합니다.
다음 시간에는 Java에서 함수형 프로그래밍을 활용하는 방법과 Stream API, 람다, 불변 컬렉션 등을 포스팅하는 주제로 돌아오겠습니다!