1. 개요

list.stream().filter(...).map(...).collect(...) 를 쓰면서도 이게 내부적으로 for 문 몇 번을 도는 건지 생각해본 적 없는 경우가 많다. 이 글은 스트림의 중간 연산과 최종 연산이 뭔지, 그리고 “지연 평가”라는 말이 실제로 어떻게 동작하는지 눈에 보이는 예제로 정리한다.


2. 핵심 내용

2-1. 중간 연산과 최종 연산

filter, map, sorted, distinct 는 중간 연산이다. 스트림을 반환하고, 그 자체로는 아무 일도 하지 않는다. collect, forEach, count, reduce 는 최종 연산이다. 이게 호출되는 순간에야 앞서 쌓인 중간 연산들이 실제로 실행된다.

List<String> names = List.of("Anna", "Bob", "Charlie", "Dan");
 
Stream<String> stream = names.stream()
    .filter(n -> n.length() > 3)
    .map(String::toUpperCase); // 여기까지는 아무것도 실행 안 됨
 
List<String> result = stream.collect(Collectors.toList()); // 이때 실행됨

2-2. 지연 평가를 눈으로 확인하기

“filter 를 리스트 전체에 돌리고, 그 결과에 map 을 전체에 또 돌린다”고 생각하기 쉽지만 실제로는 아니다. 원소 하나마다 filter → map 을 통과시키고 다음 원소로 넘어간다.

names.stream()
    .filter(n -> { System.out.println("filter: " + n); return n.length() > 3; })
    .map(n -> { System.out.println("map: " + n); return n.toUpperCase(); })
    .forEach(System.out::println);
 
// 출력:
// filter: Anna
// filter: Bob
// filter: Charlie
// map: Charlie
// CHARLIE
// filter: Dan

filter 가 리스트 전체를 먼저 훑고 나서 map 이 시작되는 게 아니라, 조건을 통과한 원소만 그 자리에서 바로 다음 단계로 넘어간다. 이게 for 문과 스트림의 실질적인 차이다. for 문을 여러 겹 쓰면 매 단계마다 전체를 한 번씩 순회하지만, 스트림 파이프라인은 원소 하나당 전체 파이프라인을 한 번에 통과시킨다.

2-3. 스트림은 한 번 쓰고 버린다

최종 연산을 한 번 호출한 스트림은 재사용할 수 없다. 다시 쓰려고 하면 IllegalStateException 이 난다. 매번 list.stream() 을 새로 호출해야 하는 이유다.

2-4. 지연 로딩 컬렉션을 스트림으로 순회할 때

스트림으로 순회하는 컬렉션이 JPA 의 지연 로딩 연관관계라면 얘기가 조금 다르다. orders.stream().map(Order::getCustomerName) 처럼 원소마다 연관 엔티티에 접근하면, 원소 개수만큼 쿼리가 나갈 수 있다. 스트림 문법이 깔끔해서 놓치기 쉬운데 원인은 스트림이 아니라 지연 로딩이다. 이 문제와 해법은 N+1 문제 글에서 다뤘다.

2-5. 그래서 for 문 대신 늘 스트림을 써야 하나

아니다. 단순 순회, 중간에 break/continue 가 필요한 로직, 디버깅이 잦은 코드는 for 문이 더 읽기 쉽고 빠르다. 스트림은 람다 호출 오버헤드가 있고, 숫자 계산에서 오토박싱이 끼면(Stream<Integer>) 성능이 더 떨어진다. 순수 숫자 연산이면 IntStream/LongStream 으로 박싱을 피할 수 있다. 필터링-변환-집계가 체인으로 이어지는 선언적 로직에는 스트림이, 단순 반복에는 for 문이 낫다.


3. 마무리

요약

  • filter/map/sorted 는 중간 연산(지연 실행), collect/forEach/count 는 최종 연산(즉시 실행 트리거).
  • 스트림은 원소별로 파이프라인 전체를 통과시키지, 단계별로 전체를 순회하지 않는다.
  • 스트림은 최종 연산 후 재사용할 수 없다.
  • 지연 로딩 컬렉션을 스트림으로 순회하면 N+1 문제가 그대로 재현될 수 있다.