함수형 프로그래밍

david1-p·2025년 11월 5일

CS 지식 창고

목록 보기
17/26
post-thumbnail

함수형 프로그래밍 핵심: 순수 함수와 함수 합성이란?

최근 백엔드 개발에서 함수형 프로그래밍 패러다임이 주목받고 있습니다. Java에서도 Stream API나 Lambda를 통해 함수형 프로그래밍을 적극적으로 활용하고 있는데요. 왜 함수형 프로그래밍이 중요하며, 핵심 개념은 무엇인지 정리해 보았습니다.

함수형 프로그래밍(FP)이란?

함수형 프로그래밍(Functional Programming)은 객체지향 프로그래밍(OOP)과 마찬가지로 하나의 프로그래밍 패러다임입니다.

  • 객체지향 프로그래밍(OOP)은 '움직이는 부분(상태)'을 캡슐화하여 코드의 이해를 돕습니다.

  • 함수형 프로그래밍(FP)은 '움직이는 부분(상태)'을 최소화하여 코드의 이해를 돕습니다.

이 둘은 상충하는 개념이 아니며, 함께 조화되어 사용될 수 있습니다.

함수형 프로그래밍의 핵심은 함수를 합성하여 복잡한 프로그램을 쉽게 만들고, 부수 효과(Side Effect)를 공통적인 방법으로 추상화하는 것입니다.


부수 효과(Side Effect)란?

부수 효과는 함수가 값을 반환하는 것 이외에 부수적으로 발생하는 모든 일을 의미합니다.

  • (외부) 변수를 수정
  • I/O 작업 (e.g., 파일 쓰기, DB 접근, API 호출)
  • System.out.println() 호출

사람이 한 번에 인지하고 처리할 수 있는 작업은 한정되어 있습니다. 부수 효과가 많은 코드는 함수가 어떤 일을 하는지, 호출 순서에 따라 어떤 결과가 나올지 예측하기 매우 어렵게 만듭니다.

함수형 프로그래밍은 이러한 부수 효과를 최대한 분리하고 추상화하여 코드를 이해하기 쉽고 예측 가능하게 만듭니다.


함수 합성이란?

함수 합성(Function Composition)은 특정 함수의 공역(반환 타입)이 다른 함수의 정의역(입력 타입)과 일치하는 경우, 두 함수를 이어서 새로운 함수를 만드는 연산을 말합니다.

프로그래밍에서 공역과 정의역은 타입에 해당됩니다.

쉽게 말해, A 함수가 int를 반환하고, B 함수가 int를 인자로 받는다면, B(A())와 같은 형태로 호출하는 것을 함수 합성이라고 합니다.


부수 효과(와 경직된 로직)의 문제

함수형 프로그래밍은 함수를 합성하여 복잡한 프로그램을 쉽게 만듭니다. 하지만 부수 효과가 존재하거나 로직이 경직된 함수는 합성하기가 까다롭습니다.

아래 sum 함수를 살펴봅시다.

이 함수는 재사용과 합성이 어렵습니다.

  1. 만약 1부터 1,000까지 더하는 함수가 필요하다면?
  2. 만약 1부터 100까지 곱하는 함수가 필요하다면?

매번 새로운 함수를 만들어야 합니다. '무엇을'(덧셈)과 '어떻게'(1부터 100까지 루프)가 너무 강하게 결합되어 있기 때문입니다.


해결책: 순수 함수와 고차 함수

함수형 프로그래밍은 이 문제를 순수 함수(Pure Function)고차 함수(Higher-Order Function)로 해결합니다.

1. 순수 함수 (Pure Function)

순수 함수는 같은 입력이 들어오면, 항상 같은 값을 반환하는 함수를 의미합니다. 그리고 가장 중요한 것은, 부수 효과를 일으키지 않는다는 점입니다.

  • 입력 값에만 의존합니다.
  • 외부의 상태를 변경하지 않습니다. (e.g., 전역 변수 수정, I/O, DB 접근 X)
  • 결과를 예측하기 쉽고 테스트하기 용이합니다.

2. 고차 함수 (Higher-Order Function)

고차 함수는 함수를 인자(Argument)로 받거나, 함수를 결과로 반환하는 함수를 말합니다.

함수형 프로그래밍에서 함수 합성은 바로 이 순수 함수들로 이뤄집니다. '어떻게'를 담당하는 고차 함수(e.g., loop, map, filter, reduce)와 '무엇을' 담당하는 순수 함수(e.g., lambda)를 분리하여 합성하는 것입니다.


Java 예제: loop로 합성을 구현하기

아래 코드는 '어떻게'에 해당하는 loop 함수(고차 함수)를 정의하고, '무엇을'에 해당하는 (a, b) -> a + b (덧셈) 또는 (a, b) -> a * b (곱셈)을 인자로 전달하여 sum과 factorial을 구현한 예제입니다.

loop 함수는 재귀를 사용하며, sum이라는 지역 변수(상태)를 변경하는 대신 새로운 값을 다음 loop 함수에 인자로 넘깁니다.

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.LinkedList;
import java.util.Queue;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

class FunctionCompositionTest {
    @Test
    @DisplayName("함수 합성")
    void fp() {
        System.out.println("1부터 100까지의 합: " + sum()); // 5050
        System.out.println("10 팩토리얼: " + factorial(10)); // 3628800
    }

    // 1부터 100까지의 합
    private int sum() {
        // '무엇을' (덧셈)과 '초기값' (0)을 전달
        return loop((a, b) -> a + b, 0, range(1, 100));
    }

    // 팩토리얼
    private int factorial(int n) {
        // '무엇을' (곱셈)과 '초기값' (1)을 전달
        return loop((a, b) -> a * b, 1, range(1, n));
    }

    /**
     * '어떻게'를 담당하는 고차 함수 (reduce/fold와 유사)
     * @param fn : '무엇을' 할지 정의한 함수 (e.g., 덧셈, 곱셈)
     * @param initialValue : 초기값
     * @param queue : 데이터
     * @return 연산 결과
     */
    private int loop(BiFunction<Integer, Integer, Integer> fn, int initialValue, Queue<Integer> queue) {
        if (queue.isEmpty()) {
            return initialValue;
        }
        
        // 재귀를 사용해 부수 효과 없이(지역 변수 변경 없이) 연산을 수행
        // fn.apply(현재값, 큐에서 꺼낸 값)
        return loop(fn, fn.apply(initialValue, queue.poll()), queue);
    }

    // 데이터를 생성하는 헬퍼 함수
    private Queue<Integer> range(Integer start, Integer to) {
        return IntStream.rangeClosed(start, to)
                .boxed()
                .collect(Collectors.toCollection(LinkedList::new));
    }
}

요약

  • 함수형 프로그래밍은 부수 효과를 최소화하여 코드를 이해하기 쉽게 만듭니다.
  • 이를 위해 순수 함수를 사용하며,
  • 고차 함수를 통해 '어떻게'(로직)와 '무엇을'(연산) 분리하고, 이를 합성하여 복잡한 로직을 구축합니다.

이러한 접근 방식은 코드의 재사용성을 높이고, 예측 가능하게 만들며, 테스트를 용이하게 합니다.

profile
DONE IS BETTER THAN PERFECT.

0개의 댓글