함수형 프로그래밍이란? (feat. Java Stream API)

2026. 1. 7. 18:05·자바

0. 들어가며

자바로 개발을 하다 보면 컬렉션을 다루는 코드가 금세 길어집니다.
처음에는 단순한 for문이었는데 조건이 하나씩 붙고 상태를 저장하다 보면
어느 순간 이 코드가 “무엇을 위한 코드인지” 스스로도 설명하기 어려워집니다.

저 역시 그랬습니다.
동작은 이해되지만, 의도는 잘 보이지 않는 코드.
그런 코드를 계속 작성하고 있다는 느낌이 들었습니다.

그러다 Stream API를 접하게 되었고, 문법을 넘어서 전혀 다른 사고방식이 필요하다는 걸 알게 되었습니다.
그 사고방식이 바로 함수형 프로그래밍이었습니다.

이 글은 Stream API를 이해하기 위해 제가 함수형 프로그래밍을 정리해 나갔던 과정을 담고 있습니다.

 


1. 함수형 언어(프로그래밍)란?

함수형 프로그래밍은 컴퓨터 프로그래밍의 여러 패러다임 중 하나로 프로그램을 상태를 변경하는 절차의 나열이 아니라
수학적 함수의 평가 과정으로 바라보는 방식입니다.

즉, “어떻게 순서대로 실행할 것인가”보다는 “어떤 값을 입력하면 어떤 결과가 나오는가”에 집중합니다.

이러한 사고 방식은 프로그램에서 상태(state)와 가변 데이터(mutable data)를 최소화하고  그 결과 코드의 실행 흐름을  예측 가능하고 안정적으로 만들어 줍니다.

Java Stream API가 바로 이 관점을 자바 코드에 녹여낸 대표적인 예입니다.


2. 함수형 프로그래밍의 핵심 개념

1. 순수 함수 (Pure Function)

함수형 프로그래밍의 가장 중요한 개념은 순수 함수입니다.
순수 함수는 다음 두 가지 특징을 가집니다.

  • 동일한 입력에 대해 항상 동일한 출력을 반환한다
  • 외부 상태를 변경하지 않는다
const sum = (a, b) => a + b;

위 함수는 언제 호출하더라도 같은 입력에 대해 같은 결과를 반환하며 외부 변수나 상태에 전혀 영향을 주지 않습니다.

이러한 특성 덕분에 순수 함수는

  • 프로그램의 동작을 이해하기 쉽고
  • 테스트가 간단하며
  • 버그가 발생할 가능성이 낮습니다.

테스트가 간단한  순수 함수 예시

const sum = (a, b) => a + b;

이 함수에 대한 테스트는 다음처럼 작성할 수 있습니다.

test('sum 함수는 두 숫자를 더한 값을 반환한다', () => {
  expect(sum(1, 2)).toBe(3);
  expect(sum(10, 20)).toBe(30);
  expect(sum(-5, 5)).toBe(0);
});

이 테스트에서는

  • 어떤 변수도 미리 세팅할 필요가 없고
  • DB, 네트워크, 시간, 환경 설정도 필요 없으며
  • 호출 순서에 따라 결과가 바뀌지도 않습니다

그저 입력값을 넣고 결과만 검증하면 됩니다.


 

2. 불변성 (Immutability)

함수형 프로그래밍에서는 데이터는 한 번 생성되면 변경하지 않는다는 원칙을 중요하게 여깁니다.
기존 데이터를 직접 수정하는 대신, 새로운 데이터를 만들어 반환합니다.

List<Integer> result = numbers.stream()
                              .map(n -> n * 2)
                              .toList();

Stream API 역시 기존 컬렉션을 변경하지 않고, 항상 새로운 결과 컬렉션을 만들어 반환합니다.

이 방식은 처음에는 비효율적으로 보일 수 있지만

  • 예기치 않은 사이드 이펙트를 줄이고
  • 여러 로직에서 같은 데이터를 사용해도 안전하며
  • 멀티스레드 환경에서도 안정적인 코드를 작성할 수 있게 해줍니다.

불변하지 않을 때 문제가 되는  예제 상황

한 주문 시스템에서 주문 금액 목록을 여러 로직에서 함께 사용한다고 가정해봅시다.

 

  • 주문 총액 계산
  • 할인 적용
  • 로그 기록

불변하지 않은 코드

 

public class OrderService {

