공공부하자개발 · 영어 학습 노트
자바
고급모던 자바와 성능0/10 완료
  • 01제네릭과 와일드카드
  • 02멀티스레드와 동기화
  • 03람다식과 함수형 인터페이스
  • 04Stream API와 병렬 처리
  • 05Optional로 NPE 방지
  • 06메서드 활용 패턴 (고급)
  • 07Java 21 모던 문법
  • 08어노테이션·리플렉션·동적 프록시
  • 09CompletableFuture 심화와 가상 스레드 실전
  • 10JVM 메모리·GC·OOM 진단
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 고급 › 04 / 10

Stream API와 병렬 처리

섹션 7진행 0 / 10
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

5. 자주 하는 실수 (Tip)

❌ 실수 1: 스트림 재사용

java
Stream<Order> s = orders.stream().filter(Order::paid);
long count = s.count();
List<Order> list = s.toList();   // IllegalStateException: stream has already been operated upon or closed

스트림은 일회성 파이프라인입니다. 최종 연산이 끝나면 소비되어 다시 쓸 수 없습니다.

java
✅
List<Order> paid = orders.stream().filter(Order::paid).toList();   // 결과를 컬렉션으로 받아 재사용
long count = paid.size();
// 또는 Supplier<Stream<Order>> s = () -> orders.stream().filter(Order::paid); 로 매번 새로 생성

❌ 실수 2: toMap 에 merge 함수 없이 사용

java
Map<String, Order> byCustomer = orders.stream().collect(toMap(Order::customer, o -> o));
// 같은 고객이 두 번 나오면 IllegalStateException: Duplicate key

테스트 데이터에선 안 터지고 운영 데이터가 늘어나면 터지는 대표적 시한폭탄입니다.

java
✅
Map<String, Order> latest = orders.stream()
    .collect(toMap(Order::customer, o -> o, (a, b) -> a.date().isAfter(b.date()) ? a : b));
// 또는 groupingBy 로 List 로 받기

❌ 실수 3: 스트림 안에서 외부 변수 변경 (부수효과)

java
List<String> ids = new ArrayList<>();
orders.stream().filter(Order::paid).forEach(o -> ids.add(o.id()));   // 순차에선 되지만 parallel 붙이면 깨짐

스트림 람다는 무상태·비간섭이어야 합니다. forEach 로 외부 컬렉션을 채우는 것은 스트림을 for 문처럼 쓰는 것이고, 병렬화하는 순간 경쟁 조건이 됩니다.

java
✅
List<String> ids = orders.stream().filter(Order::paid).map(Order::id).toList();

❌ 실수 4: Files.lines 를 닫지 않음

java
long errors = Files.lines(path).filter(l -> l.contains("ERROR")).count();   // 파일 핸들 누수

Files.lines 는 파일을 열어둔 채 지연 읽기를 합니다. 스트림을 닫아야 파일이 닫힙니다. 배치에서 수천 파일을 처리하면 "Too many open files" 로 죽습니다.

java
✅
try (Stream<String> lines = Files.lines(path)) {
    long errors = lines.filter(l -> l.contains("ERROR")).count();
}

❌ 실수 5: 무조건 parallelStream()

java
List<String> names = users.parallelStream().map(User::name).toList();   // 요소 50개, 작업은 getter 하나

분할·합치기 비용이 작업보다 크면 순차보다 느립니다. 게다가 공용 풀을 공유하므로 서블릿 스레드 200개가 동시에 병렬 스트림을 쓰면 서로 기다립니다. 병렬은 측정 후, CPU 바운드 대량 데이터에만.

java
✅
List<String> names = users.stream().map(User::name).toList();
// 병렬이 필요하면: 데이터 수만 개 이상 + 요소당 계산 무거움 + I/O 없음 + 벤치마크로 확인

❌ 실수 6: peek 으로 로직 수행

java
orders.stream().peek(o -> o.markProcessed()).count();   // JDK 9+ 에서 count 가 크기를 바로 알면 peek 이 실행 안 될 수 있음

peek 은 디버깅 용도이며 실행이 보장되지 않습니다. 부수효과가 필요하면 forEach 를, 변환이면 map 을 쓰세요.

java
✅
orders.forEach(Order::markProcessed);

❌ 실수 7: Optional 반환 최종 연산에 .get() 바로 호출

java
Order biggest = orders.stream().max(comparingLong(Order::amount)).get();   // 빈 리스트면 NoSuchElementException
java
✅
Order biggest = orders.stream().max(comparingLong(Order::amount))
    .orElseThrow(() -> new IllegalStateException("주문 없음"));
자주 하는 실수 (Tip)
  • ❌ 실수 1: 스트림 재사용
  • ❌ 실수 2: toMap 에 merge 함수 없이 사용
  • ❌ 실수 3: 스트림 안에서 외부 변수 변경 (부수효과)
  • ❌ 실수 4: Files.lines 를 닫지 않음
  • ❌ 실수 5: 무조건 parallelStream()
  • ❌ 실수 6: peek 으로 로직 수행
  • ❌ 실수 7: Optional 반환 최종 연산에 .get() 바로 호출
이전 섹션4 응용 변형 예제5 / 7다음 섹션6 연습 문제