메뉴 닫기

Java Stream 생성 방법과 filter, map 중간 연산 완벽 가이드

Java Stream 생성 방법과 filter, map 중간 연산 완벽 가이드

🚀 스트림 생성부터 강력한 중간 연산까지 한 번에 배우는 실전 활용법

Java 프로그래밍을 하다 보면 대량의 데이터를 효율적으로 처리해야 하는 순간이 자주 찾아옵니다.
그럴 때 스트림(Stream) API는 복잡한 반복문 없이도 선언적이고 깔끔한 코드를 작성하게 해주는 강력한 도구입니다.
하지만 스트림의 생성 방식과 filter, map 같은 중간 연산의 원리를 정확히 이해하지 못하면 그 진가를 발휘하기 어렵습니다.
이번 글에서는 다양한 스트림 생성 방법과 자주 쓰이는 중간 연산의 작동 방식, 그리고 실전 예제를 통해 스트림을 능숙하게 다루는 방법을 차근차근 안내해 드리겠습니다.

특히 Java 8 이후로 도입된 람다(Lambda)와 스트림 API는 컬렉션, 배열, 파일 데이터 등 다양한 소스를 처리하는 데 있어 필수적인 기능이 되었습니다.
단순한 데이터 변환부터 복잡한 필터링, 매핑 작업까지 스트림이 제공하는 기능을 이해하면 코드의 가독성과 유지보수성이 크게 향상됩니다.
이 글을 끝까지 읽으면, 여러분은 스트림을 활용한 데이터 처리에 한층 자신감을 가질 수 있을 것입니다.



💡 스트림(Stream) API란?

Java의 스트림(Stream) API는 데이터의 흐름을 추상화하여, 배열이나 컬렉션 같은 데이터 소스를 효율적으로 처리할 수 있도록 해주는 기능입니다.
단순 반복문으로 데이터를 다루는 대신, 선언형 프로그래밍 방식으로 읽기 쉽고 유지보수가 간편한 코드를 작성할 수 있게 합니다.
이 덕분에 데이터 필터링, 매핑, 집계 같은 복잡한 작업도 간단한 메서드 호출로 구현할 수 있습니다.

스트림 API는 Java 8에서 처음 도입되었으며, 람다(Lambda)와 함께 사용하면 강력한 데이터 처리 파이프라인을 구축할 수 있습니다.
예를 들어, 리스트에서 특정 조건을 만족하는 값만 걸러내거나, 데이터를 변환하고, 합계나 평균을 구하는 작업까지 한 줄로 작성 가능합니다.
이러한 작업은 모두 중간 연산최종 연산을 통해 이루어집니다.

📌 스트림의 핵심 특징

  • 🚫데이터 소스를 변경하지 않음 (불변성 보장)
  • 필요한 데이터만 지연(lazy) 처리로 최적화
  • 🔗여러 중간 연산을 체인 형태로 연결 가능
  • 🛠️병렬 처리(Parallel Stream)로 성능 향상 가능

💬 스트림은 데이터 저장 구조가 아닌, 데이터를 처리하는 연산들의 모음입니다.
따라서 한 번 소비(consume)하면 다시 사용할 수 없다는 점을 기억해야 합니다.

CODE BLOCK
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

names.stream()
     .filter(name -> name.startsWith("A"))
     .map(String::toUpperCase)
     .forEach(System.out::println);
// 출력: ALICE

🔨 스트림 생성 방법

Java에서 스트림(Stream)을 생성하는 방법은 매우 다양합니다.
컬렉션, 배열, 파일, 그리고 무한 시퀀스까지 다양한 데이터 소스를 스트림으로 변환할 수 있습니다.
올바른 스트림 생성 방법을 아는 것은 효율적인 데이터 처리의 첫걸음이 됩니다.

📌 컬렉션에서 스트림 생성

List, Set, Map 같은 컬렉션은 .stream() 메서드를 통해 바로 스트림을 만들 수 있습니다.
또한, 병렬 처리가 필요할 경우 .parallelStream()을 사용할 수 있습니다.

CODE BLOCK
List<String> list = Arrays.asList("A", "B", "C");
Stream<String> stream = list.stream();

📌 배열에서 스트림 생성

배열은 Arrays.stream() 또는 Stream.of()를 사용하여 스트림으로 변환할 수 있습니다.

CODE BLOCK
String[] arr = {"A", "B", "C"};
Stream<String> stream1 = Arrays.stream(arr);
Stream<String> stream2 = Stream.of(arr);

📌 파일에서 스트림 생성

Java NIO의 Files.lines() 메서드를 사용하면 텍스트 파일의 각 라인을 스트림으로 읽을 수 있습니다.

CODE BLOCK
try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
    lines.forEach(System.out::println);
}