    public int calculateTotal(List<Integer> prices) {
        // 할인 적용 (기존 리스트 직접 수정)
        for (int i = 0; i < prices.size(); i++) {
            prices.set(i, prices.get(i) - 1000);
        }

        return prices.stream()
                     .mapToInt(Integer::intValue)
                     .sum();
    }
}


그리고 다른 곳에서는 같은 리스트를 이렇게 사용합니다.

List<Integer> prices = new ArrayList<>(List.of(10000, 20000, 30000));

int total = orderService.calculateTotal(prices);

// 로그 기록
System.out.println(prices);

발생하는 문제

로그를 찍어보면 이런 결과가 나옵니다.

[9000, 19000, 29000]

할인은 총액 계산용이었는데, 원본데이터까지 바뀌어 버렸고 이후 로직들은 이미 변형된 데이터를 기준으로 동작합니다.

이러한 변경은 메서드 이름이나 시그니처만 보고는 전혀 예측할 수 없습니다.


3. 함수의 일급 객체 (First-Class Function)

함수형 프로그래밍에서는 함수 자체가 값처럼 취급됩니다.

즉  함수는

  • 변수에 할당될 수 있고
  • 다른 함수의 인자로 전달될 수 있으며
  • 함수의 반환값이 될 수도 있습니다.
numbers.stream()
       .filter(n -> n > 10)
       .map(n -> n * 2);

여기서 n -> n > 10, n -> n * 2는 모두 값처럼 전달되는 함수입니다.

이 개념 덕분에

  • 공통 로직을 함수로 분리하기 쉬워지고
  • 코드의 재사용성과 모듈성이 크게 향상됩니다.

3.   함수형 프로그래밍의 장점

이러한 특성들이 모여 함수형 프로그래밍은 다음과 같은 장점을 가집니다.

  • 코드의 가독성 향상
  • 오류 발생 가능성 감소
  • 테스트와 유지 보수 용이
  • 대규모 시스템에서의 안정성 증가

특히 Stream API를 사용하면 반복문이 사라지고, 로직의 의도가 그대로 드러나는 코드를 작성할 수 있습니다.

// 명령형
for (int n : numbers) {
    if (n > 10) {
        result.add(n * 2);
    }
}

// 함수형 (Stream)
numbers.stream()
       .filter(n -> n > 10)
       .map(n -> n * 2)
       .toList();

규모가 커질수록 상태 변경을 최소화하고 함수 단위로 사고하는 방식의 장점은 더욱 커집니다.


4.   함수형 프로그래밍의 단점

1. 처음에는 이해하기 어렵다

함수형 프로그래밍은 기존의 명령형·객체지향 방식과 사고방식 자체가 다릅니다.

  • 상태를 변경하지 않기
  • 반복문 대신 함수 조합 사용
  • 고차 함수, 불변성 개념 이해 필요

이 때문에 처음 접하면 다음과 같은 느낌을 받기 쉽습니다.

코드는 짧아졌는데, 오히려 이해가 안 된다

특히 map, filter, reduce가 중첩된 코드는 익숙하지 않은 개발자에게 가독성이 더 떨어질 수 있습니다.

map — “각 요소를 어떻게 바꿀 것인가”

numbers.stream()
       .map(n -> n * 2)
       .toList();

 

  • map은 컬렉션의 각 요소를 다른 값으로 변환합니다
  • 기존 반복문에서의 for + set 역할을 대신합니다

명령형 코드로 보면 아래와 같습니다.

List<Integer> result = new ArrayList<>();
for (int n : numbers) {
    result.add(n * 2);
}

 

동작은 단순하지만 람다 (n -> n * 2)와 체이닝 문법이 처음에는 낯설게 느껴집니다.

filter — “조건에 맞는 것만 남긴다”

numbers.stream()
       .filter(n -> n > 10)
       .toList();

 

  • filter는 조건을 만족하는 요소만 통과시킵니다
  • if 문이 함수로 표현된 형태입니다

 

for (int n : numbers) {
    if (n > 10) {
        result.add(n);
    }
}

조건이 코드 흐름이 아니라 함수 인자로 들어간다는 점이 익숙하지 않습니다.

reduce — “여러 값을 하나로 합친다”

int sum = numbers.stream()
                 .reduce(0, Integer::sum);

 

  • reduce는 누적 연산을 수행합니다
  • 초기값 + 누적 규칙을 함께 넘겨야 합니다
