[Java] 8. 람다함수, 함수형 프로그래밍, Stream API

2026. 2. 15. 00:36·프로젝트/Java

1. 람다함수

Java 8에서 가장 큰 변화는 람다함수, 함수형 프로그래밍의 적용,  Stream API의 등장이다. 이런 변화의 중심에는 인터페이스의 변경이 있었고, 이를 토대로 함수형 프로그래밍과 Stream API가 등장할 수 있었다. 먼저 람다함수에 대해 알아보자.

 

람다표현식의 등장은 불필요한 코드를 줄이고, 코드의 이해를 돕는다. 람다 표현식은 메소드로 전달할 수 있는 익명 함수를 단순화한 코드 블럭이다. 람다 표현식은 특정 클래스에 종속되지 않으며 함수라는 이름으로 명명한다. 함수 자체를 전달 인자로 보내거나 변수에 저장하는 것이 가능하다. 

 

람다 함수의 문법은 아래와 같다.

// 기존 방식 (익명 클래스)
new Comparator<Integer>() {
    @Override
    public int compare(Integer a, Integer b) {
        return a - b;
    }
};

// 람다 방식
(a, b) -> {a - b};
  • 매개변수: `(a, b)` 메서드에 전달되는 파라미터이다. 타입 추론이 가능하므로 타입을 생략할 수 있다.
  • 화살표: `->` 매개변수와 Body를 구분한다.
  • 함수 몸체: `a - b` 함수의 실행 로직이다. 식이 하나라면 중괄호 {}와 return 문을 생략할 수 있다.

자바는 모든 것이 객체여야한다. 람다 역시 실제로는 객체(인터페이스를 구현한 객체)이고 람다를 담을 수 있는 변수 타입이 필요하다. 람다함수를 담는 타입은 함수형 인터페이스이다. 함수형 인터페이스는 단 하나의 추상 메소드만 선언된 인터페이스이다. `@FunctionalInterface` 어노테이션을 통해 이 인터페이스가 함수형 인터페이스임을 밝히고 메소드를 두 개 이상 선언하면 컴파일 단계에서 에러를 발생시킬 수 있다.

 

자바에서는 함수형 인터페이스를 매번 만들기 귀찮기 때문에 자주 쓰인 형태를 미리 정의해두었다. `java.util.funtion` 패키지를 통해 함수형 인터페이스를 제공하고 있다.

함수형 인터페이스 함수 디스크립터 추상 메소드 설명
Predicate<T> T -> boolean `test(T t)` 조건을 검사하여 true, false 반환한다.
Consumer<T> T -> void `accept(T t)` 매개변수를 받아서 소비한다.
Supplier<T> () -> T `get()` 매개변수 없이 데이터를 공급하기만 한다.
Function<T, R> T -> R `apply(T t)` T를 받아서 R을 리턴한다.

 

 

* 메소드 레퍼런스

람다식에서 단 하나의 메소드만 호출하고 끝날 때, 불필요한 매개변수를 생략하고 `::` 이중콜론 기호를 사용해서 간결하게 사용할 수 있다. 이를 메소드 참조라고 한다. 

  코드 설명
람다식 `(s) -> System.out.println(s)` 매개변수 s를 받아 println(s)에 전달한다.
메소드 참조 `System.out::println` println 메서드를 직접 지칭한다.

 

매개변수를 직접 전달하지 않아서 출력이 되는지 의심될 수 있는데 여기서 컴파일러가 역할을 한다. 컴파일러가 함수형 인터페이스에 선언된 메소드의 명세(파라미터 타입과 개수)를 보고 자동으로 매칭해준다. 아래와 같이 활용이 가능하다.

 

List<String> names = Arrays.asList("Samsung", "Apple", "Google");

// 1. 람다식 활용
names.sort((s1, s2) -> s1.compareTo(s2));

// 2. 메서드 참조 활용
names.sort(String::compareTo);

2. 함수형 프로그래밍

프로그래밍언어론 시간에 LISP이라는 언어를 배운 적 있다. LISP은 괄호가 겁나 많은 언어이다. 과거 딥러닝 이전의 AI인 기호주의 AI 구현에 주로 쓰였던 언어인데 이 언어가 함수형 프로그래밍 방식을 따르는 언어라고 배웠었다. 과제 구현을 하면서 굉장히 불편함을 느꼈었는데 for문 없이 반복을 재귀로 구현해야했던 기억이 있다. 함수형 프로그래밍에 대해 알기 위해선 우선 명령형 프로그래밍과 선언형 프로그래밍에 대해 구분해야한다.

  • 명령형 프로그래밍: 특정 기능을 수행하기 위해 어떻게(how)에 집중하는 방식을 의미한다.
  • 선언형 프로그래밍: 특정 기능을 수행하기 위해 무엇(what)에 집중하는 방식을 의미한다.

