2025-09-30 Stream

Ckd gus·2026년 2월 2일

스트림(Stream)

스트림(Stream)은 데이터의 추상화된 연속된 흐름을 의미한다.
데이터 컬렉션을 더 간결하고 직관적인 방식으로 처리할 수 있게 해주는 도구이다.


주요 특징

  • 데이터 소스를 변경하지 않고, 스트림을 통해 한 번만 처리하는 일회용 객체이며 재사용할 수 없다.
  • 반복문을 내부적으로 숨기고, 선언형 코드로 데이터를 다룰 수 있다.
  • 중간 연산(filter, map 등)과 최종 연산(collect, sum, forEach 등)으로 구분된다.
  • 병렬 처리(멀티스레드) 기능도 지원하여 큰 데이터를 효율적으로 처리할 수 있다.
  • 기본형 스트림(IntStream, LongStream, DoubleStream)을 지원하여 오토박싱/언박싱 비효율을 줄인다.
  • 컬렉션, 배열, 파일 등 다양한 데이터 소스에서 스트림을 생성할 수 있다.

스트림 생성

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;

public class StreamCreateExample {
    public static void main(String[] args) {
        // Set
        System.out.println("=".repeat(50));
        Set<Integer> set = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5));
        Stream<Integer> stream1 = set.stream();
        stream1.forEach(System.out::println);

        // Array
        System.out.println("=".repeat(50));
        String[] array = {"Java", "Python", "JavaScript"};
        Stream<String> stream2 = Arrays.stream(array);
        stream2.forEach(System.out::println);

        // 부분 배열
        System.out.println("=".repeat(50));
        Stream<String> stream3 = Arrays.stream(array, 0, 2);
        stream3.forEach(System.out::println);

        // 정적 메서드 사용
        System.out.println("======== 정적 메서드 사용 =======");

        // Stream.iterate() - 무한 스트림 생성
        Stream<Integer> iterateStream = Stream.iterate(0, n -> n + 2)
                .limit(10);

        // Stream.generate() - 무한 스트림 생성
        Stream<Double> randomStream = Stream.generate(Math::random)
                .limit(5);

        iterateStream.forEach(System.out::println);
        randomStream.forEach(System.out::println);

        // 파일에서 라인 스트림 생성
        try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
            lines.forEach(System.out::println);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

중간 연산과 최종 연산

  • 중간 연산: 스트림을 변환/필터링하며 다른 스트림을 반환한다. 여러 개를 체이닝할 수 있고, 지연 실행(Lazy Evaluation) 된다.
  • 최종 연산: 스트림 파이프라인을 실행하고 결과를 반환한다. 최종 연산이 호출되어야 중간 연산이 실제로 실행된다.
import java.util.*;
import java.util.stream.Collectors;

public class StreamPipelineExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");

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

        System.out.println(result); // [ALICE, CHARLIE, DAVID]
    }
}


필터링

filter

import java.util.*;

public class FilterExample {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("Apple", "Banana", "Cherry", "Apple", "Cherry", "Date");

        List<String> resultWords = new ArrayList<>();
        for (String word : words) {
            if (word.length() >= 5) {
                if (resultWords.contains(word)) continue;
                resultWords.add(word);
            }
        }

        System.out.println(resultWords);
        System.out.println("=".repeat(50));
    }
}

distinct

import java.util.*;
import java.util.stream.Collectors;

public class DistinctExample {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("Apple", "Banana", "Cherry", "Apple", "Cherry", "Date");

        List<String> resultWords2 = words.stream()
                .filter(word -> word.length() >= 5)
                .distinct()
                .toList();

        System.out.println(resultWords2);

        System.out.println("=".repeat(50));

        List<String> resultWords3 = words.stream()
                .filter(word -> word.length() >= 5)
                .distinct()
                .collect(Collectors.toList());

        System.out.println(resultWords3);
        resultWords3.add("test");
        System.out.println(resultWords3);
    }
}

변환

map (1:1 변환)

import java.util.*;
import java.util.stream.Collectors;

public class MapExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("alice", "bob", "charlie");

        List<String> upperNames = names.stream()
                .map(String::toUpperCase)
                .collect(Collectors.toList());

        System.out.println(upperNames);

        System.out.println("=".repeat(50));

        List<Integer> lengths = names.stream()
                .map(String::length)
                .collect(Collectors.toList());

        System.out.println(lengths);
    }
}

flatMap (1:N 변환 후 평탄화)

import java.util.*;
import java.util.stream.Collectors;