int sum = 0;
for (int n : numbers) {
    sum += n;
}

 

반복과 누적이라는 개념이 추상화된 함수 호출로 숨겨져 있어 처음엔 직관적이지 않습니다.


2. 디버깅이 직관적이지 않다

함수형 코드는 보통 체이닝(파이프라인) 형태로 작성됩니다.

numbers.stream()
       .filter(n -> n > 10)
       .map(n -> n * 2)
       .sorted()
       .toList();

이 방식은 의도는 잘 드러나지만

  • 중간 결과 확인이 어렵고
  • 브레이크포인트를 단계별로 찍기 힘들며
  • 어느 단계에서 문제가 생겼는지 추적이 까다롭습니다

명령형 코드처럼 한 줄씩 상태를 보며 디버깅하기가 쉽지 않습니다.


3. 모든 상황에 최적은 아니다

  • 함수형 프로그래밍은 데이터 변환에는 강하지만,
    • 단순 반복
    • 성능이 중요한 로직
    • 복잡한 상태 변화가 핵심인 코드
    에서는 오히려 명령형 코드가 더 명확하고 효율적일 수 있습니다.
    중요한 것은 Stream을 쓰느냐 마느냐가 아니라, 어디에 쓰느냐입니다.

 마치며

Java Stream API는 함수형 프로그래밍의 사고방식을 자바 개발자가 실무에서 활용할 수 있도록 도와주는 도구입니다.

하지만 Stream 역시 만능은 아니며 모든 코드를 함수형으로 바꿀 필요도 없습니다.

필요한 곳에 적절히 사용했을 때 Stream API는 가독성과 유지보수성을 크게 높여주는 강력한 무기가 되는것을 알게 되었습니다.

 

 

저작자표시 비영리 (새창열림)

'자바' 카테고리의 다른 글

멀티스레드 동시성 이슈를 해결하는 Atomic 변수와 Concurrent 컬렉션  (0) 2025.12.24
자바 Socket 클래스와 네트워크 설정 이해하기  (0) 2025.12.21
Java 개발자가 꼭 알아야 할 버전별 특징 – 8 vs 11 vs 17  (0) 2025.12.19
Object가 왜 최상위 부모인지 이제는 알고 쓰자  (0) 2025.12.03
[JVM 완전정복 #5] Execution Engine 완전정복: 인터프리터와 JIT의 비밀  (0) 2025.11.17
'자바' 카테고리의 다른 글
  • 멀티스레드 동시성 이슈를 해결하는 Atomic 변수와 Concurrent 컬렉션
  • 자바 Socket 클래스와 네트워크 설정 이해하기
  • Java 개발자가 꼭 알아야 할 버전별 특징 – 8 vs 11 vs 17
  • Object가 왜 최상위 부모인지 이제는 알고 쓰자
깊은바다속꼬북이
깊은바다속꼬북이
  • 깊은바다속꼬북이
    CodeBlossom
    깊은바다속꼬북이
  • 전체
    오늘
    어제
    • 분류 전체보기 (53) N
      • 라이징 캠프 (4)
      • 객채지향 개발론 (3)
      • 스프링 (10) N
      • 네트워크 (2)
      • 자바 (16)
      • 자료구조 (3)
      • 운영체제 (0)
      • 데이터베이스 (4)
      • 디자인패턴 (7)
      • JSP (1)
      • 개발 알쓸신잡 (2)
      • 일반 교양 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    개발 교훈
    트랜잭션 전파레벨
    객체지향
    개발자 철학
    JUnnit5
    프로그램밍 언어
    jit-compiler
    MySQL 파서
    스프링
    백엔드
    java data area
    MySQL 옵티마이저
    java
    자바 Socket 클래스
    GC
    한국어 검색
    개발 철학
    자료구조
    디자인 패턴
    어댑터 패턴(Adapter Pattern)
    junnit5프레임워크
    java 버전별 특징
    템플릿 메서드 패턴(Template Method Pattern)
    spring
    JVM
    싱글턴 패턴(Singleton Pattern)
    디자인패턴
    전략 패턴(Strategy Pattern)
    jvm 클래스 로더
    mockito라이브러리
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
깊은바다속꼬북이
함수형 프로그래밍이란? (feat. Java Stream API)
상단으로

티스토리툴바