리스트에서 짝수의 합을 구하는 예시를 통해 두 패러다임 방식의 차이를 살펴보자.

 

[명령형 프로그래밍의 예시]

List<Integer> list = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);

int result = 0;

for (int n : list) {
	if (n % 2 == 0) {
    	result += n;
    }
}

리스트에서 짝수의 합을 구하는 예시이다. 명령형 프로그래밍은 우리가 기존에 사용하던 방식이다. 반복문, 조건문을 이용해 직접적으로 어떻게 짝수의 합을 구할지 단계별로 하나하나 기술해줘야한다.

 

[선언형 프로그래밍의 예시]

List<Integer> list = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);

int result = 0;
result = list.stream()
            .fliter(n -> n%2==0)
            .mapToInt(Integer::intValue)
            .sum();

선언형 프로그래밍에서는 새로운 방식을 사용한다. 리스트에 대해 기존에 정의된 함수들을 호출해서 처리한다. 함수 내부에서 어떻게 짝수 합을 구하는지는 관심 밖이다. 덕분에 코드가 간결해지고, 핵심 논리에만 접근해도 된다는 장점이 있다. 그러나 오류가 발생할 경우 디버깅이 힘들어진다는 문제가 있다. 이러한 방식을 선언형 프로그램이이라고하고, 함수형 프로그래밍은 선언형 프로그래밍을 따르는 대표적인 프로그래밍 패러다임이다.

 

함수형 프로그래밍은 함수들의 집합으로 프로그램을 구성하는 것을 의미한다.

 

함수형 프로그래밍에서 함수는 아래 3가지 특성을 갖는다.

  • 순수함수: 함수의 output이 오로지 input에 의해 결정된다. 함수의 실행에 따른 부수효과(side-effect)가 발생하지 않는다.
  • 일급객체: 함수의 변수 혹은 특정 데이터 구조에 담을 수 있다. 함수를 파라미터를 통해 전달할 수 있고, 결과로 반환할 수 있다.
  • 영속 자료구조: 특정 변수, 객체의 자료를 직접 변경하지 않고, 새로운 인스턴스를 통해 의도하지 않는 변경을 방지한다.

* 부수효과(side-effect): 외부 데이터를 변형시키는 것.

 

자바에서는 람다식과 Stream API를 도입하면서 이러한 함수형 프로그래밍 패러다임을 수용하게 되었다.


3. Stream API

자바 8에서 Stream API를 활용하면 Collection 프레임워크의 데이터를 표준화된 방법으로 다룰 수 있는 장점이 있다. Stream API를 통해 데이터를 간결하고, 가독성 있게 처리할 수 있다. Stream에서는 함수형 프로그래밍 방식으로 데이터를 가공할 수 있다는 것이 핵심이다.

 

Stream은 3단계 파이프라인을 갖는다. stream 객체 생성 → 중간 연산 → 최종 연산의 과정을 거친다.

 

1. stream 객체 생성

우선 stream 객체를 생성해야한다. Collection 인터페이스는 `stream()` 메소드를 default 메소드로 정의하고 있다. `stream()` 메소드는 해당 컬렉션이 가지고 있는 항목들에 대해 스트림 처리가 가능한 Stream 객체를 반환해준다. 한번 생성한 스트림은 다시 사용할 수 없으며, 전체 데이터에 대한 처리가 이루어지면 종료된다. 다시 스트림을 사용하려면 새로 생성해야한다.

  • `list.stream()`: Collection을 stream으로 변환
  • `Arrays.stream(arr)`: 배열을 스트림으로 변환
  • `IntStream.range(1, 10)`: 특정 범위의 숫자를 생성

 

2. 중간 연산

데이터를 필터링하거나 변환하며, 결과값으로 다시 스트림을 반환한다. 중간 연산의 중요한 특징은 지연 연산(Lazy Evaluation)으로 최종 연산이 호출되기 전까지는 실제로 실행되지 않는다는 것이다.

  • `filter(Predicate<T>)`: 조건에 맞는 요소만 추출한다.
  • `map(Function<T, R>)`: 요소를 다른 값으로 변환한다.
  • `sorted(Comparator<T>)`: 요소 정렬
  • `distinct()`: 중복 제거

 