public class FlatMapExample {
    public static void main(String[] args) {
        List<List<String>> nestedList = Arrays.asList(
                Arrays.asList("Apple", "Banana"),
                Arrays.asList("Cherry", "Date"),
                Arrays.asList("Elderberry", "Fig")
        );

        List<String> flatList = nestedList.stream()
                .flatMap(Collection::stream)
                .collect(Collectors.toList());

        System.out.println(flatList);

        List<String> words = Arrays.asList("Hello", "World");
        List<String> letters = words.stream()
                .flatMap(word -> Arrays.stream(word.split("")))
                .distinct()
                .collect(Collectors.toList());

        System.out.println(letters);
    }
}

정렬과 순회

sorted

import java.util.*;
import java.util.stream.Collectors;

public class SortedExample {
    public static void main(String[] args) {
        List<String> fruits = Arrays.asList("Banana", "Apple", "Cherry", "Date");

        List<String> sortedAsc = fruits.stream()
                .sorted()
                .collect(Collectors.toList());
        System.out.println("오름차순: " + sortedAsc);

        List<String> sortedDesc = fruits.stream()
                .sorted(Comparator.reverseOrder())
                .collect(Collectors.toList());
        System.out.println("내림차순: " + sortedDesc);

        List<String> sortedByLength = fruits.stream()
                .sorted(Comparator.comparingInt(String::length))
                .collect(Collectors.toList());
        System.out.println("길이순: " + sortedByLength);
    }
}

forEach (최종 연산)

import java.util.*;

public class ForEachExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        numbers.stream()
                .forEach(n -> System.out.println("Number: " + n));
    }
}

peek (중간 연산, 디버깅 목적)

import java.util.*;
import java.util.stream.Collectors;

public class PeekExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        List<Integer> doubled = numbers.stream()
                .peek(n -> System.out.println("원본: " + n))
                .map(n -> n * 2)
                .peek(n -> System.out.println("두 배: " + n))
                .collect(Collectors.toList());

        System.out.println(doubled);
    }
}

매칭과 검색

allMatch / anyMatch / noneMatch

import java.util.*;

public class MatchExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        boolean allPositive = numbers.stream()
                .allMatch(n -> n > 0);
        System.out.println("모두 양수? " + allPositive);

        boolean hasEven = numbers.stream()
                .anyMatch(n -> n % 2 == 0);
        System.out.println("짝수 존재? " + hasEven);

        boolean noNegative = numbers.stream()
                .noneMatch(n -> n < 0);
        System.out.println("음수 없음? " + noNegative);
    }
}

findFirst / findAny

import java.util.*;

public class FindExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

        Optional<String> first = names.stream()
                .filter(name -> name.startsWith("C"))
                .findFirst();
        first.ifPresent(System.out::println);

        Optional<String> any = names.parallelStream()
                .filter(name -> name.length() > 3)
                .findAny();
        any.ifPresent(System.out::println);
    }
}

집계 연산

count / min / max

import java.util.*;

public class BasicStatsExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        long count = numbers.stream().count();
        System.out.println("개수: " + count);

        Optional<Integer> min = numbers.stream().min(Integer::compareTo);
        min.ifPresent(n -> System.out.println("최소값: " + n));

        Optional<Integer> max = numbers.stream().max(Integer::compareTo);
        max.ifPresent(n -> System.out.println("최대값: " + n));
    }
}

숫자 스트림 집계

import java.util.*;
import java.util.IntSummaryStatistics;

public class PrimitiveStreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        int sum = numbers.stream()
                .mapToInt(Integer::intValue)
                .sum();
        System.out.println("합계: " + sum);

        double average = numbers.stream()
                .mapToInt(Integer::intValue)
                .average()
                .orElse(0.0);
        System.out.println("평균: " + average);

        IntSummaryStatistics stats = numbers.stream()
                .mapToInt(Integer::intValue)
                .summaryStatistics();
        System.out.println("통계: " + stats);
    }
}

리듀싱과 수집

reduce

import java.util.*;

public class ReduceExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        int sum = numbers.stream()
                .reduce(0, (a, b) -> {
                    System.out.println(a + "::" + b);
                    return a + b;
                });
        System.out.println("합계: " + sum);

        int sum2 = numbers.stream()
                .reduce(0, Integer::sum);
        System.out.println("합계2: " + sum2);

        Optional<Integer> max = numbers.stream()
                .reduce(Integer::max);
        max.ifPresent(n -> System.out.println("최대값: " + n));

        List<String> words = Arrays.asList("Hello", " ", "World", "!");
        String sent = words.stream()
                .reduce("", String::concat);
        System.out.println(sent);
    }
}

Collectors 수집

import java.util.*;
import java.util.stream.Collectors;

