포스트

JAVA (23) - 람다와 스트림

JAVA (23) - 람다와 스트림

1. 개요

Java 8에서 도입된 가장 큰 변화 중 하나는 함수형 프로그래밍 스타일의 도입이다. 그 중심에는 람다식(Lambda Expression)과 스트림 API(Stream API)가 존재한다. 람다식을 통해 코드를 더욱 간결하고 직관적으로 작성할 수 있게 되었으며, 스트림을 사용하면 컬렉션이나 배열 데이터를 효율적이고 통일된 방식으로 처리할 수 있다.

2. 람다식(Lambda Expression)의 개념과 특징

람다식은 익명 함수(Anonymous Function)를 생성하기 위한 식이다. 메서드를 하나의 간결한 ‘식(Expression)’으로 표현하여 코드를 줄이고 가독성을 높여준다.

  • 기본 문법: (매개변수) -> { 실행문; }
  • 주요 특징:
  • 메서드의 이름과 반환 타입을 생략할 수 있어 코드가 매우 간결해진다.
  • 람다식은 함수형 인터페이스(단 하나의 추상 메서드만 가지는 인터페이스)의 구현체로 취급된다.
  • 대표적인 함수형 인터페이스로는 Runnable, Supplier, Consumer, Function, Predicate 등이 있다.
1
2
3
4
5
6
7
8
9
10
11
// 기존 익명 클래스 방식
Runnable runnable1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World!");
    }
};

// 람다식 적용
Runnable runnable2 = () -> System.out.println("Hello World!");

3. 스트림 API(Stream API)의 개념과 특징

스트림은 데이터 소스(컬렉션, 배열 등)를 추상화하고, 데이터를 다루는 데 자주 사용되는 메서드들을 정의해 둔 객체다.

주요 특징:

  • 원본 데이터 비변경: 스트림은 데이터 소스로부터 읽기만 할 뿐, 원본 데이터를 직접 수정하지 않는다.
  • 일회용: 스트림은 한 번 사용하고 닫히면 재사용할 수 없다.
  • 지연 연산(Lazy Evaluation): 최종 연산이 수행되기 전까지 중간 연산은 실행되지 않는다.
  • 병렬 처리 지원: parallelStream()을 활용하면 다중 스레드를 통한 병렬 처리를 손쉽게 구현할 수 있다.

4. 스트림의 연산 구조 및 주요 연산

스트림을 이용한 데이터 처리는 생성 -> 중간 연산 -> 최종 연산의 흐름(파이프라인)을 거친다.

(1) 스트림 생성

컬렉션이나 배열 등을 스트림 객체로 변환하는 단계다.

  • list.stream() / Arrays.stream(array) / Stream.of(...)

(2) 중간 연산 (Intermediate Operations)

데이터를 가공·필터링하는 단계로, 연산 결과로 새로운 스트림을 반환하므로 체이닝(Chaining)이 가능하다. 최종 연산이 호출되기 전까지는 실행되지 않는 지연 연산(Lazy Evaluation)의 특징을 갖는다.

  • 필터링 및 변환
    • filter(Predicate): 조건에 맞는 요소만 추출
    • map(Function): 요소를 원하는 형태나 값으로 변환
    • flatMap(Function): 중첩된 구조(리스트 안의 리스트 등)를 단일 스트림으로 평평하게 변환
  • 정렬 및 중복 제거
    • distinct(): 중복 요소 제거 (equals, hashCode 기준)
    • sorted() / sorted(Comparator): 요소 오름차순 또는 지정 기준 정렬
  • 자르기 및 조회
    • limit(long maxSize): 지정한 개수만큼 요소 잘라내기
    • skip(long n): 지정한 개수만큼 요소 건너뛰기
    • peek(Consumer): 중간 결과 확인 (디버깅 용도)

(3) 최종 연산 (Terminal Operations)

가공된 스트림의 요소를 소모하여 최종 결과를 도출한다. 최종 연산이 실행되면 스트림 파이프라인이 닫히며 재사용할 수 없다.

  • 요소 수집 및 순회
    • collect(Collector): List, Set, Map 등 원하는 컬렉션 타입으로 결과 수집
    • forEach(Consumer): 각 요소를 순회하며 지정된 작업 수행
    • toArray(): 스트림 요소를 배열로 변환
  • 통계 및 누적 계산
    • count(): 전체 요소 개수 반환
    • sum() / average(): 숫자형 스트림의 합계 및 평균 계산
    • max(Comparator) / min(Comparator): 최댓값 및 최솟값 반환 (Optional)
    • reduce(BinaryOperator): 모든 요소를 누적하여 하나의 결과로 축소
  • 조건 검사 및 검색 (Short-circuit)
    • anyMatch(Predicate): 조건에 맞는 요소가 하나라도 있는지 확인 (boolean)
    • allMatch(Predicate): 모든 요소가 조건을 만족하는지 확인 (boolean)
    • noneMatch(Predicate): 모든 요소가 조건을 만족하지 않는지 확인 (boolean)
    • findFirst() / findAny(): 조건에 맞는 첫 번째 또는 임의의 요소 반환 (Optional)
1
2
3
4
5
6
7
8
List<String> names = Arrays.asList("Kim", "Lee", "Park", "Kang");

// 이름이 'K'로 시작하는 요소를 필터링하여 대문자로 변환 후 리스트로 수집
List<String> kNames = names.stream()
    .filter(name -> name.startsWith("K")) // 중간 연산
    .map(String::toUpperCase)            // 중간 연산
    .collect(Collectors.toList());       // 최종 연산

5. 람다와 스트림을 함께 사용할 때의 장점

  • 가독성과 생산성 향상: 반복문(for, while)과 조건문(if)이 복잡하게 얽힌 코드를 단 몇 줄의 직관적인 체이닝 코드로 대체할 수 있다.
  • 선언형 코드 작성: ‘어떻게 처리할 것인가(How)’에 집중하는 대신 ‘무엇을 할 것인가(What)’를 명확히 표현할 수 있다.
  • 유연한 병렬 처리: 대용량 데이터를 다룰 때 코드를 크게 변경하지 않고도 병렬 처리를 통해 성능을 향상시킬 수 있다.

6. 정리

자바의 람다와 스트림은 코드를 훨씬 간결하고 가독성 있게 만들어 주는 강력한 도구다. 람다식은 함수형 프로그래밍의 기반을 제공하며, 스트림은 데이터 처리 파이프라인(생성-중간연산-최종연산)을 통해 데이터의 가공 및 집계를 손쉽게 처리하도록 돕는다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.