3. 최종 연산

스트림을 닫으면서 void를 반환하거나 Collection 타입을 반환한다. 중간 연산을 통해 가공된 스트림은 마지막에 최종 연산을 통해 각 요소를 소모하여 결과를 출력한다. 즉, 중간연산에서 지연되었던(Lazy Evaluation) 모든 중간 연산들이 최종 연산단계에서 모두 수행되는 것이다. 최종 연산 후에는 소모한 스트림이 닫히게 되고, 스트림은 재사용이 불가능하다.

  • `collect(Collectors.toList())`: 결과를 리스트 등으로 수집
  • `forEach(Consumer)`: 스트림의 각 요소를 소비하며 람다 적용, void 반환
  • `count()`: 스트림의 요소의 개수를 반환한다.
  • `sum()`: 스트림의 요소의 합을 반환한다.
  • `reduce(BinaryOperator)`: 데이터를 하나로 합침 (합계, 최댓값 등)

 

실제 스트림 활용 예시는 아래와 같다.

 

1. 데이터 필터링과 변환 (filter, map)

List<String> result = users.stream()
    .filter(user -> user.getAge() >= 20)      // 20세 이상만 필터링
    .map(User::getName)                       // 이름(String)만 추출
    .limit(5)                                 // 상위 5개만
    .collect(Collectors.toList());            // 결과 수집

중간 연산 단계에서는 람다함수 또는 메소드 레퍼런스 방식을 통해 어떤 식으로 중간연산을 수행할 것인지 나타내 줘야한다.

 

2. 정렬과 중복 제거 (sorted, distinct)

List<Integer> numbers = Arrays.asList(3, 1, 2, 2, 4);
List<Integer> sortedUnique = numbers.stream()
    .distinct()                               // 중복 제거 (2 하나만 남음)
    .sorted()                                 // 오름차순 정렬
    .collect(Collectors.toList());            // [1, 2, 3, 4]

 

3. 요소 결합 (reduce)

int totalSum = IntStream.rangeClosed(1, 100)
    .reduce(0, (a, b) -> a + b);              // 1부터 100까지의 합계 (5050)

초기값 0과 스트림의 첫번째 요소 1을 더하고, 누적값 1과 두번째 요소 2를 더한다. 이런식으로 100까지 반복한다.

 

스트림을 사용하면 원본을 변형시키지 않고 새로운 컬렉션에 담아서 반환한다. 따라서 새로운 컬렉션에 스트림 결과를 받아야한다. 

 

 

'프로젝트 > Java' 카테고리의 다른 글

[Java] 7. Wrapper 클래스, Collection 프레임워크  (0) 2026.02.07
[Java] 6. 예외처리  (0) 2026.02.06
[Java] 5. 문자열 (String, StringBuilder, StringBuffer), 자바 표준 입출력  (0) 2026.01.29
[Java] 4. 추상 클래스, 인터페이스  (0) 2026.01.28
[Java] 3. Object 클래스 (toString, equals, hashCode, clone)  (0) 2026.01.28
'프로젝트/Java' 카테고리의 다른 글
  • [Java] 7. Wrapper 클래스, Collection 프레임워크
  • [Java] 6. 예외처리
  • [Java] 5. 문자열 (String, StringBuilder, StringBuffer), 자바 표준 입출력
  • [Java] 4. 추상 클래스, 인터페이스
sophon
sophon
sophon 님의 블로그 입니다.
  • sophon
    sophon 님의 블로그
    sophon
    • 카테고리 (172) N
      • 컴퓨터공학 (36)
        • 데이터베이스 (19)
        • 네트워크 (15)
        • 기타 이슈 (2)
      • 프로젝트 (16) N
        • Java (8)
        • Spring (4) N
        • Docker (4)
      • 코딩테스트 (95) N
        • BOJ (74)
        • 프로그래머스 (7)
        • 프로그래머스 SQL (12) N
        • PS Snippets (2)
      • 🌱 잡담 (22)
        • 자격증 (7)
        • 좋은 시 모음 (12)
        • 책과 영화 (3)
        • 기록 (0)
  • 전체
    오늘
    어제
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
sophon
[Java] 8. 람다함수, 함수형 프로그래밍, Stream API
상단으로

티스토리툴바