public class CollectorsExample {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student("Alice", 85, "CS"),
                new Student("Bob", 92, "Math"),
                new Student("Charlie", 78, "CS"),
                new Student("David", 88, "Physics"),
                new Student("Eve", 95, "Math")
        );

        List<String> names = students.stream()
                .map(Student::getName)
                .collect(Collectors.toList());
        System.out.println(names);

        Set<String> departments = students.stream()
                .map(Student::getDepartment)
                .collect(Collectors.toSet());
        System.out.println(departments);

        Map<String, Integer> nameToScore = students.stream()
                .collect(Collectors.toMap(Student::getName, Student::getScore));
        System.out.println(nameToScore);

        String allNames = students.stream()
                .map(Student::getName)
                .collect(Collectors.joining(", "));
        System.out.println(allNames);
    }

    static class Student {
        private final String name;
        private final int score;
        private final String department;

        public Student(String name, int score, String department) {
            this.name = name;
            this.score = score;
            this.department = department;
        }

        public String getName() { return name; }
        public int getScore() { return score; }
        public String getDepartment() { return department; }
    }
}

그룹화와 분할

groupingBy

import java.util.*;
import java.util.stream.Collectors;

public class GroupingByExample {
    public static void main(String[] args) {
        List<Employee> employees = Arrays.asList(
                new Employee("Alice", "IT", 5000),
                new Employee("Bob", "HR", 4000),
                new Employee("Charlie", "IT", 5500),
                new Employee("David", "HR", 4500),
                new Employee("Eve", "Sales", 4800)
        );

        Map<String, List<Employee>> byDepartment = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment));

        System.out.println("부서별 직원:");
        byDepartment.forEach((dept, emps) -> {
            System.out.println(dept + ": " +
                    emps.stream().map(Employee::getName).collect(Collectors.toList()));
        });

        Map<String, Double> avgSalaryByDept = employees.stream()
                .collect(Collectors.groupingBy(
                        Employee::getDepartment,
                        Collectors.averagingDouble(Employee::getSalary)
                ));

        System.out.println("\n부서별 평균 급여:");
        avgSalaryByDept.forEach((dept, avg) ->
                System.out.println(dept + ": " + avg)
        );

        Map<String, Map<Boolean, List<Employee>>> complexGrouping =
                employees.stream()
                        .collect(Collectors.groupingBy(
                                Employee::getDepartment,
                                Collectors.partitioningBy(e -> e.getSalary() > 4500)
                        ));
    }

    static class Employee {
        private final String name;
        private final String department;
        private final double salary;

        public Employee(String name, String department, double salary) {
            this.name = name;
            this.department = department;
            this.salary = salary;
        }

        public String getName() { return name; }
        public String getDepartment() { return department; }
        public double getSalary() { return salary; }
    }
}

partitioningBy

import java.util.*;
import java.util.stream.Collectors;

public class PartitioningByExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);

        Map<Boolean, List<Integer>> evenOddPartition = numbers.stream()
                .collect(Collectors.partitioningBy(n -> n % 2 == 0));

        System.out.println("짝수: " + evenOddPartition.get(true));
        System.out.println("홀수: " + evenOddPartition.get(false));
    }
}

병렬 스트림

병렬 스트림은 스트림 연산을 여러 스레드에서 병렬로 나눠 처리해서 속도를 높이는 기능이다.

import java.util.*;

public class ParallelStreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        for (int i = 1; i <= 1_000_000; i++) {
            numbers.add(i);
        }

        long startTime = System.currentTimeMillis();
        long sum1 = numbers.stream()
                .mapToLong(Integer::longValue)
                .sum();
        long sequentialTime = System.currentTimeMillis() - startTime;

        startTime = System.currentTimeMillis();
        long sum2 = numbers.parallelStream()
                .mapToLong(Integer::longValue)
                .sum();
        long parallelTime = System.currentTimeMillis() - startTime;

        System.out.println("순차 처리 시간: " + sequentialTime + "ms");
        System.out.println("병렬 처리 시간: " + parallelTime + "ms");
        System.out.println("결과 동일: " + (sum1 == sum2));
    }
}

느낀점

하루만에 스트림 관련 메서드와 유틸리티들을 빠르게 살펴보니 머리가 좀 아팠다.
그래도 생각보다 어렵진 않고, 메서드들이 어디서 언제 어떻게 사용되는지만 알면 쉽게 사용할 수 있을 것 같았다.

또한 어제 배운 람다식과 연결되어 많이 사용되다 보니까 람다 쪽도 다시 복습해야겠다는 생각이 들었다.

내일은 네트워크 프로그래밍에 들어가는데 전공이 네트워크 쪽이라서 꽤 재밌을 것 같다.

profile
백엔드 공부중입니다.

0개의 댓글