BackEnd

함수형 프로그래밍의 장단점

Raconer 2025. 1. 19. 19:22
728x90

장점

1. 병렬 처리 및 동시성에 유리

함수형 프로그래밍에서는 불변성과 순수 함수 덕분에 병렬 처리가 안전하며 동시성 문제를 줄일 수 있습니다.

Java 예제

import java.util.Arrays;
import java.util.List;

public class FunctionalExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);

        // Stream API를 사용하여 병렬 처리
        int sum = numbers.parallelStream()
                         .map(x -> x * 2) // 각 요소를 2배로 만듦
                         .reduce(0, Integer::sum); // 모든 요소를 합산

        System.out.println("Sum: " + sum); // 출력: 72
    }
}

Kotlin 예제

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8)

    // 병렬 처리를 위해 map과 reduce를 활용
    val sum = numbers
        .map { it * 2 } // 각 요소를 2배로
        .reduce { acc, value -> acc + value } // 합산

    println("Sum: $sum") // 출력: 72
}

장점:
병렬 처리 시 불변성 덕분에 데이터 충돌 문제가 발생하지 않으며, 동시성을 안전하게 처리할 수 있습니다.


2. 지연 평가(Lazy Evaluation)

필요할 때만 계산을 수행하여 불필요한 연산을 줄입니다.

Java 예제

import java.util.stream.Stream;

public class LazyEvaluationExample {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5)
                                       .filter(x -> {
                                           System.out.println("Filtering: " + x);
                                           return x > 2;
                                       })
                                       .map(x -> {
                                           System.out.println("Mapping: " + x);
                                           return x * 2;
                                       });

        // 최종 연산 수행 (지연 평가로 인해 여기서 계산 시작)
        stream.forEach(System.out::println);
    }
}

출력:

Filtering: 1
Filtering: 2
Filtering: 3
Mapping: 3
6
Filtering: 4
Mapping: 4
8
Filtering: 5
Mapping: 5
10

Kotlin 예제

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)

    val result = numbers
        .asSequence() // 지연 평가 시작
        .filter {
            println("Filtering: $it")
            it > 2
        }
        .map {
            println("Mapping: $it")
            it * 2
        }

    result.forEach { println(it) }
}

출력:

Filtering: 1
Filtering: 2
Filtering: 3
Mapping: 3
6
Filtering: 4
Mapping: 4
8
Filtering: 5
Mapping: 5
10

장점:
filter와 map은 필요할 때만 수행되어, 연산량을 줄이고 성능을 최적화합니다.


3. 캐싱 및 메모이제이션(Memoization)에 적합

순수 함수의 특성상, 동일 입력에 대해 동일 출력을 보장하므로, 결과를 캐싱하여 성능을 개선할 수 있습니다.

Kotlin 예제

fun fibonacci(): (Int) -> Long {
    val cache = mutableMapOf<Int, Long>()
    return { n ->
        cache.getOrPut(n) {
            if (n <= 1) 1L else fibonacci()(n - 1) + fibonacci()(n - 2)
        }
    }
}

fun main() {
    val fib = fibonacci()
    println(fib(10)) // 출력: 89
    println(fib(15)) // 출력: 987
}

단점

1. 높은 메모리 소비

불변 데이터 구조는 데이터를 복사하여 새로 생성하므로, 메모리 사용량이 증가할 수 있습니다.

Kotlin 예제

fun main() {
    val numbers = listOf(1, 2, 3)
    val newNumbers = numbers + 4 // 기존 리스트 복사 후 새로운 리스트 생성

    println("Original: $numbers") // [1, 2, 3]
    println("New: $newNumbers")   // [1, 2, 3, 4]
}
  • 단점: 기존 데이터를 유지하면서 새 데이터를 생성하기 때문에, 메모리 사용량이 증가합니다.

2. 재귀 호출로 인한 스택 오버플로우

함수형 프로그래밍에서 재귀를 자주 사용하지만, 꼬리 재귀 최적화(Tail Call Optimization)가 없는 환경에서는 스택 오버플로우 문제가 발생할 수 있습니다.

Java 예제

public class RecursiveExample {
    public static void main(String[] args) {
        System.out.println(factorial(10000)); // 스택 오버플로우 발생
    }

    public static int factorial(int n) {
        if (n <= 1) return 1;
        return n * factorial(n - 1);
    }
}

Kotlin 예제 (꼬리 재귀 최적화)

Kotlin은 tailrec 키워드를 사용하여 꼬리 재귀 최적화를 지원합니다.

tailrec fun factorial(n: Int, acc: Long = 1): Long {
    return if (n <= 1) acc else factorial(n - 1, acc * n)
}

fun main() {
    println(factorial(10000)) // 꼬리 재귀 최적화로 스택 오버플로우 없음
}

단점: 꼬리 재귀 최적화가 없는 경우, 반복문이 재귀보다 성능이 더 좋습니다.


3. 불필요한 중간 데이터 생성

함수형 체이닝(map, filter)에서 각 단계가 중간 데이터를 생성하면 성능이 저하될 수 있습니다.

Java 예제

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class IntermediateData {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        // 각 단계에서 중간 데이터가 생성됨
        List<Integer> result = numbers.stream()
                                      .map(x -> x * 2)
                                      .filter(x -> x > 5)
                                      .collect(Collectors.toList());

        System.out.println(result); // [6, 8, 10]
    }
}

단점:
중간 데이터(map -> filter -> collect)가 계속 생성되어, 메모리와 CPU 사용량이 증가합니다.
해결: Lazy Evaluation을 지원하는 스트림 활용.


결론

  • 장점: 병렬 처리, 지연 평가, 캐싱 등으로 성능 최적화 가능.
  • 단점: 메모리 사용 증가, 재귀 호출 문제, 중간 데이터 생성으로 인한 성능 저하 가능.
    언어와 상황에 맞게 함수형 접근 방식을 최적화하여 사용하는 것이 중요합니다.
728x90