콘텐츠로 이동

함수형 프로그래밍 심화

함수형 프로그래밍 (Functional Programming)

왜 쓰는지

객체지향 프로그래밍(OOP)은 상태(state)를 객체에 담아 변경하며 동작한다. 이 방식은 시스템이 커질수록 어디서 상태가 바뀌는지 추적하기 어려워지고, 멀티스레드 환경에서 버그가 생기기 쉽다.

==함수형 프로그래밍==은 불변 데이터순수 함수를 중심으로, 상태 변경 없이 값을 변환하는 방식으로 이 문제를 해결한다.

핵심 개념

순수 함수 (Pure Function)

같은 입력이면 항상 같은 출력. 외부 상태를 읽거나 변경하지 않는다.

// 순수 함수
int add(int a, int b) {
    return a + b; // 외부 의존 없음, 사이드 이펙트 없음
}

// 순수 함수가 아닌 예
int count = 0;
void increment() {
    count++; // 외부 상태 변경 → 부작용(side effect)
}

불변성 (Immutability)

데이터를 변경하지 않고, 변환된 새 값을 반환한다.

// 가변 방식
List<String> list = new ArrayList<>();
list.add("a"); // 원본 변경

// 불변 방식 (스트림)
List<String> newList = original.stream()
    .filter(s -> !s.isEmpty())
    .toList(); // 원본 그대로, 새 리스트 반환

1급 함수 (First-class Function)

함수를 값처럼 다룰 수 있다. 변수에 저장하고, 파라미터로 전달하고, 반환값으로 돌려줄 수 있다.

// 함수를 변수에 저장
Function<Integer, Integer> square = x -> x * x;

// 함수를 파라미터로 전달
List<Integer> result = numbers.stream()
    .map(square)   // 함수 전달
    .toList();

// 함수를 반환하는 함수 (고차 함수)
Function<Integer, Function<Integer, Integer>> adder = a -> b -> a + b;
Function<Integer, Integer> add5 = adder.apply(5);
add5.apply(3); // 8

고차 함수 (Higher-order Function)

함수를 인자로 받거나 함수를 반환하는 함수.

// map, filter, reduce 모두 고차 함수
list.stream()
    .filter(x -> x > 0)      // 조건 함수를 받음
    .map(x -> x * 2)          // 변환 함수를 받음
    .reduce(0, Integer::sum); // 누산 함수를 받음

Java에서 함수형 프로그래밍

Java 8부터 람다, Stream API, 함수형 인터페이스로 함수형 스타일을 지원한다.

주요 함수형 인터페이스

Function<T, R>    // T 받아 R 반환: map에 사용
Predicate<T>      // T 받아 boolean: filter에 사용
Consumer<T>       // T 받아 반환 없음: forEach에 사용
Supplier<T>       // 인자 없이 T 반환: 지연 생성에 사용
UnaryOperator<T>  // T 받아 T 반환: 같은 타입 변환
BinaryOperator<T> // T 두 개 받아 T 반환: reduce에 사용

함수 합성

Function<Integer, Integer> times2 = x -> x * 2;
Function<Integer, Integer> plus3  = x -> x + 3;

// andThen: times2 실행 후 plus3
Function<Integer, Integer> times2ThenPlus3 = times2.andThen(plus3);
times2ThenPlus3.apply(5); // (5 * 2) + 3 = 13

// compose: plus3 먼저, 그 다음 times2
Function<Integer, Integer> plus3ThenTimes2 = times2.compose(plus3);
plus3ThenTimes2.apply(5); // (5 + 3) * 2 = 16

Predicate 합성

Predicate<Integer> isPositive = x -> x > 0;
Predicate<Integer> isEven     = x -> x % 2 == 0;

Predicate<Integer> isPositiveAndEven = isPositive.and(isEven);
Predicate<Integer> isPositiveOrEven  = isPositive.or(isEven);
Predicate<Integer> isNotPositive     = isPositive.negate();

list.stream()
    .filter(isPositiveAndEven)
    .toList();

OOP vs 함수형 비교

항목 OOP 함수형
중심 개념 객체와 상태 함수와 값 변환
상태 가변 (mutable) 불변 (immutable)
부작용 허용 최소화
코드 재사용 상속, 다형성 함수 합성
멀티스레드 동기화 필요 상태 없으므로 안전

장점

  • 사이드 이펙트가 없어 버그 추적이 쉬움
  • 순수 함수는 테스트가 간단 (입력→출력만 검증)
  • 함수 합성으로 복잡한 로직을 작은 단위로 분리
  • 불변 데이터로 멀티스레드 안전성 확보

단점

  • 익숙하지 않으면 가독성이 오히려 낮아짐
  • 불변 데이터는 매번 새 객체 생성 → 메모리 사용량 증가 가능
  • 완전한 불변성은 Java에서 강제되지 않아 규율 필요
  • 재귀 중심 코드는 스택 오버플로우 위험 (Java는 꼬리재귀 최적화 없음)

주의할 점

람다 안에서 외부 변수 변경 불가

int count = 0;
list.forEach(s -> count++); // 컴파일 오류 - effectively final 위반

// 대신 스트림으로 집계
long count = list.stream().count();

사이드 이펙트를 forEach에 몰아넣기

순수 함수 파이프라인 끝에만 부작용을 허용하는 패턴을 권장한다.

// 좋은 패턴: 변환은 map/filter, 출력은 마지막 forEach
list.stream()
    .filter(s -> s.length() > 3)
    .map(String::toUpperCase)
    .forEach(System.out::println); // 사이드 이펙트는 맨 끝에만