📌 무한 스트림 생성

Stream.iterate() 또는 Stream.generate()를 사용하면 무한 스트림을 만들 수 있습니다.
이 경우 반드시 limit()으로 요소 개수를 제한해야 메모리 과부하를 방지할 수 있습니다.

CODE BLOCK
Stream.iterate(0, n -> n + 2)
      .limit(5)
      .forEach(System.out::println);
// 출력: 0, 2, 4, 6, 8



🎯 filter로 데이터 선별하기

filter() 메서드는 스트림의 각 요소를 주어진 조건(Predicate)에 따라 걸러내는 중간 연산입니다.
조건을 만족하는 요소만 다음 단계로 전달되며, 조건에 맞지 않는 요소는 제외됩니다.
이는 데이터 검색, 유효성 검사, 조건부 처리에 매우 유용합니다.

📌 filter 기본 사용법

filter는 Predicate<T>를 매개변수로 받습니다.
즉, 각 요소를 검사하여 true를 반환하면 유지, false면 제거됩니다.

CODE BLOCK
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

numbers.stream()
       .filter(n -> n % 2 == 0)
       .forEach(System.out::println);
// 출력: 2, 4, 6

📌 복합 조건 사용

filter 안에서 && 또는 ||를 사용하여 복합 조건을 구현할 수 있습니다.

CODE BLOCK
List<String> names = Arrays.asList("Anna", "Bob", "Charlie", "Amanda");

names.stream()
     .filter(name -> name.startsWith("A") && name.length() > 3)
     .forEach(System.out::println);
// 출력: Anna, Amanda

📌 실전 활용 예시

예를 들어, 회원 목록에서 성인 회원만 추출하거나, 주문 목록에서 특정 상태의 주문만 필터링할 때 filter를 사용할 수 있습니다.

💡 TIP: filter는 여러 번 연속해서 사용할 수 있으며, 이 경우 각 조건이 순차적으로 적용됩니다.

CODE BLOCK
orders.stream()
      .filter(order -> order.getStatus().equals("DELIVERED"))
      .filter(order -> order.getTotalPrice() >= 50000)
      .forEach(System.out::println);

🔄 map으로 데이터 변환하기

map() 메서드는 스트림의 각 요소를 다른 형태로 변환할 때 사용하는 중간 연산입니다.
데이터 타입을 바꾸거나, 특정 필드를 추출하거나, 값을 가공하는 데 활용됩니다.
이 과정에서 원본 데이터는 변경되지 않으며, 변환된 새로운 요소들이 포함된 스트림이 생성됩니다.

📌 map 기본 사용법

map은 Function<T, R>을 인자로 받아 각 요소를 새로운 값으로 변환합니다.

CODE BLOCK
List<String> names = Arrays.asList("alice", "bob", "charlie");

names.stream()
     .map(String::toUpperCase)
     .forEach(System.out::println);
// 출력: ALICE, BOB, CHARLIE

📌 객체 변환

map을 사용하면 DTO 변환, 특정 필드 값 추출 등 객체 가공에도 적합합니다.

CODE BLOCK
List<User> users = Arrays.asList(
    new User("Alice", 25),
    new User("Bob", 30)
);

List<String> namesOnly = users.stream()
                               .map(User::getName)
                               .collect(Collectors.toList());
// 결과: ["Alice", "Bob"]

📌 map과 다른 변환 연산

map()과 유사한 연산으로는 flatMap()이 있습니다.
flatMap은 각 요소를 스트림으로 변환한 뒤 이를 평탄화(flatten)하여 하나의 스트림으로 합칩니다.
이는 중첩 구조를 해제하는 데 유용합니다.

CODE BLOCK
List<List<String>> listOfLists = Arrays.asList(
    Arrays.asList("A", "B"),
    Arrays.asList("C", "D")
);

listOfLists.stream()
           .flatMap(List::stream)
           .forEach(System.out::println);
// 출력: A, B, C, D

💡 TIP: map과 filter를 함께 사용하면 데이터 변환과 조건 필터링을 한 번에 처리할 수 있습니다.



⚙️ 중간 연산의 동작 원리

Java 스트림의 중간 연산(Intermediate Operation)은 데이터를 변환, 필터링, 정렬 등 가공하는 과정입니다.
대표적인 예로는 filter(), map(), sorted() 등이 있습니다.
중간 연산은 최종 연산이 호출되기 전까지 실제로 실행되지 않으며, 이를 지연 처리(lazy evaluation)라고 부릅니다.

📌 지연 처리의 장점

지연 처리를 통해 불필요한 연산을 줄이고, 최종적으로 필요한 데이터만 계산할 수 있습니다.
예를 들어, 1,000개의 데이터 중 상위 5개만 출력한다면, 나머지 995개는 처리하지 않습니다.

CODE BLOCK
Stream.of("A", "B", "C", "D", "E")
      .filter(s -> {
          System.out.println("필터링: " + s);
          return true;
      })
      .limit(2)
      .forEach(System.out::println);
// 출력은 A, B까지만 필터링 후 종료

📌 파이프라인 처리

중간 연산은 체이닝 방식으로 연결되어 파이프라인을 형성합니다.
각 요소는 파이프라인을 따라 이동하며, 모든 중간 연산을 거친 후 최종 연산에 도달합니다.
이 구조 덕분에 가독성과 유지보수성이 크게 향상됩니다.

CODE BLOCK
List<String> result = names.stream()
                            .filter(n -> n.length() >= 3)
                            .map(String::toUpperCase)
                            .sorted()
                            .collect(Collectors.toList());

📌 상태 있는 중간 연산 vs 상태 없는 중간 연산

중간 연산은 크게 두 가지로 나눌 수 있습니다.

  • 상태 없는 중간 연산: 각 요소를 독립적으로 처리 (예: filter, map)
  • 📦상태 있는 중간 연산: 모든 요소를 모은 후 처리 (예: sorted, distinct)

⚠️ 주의: 상태 있는 중간 연산은 메모리 사용량이 많아질 수 있으므로 대용량 데이터 처리 시 주의가 필요합니다.

자주 묻는 질문 (FAQ)

스트림과 컬렉션의 차이는 무엇인가요?
컬렉션은 데이터 저장 구조이고, 스트림은 데이터를 처리하는 연산의 집합입니다. 스트림은 한 번 사용하면 재사용할 수 없습니다.
filter와 map의 실행 순서는 어떻게 되나요?
filter로 조건을 만족하는 요소를 먼저 걸러낸 후, map으로 해당 요소들을 변환하는 방식으로 순차 실행됩니다.
flatMap은 언제 사용하나요?
데이터가 중첩 구조(리스트 안의 리스트 등)일 때 이를 하나의 평평한 스트림으로 만들고 싶을 때 사용합니다.
스트림은 왜 지연 처리를 하나요?
최종 연산이 호출될 때까지 계산을 미루어 불필요한 연산을 줄이고 성능을 최적화하기 위해서입니다.
중간 연산과 최종 연산은 어떻게 구분되나요?
중간 연산은 filter, map처럼 스트림을 반환하며, 최종 연산은 collect, forEach처럼 결과를 반환하고 스트림을 종료합니다.
parallelStream은 언제 쓰면 좋은가요?
대량의 데이터를 멀티코어 CPU에서 병렬 처리하여 성능 향상이 기대될 때 사용합니다. 다만, 오버헤드가 커서 항상 효율적이지는 않습니다.
스트림 사용 시 성능 저하가 발생할 수 있나요?
불필요하게 많은 중간 연산이나 상태 있는 중간 연산을 사용하면 성능 저하가 발생할 수 있습니다.
람다 없이 스트림을 사용할 수 있나요?
가능합니다. 익명 클래스(Anonymous Class)를 사용해 구현할 수 있지만, 람다를 사용하면 코드가 훨씬 간결해집니다.

🚀 스트림 API로 더 효율적인 Java 프로그래밍

Java 스트림(Stream) API는 데이터 처리 방식을 단순화하고, 가독성과 유지보수성을 크게 높여주는 강력한 도구입니다.
이번 글에서는 스트림의 개념과 다양한 생성 방법, filter와 map 같은 핵심 중간 연산, 그리고 중간 연산의 동작 원리까지 살펴보았습니다.
스트림을 올바르게 활용하면 반복문을 최소화하고, 선언형 프로그래밍 스타일로 더욱 직관적인 코드를 작성할 수 있습니다.

특히, 지연 처리와 파이프라인 구조를 이해하고 적절히 사용하는 것이 중요합니다.
필요한 데이터만 효율적으로 처리하여 성능을 높이고, 불필요한 연산을 줄일 수 있습니다.
또한, filter와 map을 적절히 조합하면 데이터 필터링과 변환을 한 번에 처리할 수 있어 코드의 효율성이 배가됩니다.

이제 여러분은 컬렉션, 배열, 파일 등 다양한 데이터 소스에서 스트림을 생성하고, 원하는 형태로 가공하는 전 과정을 이해하게 되었습니다.
다음 프로젝트에서 스트림 API를 적극 활용해보세요.
한층 깔끔하고 유지보수가 쉬운 코드를 작성할 수 있을 것입니다.


🏷️ 관련 태그 : Java, 스트림API, 람다식, filter, map, 중간연산, 지연처리, flatMap, 컬렉션처리, 병렬스트림