3주차 Unit 10.2 — Stream API

Psj·2026년 5월 20일

F-lab

목록 보기
118/197

Unit 10.2 — Stream API

F-LAB JAVA · 3주차 · Phase 10 · 함수형 프로그래밍


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • Stream 의 정의 와 컬렉션과의 차이는?
  • Stream 의 5가지 특성 (지연 평가, 한 번 사용 등) 은?
  • Stream 생성 방법 (of, stream, iterate, generate, Files.lines 등) 은?
  • 중간 연산 (Intermediate Operation) 의 9가지 핵심은?
  • 최종 연산 (Terminal Operation) 의 10가지 핵심은?
  • 지연 평가 (Lazy Evaluation) 의 메커니즘은?
  • 단락 평가 (Short-circuit) 의 의미는?
  • 병렬 Stream 의 동작과 주의점은?
  • Stream 의 함정 (재사용/null/성능 등) 은?

🎯 핵심 한 문장

Stream API 는 Java 8+ 의 핵심으로, 데이터 소스 (컬렉션, 배열, 파일 등) 에 대한 일련의 연산을 선언적으로 표현 하는 도구다.
컬렉션이 데이터 저장 에 집중한다면, Stream 은 데이터 처리 (변환/필터/집계) 에 집중.
중간 연산 (map, filter, sorted 등) 은 새 Stream 반환 + 지연 평가 (실제 실행 X), 최종 연산 (collect, forEach, reduce 등) 은 결과 반환 + 그 시점에 파이프라인 전체 실행.
한 번만 사용 (재사용 시 IllegalStateException), 원본 변경 X (불변성), 무한 Stream 가능 (iterate, generate).
병렬 Stream (parallelStream) 은 자동 병렬화하지만 공유 상태/순서 의존 작업/작은 데이터 엔 부적합.

비유 — 공장 컨베이어 벨트

컬렉션 (List, Set, Map):
  창고에 물건 보관
  - 데이터 저장
  - 직접 접근 (get, contains)
  - 변경 가능

Stream:
  공장 컨베이어 벨트
  - 물건이 흐름 (데이터의 흐름)
  - 각 작업소에서 가공 (중간 연산)
  - 끝에 최종 처리 (최종 연산)
  - 한 번 흐르면 다시 사용 X

중간 연산:
  벨트 위에서 가공 (페인트칠, 자르기)
  - 새 컨베이어 벨트 반환
  - 실제 작업은 아직 X

최종 연산:
  벨트 끝의 박스 (수집)
  - 결과 만들기
  - 이때 전체 작업 시작

→ Stream = 데이터 처리의 파이프라인.


🧭 9개 섹션 로드맵

1. Stream 의 정의와 5가지 특성
2. Stream 생성 방법
3. 중간 연산 9가지
4. 최종 연산 10가지
5. 지연 평가의 메커니즘
6. 단락 평가 (Short-circuit)
7. 병렬 Stream
8. Stream 의 함정과 실무 패턴
9. 면접 + 자기 점검

1️⃣ Stream 의 정의와 5가지 특성

1.1 Stream 의 정의

Stream:

  데이터의 흐름 (시퀀스) 을 표현하는 객체.
  연산을 파이프라인으로 연결.

Java 8+ (2014) 도입.

본질:
  - 데이터 저장 X
  - 데이터 처리 (변환/필터/집계)
  - 함수형 스타일

1.2 컬렉션 vs Stream

항목컬렉션Stream
본질데이터 저장데이터 처리
외부 반복for, iteratorX
내부 반복X✓ (forEach 등)
평가즉시지연 (Lazy)
재사용✗ (한 번만)
변경가능X (불변)
무한X✓ (iterate, generate)
병렬X✓ (parallelStream)

1.3 기본 예제

List<String> names = List.of("Alice", "Bob", "Charlie", "Dave");

// 전통적 방식 (외부 반복)
List<String> longNames = new ArrayList<>();
for (String name : names) {
    if (name.length() > 3) {
        longNames.add(name.toUpperCase());
    }
}

// Stream 방식 (내부 반복)
List<String> longNames2 = names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .toList();

// 결과: ["ALICE", "CHARLIE", "DAVE"]

// 차이:
// - 전통: HOW (어떻게)
// - Stream: WHAT (무엇을)
// - 선언적, 가독성 ↑

1.4 5가지 특성

1. 데이터 저장 X
   - Stream 은 데이터 보관 X
   - 컬렉션/배열을 처리

2. 함수형 (불변)
   - 원본 변경 X
   - 새 Stream 반환

3. 지연 평가
   - 중간 연산은 즉시 X
   - 최종 연산 호출 시 시작

4. 한 번만 사용
   - 최종 연산 후 닫힘
   - 재사용 → IllegalStateException

5. 무한 가능
   - iterate, generate
   - 단, limit 등으로 유한화

1.5 시각화

Stream 의 파이프라인:

데이터 소스 (List, Set, 배열, ...)
   ↓
.stream()
   ↓
.filter(predicate)   ← 중간 연산 (새 Stream)
   ↓
.map(function)        ← 중간 연산 (새 Stream)
   ↓
.sorted()              ← 중간 연산 (새 Stream)
   ↓
.collect(Collectors.toList())   ← 최종 연산 (실행 + 결과)
   ↓
결과

특징:
  - 중간 연산: 새 Stream
  - 최종 연산: 실행 + 결과
  - 지연 평가

1.6 ILIC 활용

public class ShipmentStreamBasics {
    
    private final List<Shipment> shipments = new ArrayList<>();
    
    // 1. 필터링 + 변환
    public List<String> getUrgentBlNos() {
        return shipments.stream()
            .filter(Shipment::isUrgent)
            .map(Shipment::getBlNo)
            .toList();
    }
    
    // 2. 집계
    public BigDecimal totalWeight() {
        return shipments.stream()
            .map(Shipment::getWeight)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    // 3. 카운트
    public long countActive() {
        return shipments.stream()
            .filter(Shipment::isActive)
            .count();
    }
}

1.7 자기 점검 답변

Stream 의 정의와 특성은?

:
1. 정의:

  • 데이터의 흐름
  • 처리 파이프라인
  • 함수형 스타일
  1. 5가지 특성:

    • 데이터 저장 X
    • 함수형 (불변)
    • 지연 평가
    • 한 번만 사용
    • 무한 가능
  2. 컬렉션과 차이:

    • 컬렉션: 저장
    • Stream: 처리
    • 컬렉션: 즉시
    • Stream: 지연
  3. 연산:

    • 중간 연산 → 새 Stream
    • 최종 연산 → 결과

2️⃣ Stream 생성 방법

2.1 컬렉션에서

// List, Set, Queue 등
List<String> list = List.of("A", "B", "C");
Stream<String> s1 = list.stream();

Set<Integer> set = Set.of(1, 2, 3);
Stream<Integer> s2 = set.stream();

// 병렬 Stream
Stream<String> s3 = list.parallelStream();

// Map 은 직접 Stream X
Map<String, Integer> map = Map.of("a", 1, "b", 2);
// map.stream();   // ❌
map.entrySet().stream();   // ✓
map.keySet().stream();
map.values().stream();

2.2 Stream.of

// 고정된 값
Stream<String> s1 = Stream.of("A", "B", "C");

// 배열로부터
String[] arr = {"X", "Y", "Z"};
Stream<String> s2 = Stream.of(arr);
// 또는
Stream<String> s3 = Arrays.stream(arr);

// 원시 타입 배열
int[] ints = {1, 2, 3};
IntStream s4 = Arrays.stream(ints);
IntStream s5 = IntStream.of(1, 2, 3);

// 빈 Stream
Stream<String> empty = Stream.empty();

2.3 Stream.iterate

// 무한 Stream — iterate
Stream<Integer> infinite = Stream.iterate(0, n -> n + 1);
// 0, 1, 2, 3, ...

// limit 으로 유한화
List<Integer> first10 = Stream.iterate(0, n -> n + 1)
    .limit(10)
    .toList();
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

// Java 9+ — 종료 조건
List<Integer> nums = Stream.iterate(0, n -> n < 100, n -> n + 2)
    .toList();
// [0, 2, 4, ..., 98]
// 무한 X (종료 조건 있음)

// 활용
Stream<LocalDate> dates = Stream.iterate(
    LocalDate.now(),
    d -> d.plusDays(1));
// 오늘, 내일, 모레, ...

2.4 Stream.generate

// 무한 Stream — generate (Supplier)
Stream<Double> randoms = Stream.generate(Math::random);
// 0.x, 0.y, 0.z, ... (무한)

// limit 필수 (무한)
List<Double> ten = Stream.generate(Math::random)
    .limit(10)
    .toList();

// 고정 값
Stream<String> hellos = Stream.generate(() -> "Hello")
    .limit(5);
// "Hello", "Hello", "Hello", "Hello", "Hello"

// UUID
List<UUID> ids = Stream.generate(UUID::randomUUID)
    .limit(100)
    .toList();

2.5 IntStream.range, IntStream.rangeClosed

// IntStream.range — 시작 포함, 끝 제외
IntStream.range(0, 10);   // 0, 1, 2, ..., 9

// IntStream.rangeClosed — 시작 + 끝 모두 포함
IntStream.rangeClosed(0, 10);   // 0, 1, 2, ..., 10

// 활용 — 인덱스 + 값
List<String> list = List.of("A", "B", "C");
IntStream.range(0, list.size())
    .forEach(i -> System.out.println(i + ": " + list.get(i)));
// 0: A
// 1: B
// 2: C

// 합계
int sum = IntStream.range(1, 101).sum();   // 1 + ... + 100 = 5050

2.6 파일/문자열

// 파일의 줄 단위
try (Stream<String> lines = Files.lines(Path.of("file.txt"), UTF_8)) {
    lines.forEach(System.out::println);
}

// 디렉토리
try (Stream<Path> paths = Files.list(Path.of("/var"))) {
    paths.forEach(System.out::println);
}

// 디렉토리 트리 (재귀)
try (Stream<Path> paths = Files.walk(Path.of("/var"))) {
    paths.filter(Files::isRegularFile)
        .forEach(System.out::println);
}

// 문자열 분할
"a,b,c".chars();   // IntStream (each char)
Stream<String> parts = Pattern.compile(",").splitAsStream("a,b,c");
// "a", "b", "c"

2.7 빌더

// Stream.Builder
Stream<String> s = Stream.<String>builder()
    .add("A")
    .add("B")
    .add("C")
    .build();

// 활용 - 조건부 추가
Stream.Builder<String> builder = Stream.builder();
if (cond1) builder.add("A");
if (cond2) builder.add("B");
Stream<String> stream = builder.build();

2.8 ILIC 활용

public class ShipmentStreamCreation {
    
    // 1. 컬렉션
    public Stream<Shipment> all() {
        return shipments.stream();
    }
    
    // 2. 고정 값
    public Stream<String> defaultStatuses() {
        return Stream.of("PENDING", "SHIPPED", "DELIVERED");
    }
    
    // 3. 무한 + limit
    public Stream<LocalDate> next30Days() {
        return Stream.iterate(LocalDate.now(), d -> d.plusDays(1))
            .limit(30);
    }
    
    // 4. ID 생성
    public Stream<UUID> generateBatchIds(int count) {
        return Stream.generate(UUID::randomUUID)
            .limit(count);
    }
    
    // 5. 인덱스 활용
    public Stream<IndexedShipment> indexed(List<Shipment> list) {
        return IntStream.range(0, list.size())
            .mapToObj(i -> new IndexedShipment(i, list.get(i)));
    }
    
    // 6. 파일에서
    public Stream<Shipment> loadFromFile(Path path) throws IOException {
        return Files.lines(path, StandardCharsets.UTF_8)
            .skip(1)   // 헤더
            .map(this::parseShipment);
    }
    
    record IndexedShipment(int index, Shipment shipment) {}
}

2.9 자기 점검 답변

Stream 생성 방법은?

:
1. 컬렉션:

  • .stream(), .parallelStream()
  • Map 은 entrySet/keySet/values
  1. 고정 값:

    • Stream.of(...)
    • Arrays.stream(arr)
  2. 무한:

    • Stream.iterate(seed, fn)
    • Stream.generate(Supplier)
    • limit 필수
  3. IntStream:

    • range(start, end) — end 제외
    • rangeClosed(start, end) — 모두 포함
  4. 파일:

    • Files.lines(path)
    • Files.list/walk

3️⃣ 중간 연산 9가지

3.1 중간 연산의 특징

중간 연산 (Intermediate Operation):

  - 새 Stream 반환
  - 지연 평가 (lazy)
  - 체이닝 가능
  - 최종 연산 호출 전엔 실행 X

9가지 핵심:
  1. filter — 조건 만족만
  2. map — 변환
  3. flatMap — 평탄화
  4. distinct — 중복 제거
  5. sorted — 정렬
  6. limit — 개수 제한
  7. skip — 처음 N개 건너뛰기
  8. peek — 엿보기 (디버깅)
  9. mapToInt/Long/Double — 기본 타입 변환

3.2 filter — 조건

// Predicate 로 필터
Stream<T> filter(Predicate<? super T> predicate);

// 활용
List<Integer> evens = numbers.stream()
    .filter(n -> n % 2 == 0)
    .toList();

// 여러 조건
List<Shipment> urgent = shipments.stream()
    .filter(s -> s.isActive())
    .filter(s -> s.isUrgent())
    .toList();
// 또는
.filter(s -> s.isActive() && s.isUrgent())

3.3 map — 변환

// Function 으로 변환
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

// 활용
List<String> upper = names.stream()
    .map(String::toUpperCase)
    .toList();

// 객체 → 필드
List<String> blNos = shipments.stream()
    .map(Shipment::getBlNo)
    .toList();

// 변환 체이닝
List<Integer> lengths = names.stream()
    .map(String::trim)
    .map(String::length)
    .toList();

3.4 flatMap — 평탄화

// 각 요소를 Stream 으로 변환 + 평탄화
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

// 활용 — 중첩 컬렉션
List<List<Integer>> nested = List.of(
    List.of(1, 2, 3),
    List.of(4, 5, 6),
    List.of(7, 8, 9));

List<Integer> flat = nested.stream()
    .flatMap(List::stream)
    .toList();
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

// vs map (잘못된 결과)
List<Stream<Integer>> wrong = nested.stream()
    .map(List::stream)
    .toList();
// Stream 의 Stream — 평탄화 X

// 문자열 → 단어
List<String> sentences = List.of("Hello World", "Goodbye Cruel World");
List<String> words = sentences.stream()
    .flatMap(s -> Arrays.stream(s.split(" ")))
    .toList();
// [Hello, World, Goodbye, Cruel, World]

// Shipment 의 items
List<ShipmentItem> allItems = shipments.stream()
    .flatMap(s -> s.getItems().stream())
    .toList();

3.5 distinct — 중복 제거

// equals 기반 중복 제거
Stream<T> distinct();

// 활용
List<Integer> unique = Stream.of(1, 2, 2, 3, 3, 3, 4)
    .distinct()
    .toList();
// [1, 2, 3, 4]

// 객체 (equals/hashCode 필요)
List<Shipment> uniqueShipments = shipments.stream()
    .distinct()   // Shipment 의 equals/hashCode 사용
    .toList();

// 특정 필드 기준 중복 제거
List<String> uniqueBlNos = shipments.stream()
    .map(Shipment::getBlNo)
    .distinct()
    .toList();

3.6 sorted — 정렬

// 자연 순서
Stream<T> sorted();   // Comparable 필요

// 사용자 정의
Stream<T> sorted(Comparator<? super T> comparator);

// 활용
List<Integer> sorted = Stream.of(3, 1, 4, 1, 5, 9, 2)
    .sorted()
    .toList();
// [1, 1, 2, 3, 4, 5, 9]

// 객체 정렬
List<Shipment> byWeight = shipments.stream()
    .sorted(Comparator.comparing(Shipment::getWeight))
    .toList();

// 역순
List<Shipment> byWeightDesc = shipments.stream()
    .sorted(Comparator.comparing(Shipment::getWeight).reversed())
    .toList();

// 다중 기준
List<Shipment> sorted = shipments.stream()
    .sorted(Comparator.comparing(Shipment::isUrgent).reversed()
        .thenComparing(Shipment::getWeight))
    .toList();

3.7 limit, skip

// limit — 처음 N개
Stream<T> limit(long maxSize);

// skip — 처음 N개 건너뛰기
Stream<T> skip(long n);

// 활용 — 페이지네이션
int pageSize = 10;
int pageNumber = 3;
List<Shipment> page = shipments.stream()
    .skip(pageSize * pageNumber)
    .limit(pageSize)
    .toList();

// 상위 10개
List<Shipment> top10 = shipments.stream()
    .sorted(Comparator.comparing(Shipment::getWeight).reversed())
    .limit(10)
    .toList();

// 무한 + limit
List<Integer> first100 = Stream.iterate(0, n -> n + 1)
    .limit(100)
    .toList();

3.8 peek — 엿보기

// 디버깅용 — Consumer 호출 + 통과
Stream<T> peek(Consumer<? super T> action);

// 활용 — 중간 로그
List<Shipment> result = shipments.stream()
    .peek(s -> log.debug("Before filter: {}", s.getId()))
    .filter(Shipment::isActive)
    .peek(s -> log.debug("After filter: {}", s.getId()))
    .map(Shipment::getBlNo)
    .peek(blNo -> log.debug("Mapped: {}", blNo))
    .toList();

// 주의:
// - 부수 효과 (side effect) 가능
// - 일반적으로 디버깅만 권장
// - 비즈니스 로직에 사용 X

3.9 mapToInt, mapToLong, mapToDouble

// 기본 타입 Stream 으로 변환 (boxing 회피)
IntStream mapToInt(ToIntFunction<? super T> mapper);
LongStream mapToLong(ToLongFunction<? super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);

// 활용 — 합계
int totalLength = names.stream()
    .mapToInt(String::length)
    .sum();
// IntStream 의 sum (boxing X)

// 평균
OptionalDouble avgWeight = shipments.stream()
    .mapToDouble(s -> s.getWeight().doubleValue())
    .average();

// 통계
IntSummaryStatistics stats = numbers.stream()
    .mapToInt(Integer::intValue)
    .summaryStatistics();

stats.getCount();
stats.getSum();
stats.getMin();
stats.getMax();
stats.getAverage();

3.10 ILIC 활용

public class ShipmentIntermediate {
    
    // 종합 활용
    public List<String> findTopActiveBlNos(int limit) {
        return shipments.stream()
            .filter(Shipment::isActive)           // 활성만
            .filter(s -> s.getWeight()             // 중량 > 100
                .compareTo(BigDecimal.valueOf(100)) > 0)
            .sorted(Comparator.comparing           // 무게 내림차순
                (Shipment::getWeight).reversed())
            .limit(limit)                          // 상위 limit 개
            .map(Shipment::getBlNo)                // BL 번호만
            .distinct()                             // 중복 제거
            .toList();
    }
    
    // 통계
    public double averageWeight() {
        return shipments.stream()
            .filter(Shipment::isActive)
            .mapToDouble(s -> s.getWeight().doubleValue())
            .average()
            .orElse(0.0);
    }
    
    // 평탄화
    public List<ShipmentItem> allActiveItems() {
        return shipments.stream()
            .filter(Shipment::isActive)
            .flatMap(s -> s.getItems().stream())
            .toList();
    }
}

3.11 자기 점검 답변

중간 연산 9가지는?

:
1. filter: Predicate 로 필터
2. map: Function 으로 변환
3. flatMap: 평탄화 (중첩 풀기)
4. distinct: 중복 제거
5. sorted: 정렬
6. limit: 개수 제한
7. skip: 건너뛰기
8. peek: 엿보기 (디버깅)
9. mapToInt/Long/Double: 기본 타입 변환

모두 새 Stream 반환, 지연 평가


4️⃣ 최종 연산 10가지

4.1 최종 연산의 특징

최종 연산 (Terminal Operation):

  - Stream 닫음
  - 결과 반환 (Stream X)
  - 파이프라인 전체 실행
  - 한 번만 가능

10가지 핵심:
  1. forEach — 각 요소에 동작
  2. toArray — 배열
  3. toList — List (Java 16+)
  4. collect — Collector 로 수집
  5. reduce — 축약
  6. count — 개수
  7. min/max — 최소/최대
  8. anyMatch/allMatch/noneMatch — 매칭
  9. findFirst/findAny — 찾기
  10. forEachOrdered — 순서 보장 forEach

4.2 forEach, forEachOrdered

// forEach — 각 요소에 Consumer
void forEach(Consumer<? super T> action);

// 활용
shipments.stream()
    .filter(Shipment::isActive)
    .forEach(s -> log.info("Active: {}", s.getId()));

// 병렬에서 순서 보장 X
shipments.parallelStream().forEach(System.out::println);
// 출력 순서 무작위

// forEachOrdered — 순서 보장 (병렬에서)
shipments.parallelStream().forEachOrdered(System.out::println);
// 입력 순서로 출력 (병렬 효과 ↓)

4.3 toArray, toList

// toArray
Object[] arr = shipments.stream().toArray();
Shipment[] shipArr = shipments.stream().toArray(Shipment[]::new);

// toList (Java 16+) — 불변 List
List<Shipment> list = shipments.stream()
    .filter(Shipment::isActive)
    .toList();
// list 는 unmodifiable

// 또는 collect (Java 8+)
List<Shipment> list2 = shipments.stream()
    .filter(Shipment::isActive)
    .collect(Collectors.toList());
// list2 는 modifiable (ArrayList)

// 차이:
// - toList(): 불변, Java 16+
// - Collectors.toList(): 가변, Java 8+

4.4 collect — Collector

// collect 는 다양한 컬렉터 사용
<R, A> R collect(Collector<? super T, A, R> collector);

// 자주 사용:
.collect(Collectors.toList());
.collect(Collectors.toSet());
.collect(Collectors.toMap(keyFn, valueFn));
.collect(Collectors.joining(", "));
.collect(Collectors.groupingBy(classifier));
.collect(Collectors.counting());
.collect(Collectors.summingInt(...));
.collect(Collectors.averagingDouble(...));

// 다음 Unit 10.3 에서 정밀

4.5 reduce — 축약

// 1. identity + 누적 함수
T reduce(T identity, BinaryOperator<T> accumulator);

// 2. 누적 함수만 (Optional)
Optional<T> reduce(BinaryOperator<T> accumulator);

// 3. 다른 타입으로 축약 (병렬)
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

// 활용
int sum = numbers.stream().reduce(0, Integer::sum);
int sum2 = numbers.stream().reduce(0, (a, b) -> a + b);

// Optional
Optional<Integer> max = numbers.stream().reduce(Integer::max);

// 문자열
String joined = words.stream().reduce("", String::concat);

// BigDecimal
BigDecimal total = shipments.stream()
    .map(Shipment::getWeight)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

4.6 count, min, max

// count
long count = shipments.stream()
    .filter(Shipment::isActive)
    .count();

// min, max — Optional 반환
Optional<Shipment> heaviest = shipments.stream()
    .max(Comparator.comparing(Shipment::getWeight));

Optional<Shipment> lightest = shipments.stream()
    .min(Comparator.comparing(Shipment::getWeight));

// IntStream 의 경우
OptionalInt maxLen = names.stream()
    .mapToInt(String::length)
    .max();

4.7 anyMatch, allMatch, noneMatch

// 매칭 검사 (boolean 반환)
boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);

// 활용
boolean hasUrgent = shipments.stream()
    .anyMatch(Shipment::isUrgent);

boolean allActive = shipments.stream()
    .allMatch(Shipment::isActive);

boolean noErrors = shipments.stream()
    .noneMatch(s -> s.hasError());

// 단락 평가 (Short-circuit)
// - anyMatch: 첫 매칭 시 즉시 true
// - allMatch: 첫 불일치 시 즉시 false
// - noneMatch: 첫 매칭 시 즉시 false

4.8 findFirst, findAny

// 첫 번째 또는 임의 요소
Optional<T> findFirst();
Optional<T> findAny();

// 활용
Optional<Shipment> first = shipments.stream()
    .filter(Shipment::isUrgent)
    .findFirst();

// 병렬에서:
// - findFirst: 첫 번째 (느릴 수 있음)
// - findAny: 아무거나 (빠름)

// findFirst 활용
shipments.stream()
    .filter(s -> s.getBlNo().equals("ABC123"))
    .findFirst()
    .ifPresent(s -> log.info("Found: {}", s));

// orElse
Shipment found = shipments.stream()
    .filter(s -> s.getId().equals(1L))
    .findFirst()
    .orElse(null);

4.9 forEach vs collect

forEach:
  - 부수 효과 (side effect) 가 목적
  - 외부 상태 변경 가능 (비권장)
  - 결과 X (void)

collect:
  - 결과 반환
  - 함수형 (부수 효과 X)
  - 권장 패턴

비권장:
List<String> result = new ArrayList<>();
shipments.stream()
    .forEach(s -> result.add(s.getBlNo()));   // 부수 효과

권장:
List<String> result = shipments.stream()
    .map(Shipment::getBlNo)
    .toList();

4.10 ILIC 활용

public class ShipmentTerminalOps {
    
    private final List<Shipment> shipments = new ArrayList<>();
    
    // 1. 카운트
    public long countActive() {
        return shipments.stream()
            .filter(Shipment::isActive)
            .count();
    }
    
    // 2. 합계 (reduce)
    public BigDecimal totalWeight() {
        return shipments.stream()
            .map(Shipment::getWeight)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    // 3. 최대
    public Optional<Shipment> heaviest() {
        return shipments.stream()
            .max(Comparator.comparing(Shipment::getWeight));
    }
    
    // 4. 매칭
    public boolean hasOverdue() {
        return shipments.stream()
            .anyMatch(this::isOverdue);
    }
    
    // 5. 찾기
    public Optional<Shipment> findByBlNo(String blNo) {
        return shipments.stream()
            .filter(s -> s.getBlNo().equals(blNo))
            .findFirst();
    }
    
    // 6. forEach
    public void logAll() {
        shipments.stream()
            .filter(Shipment::isActive)
            .forEach(s -> log.info("{}: {}", s.getId(), s.getBlNo()));
    }
    
    // 7. 수집
    public List<String> activeBlNos() {
        return shipments.stream()
            .filter(Shipment::isActive)
            .map(Shipment::getBlNo)
            .toList();
    }
    
    private boolean isOverdue(Shipment s) {
        return s.getDueDate().isBefore(LocalDate.now());
    }
}

4.11 자기 점검 답변

최종 연산 10가지는?

:
1. forEach: Consumer 실행
2. forEachOrdered: 순서 보장
3. toArray: 배열
4. toList (Java 16+): 불변 List
5. collect: Collector
6. reduce: 축약
7. count: 개수
8. min/max: Optional
9. anyMatch/allMatch/noneMatch: boolean
10. findFirst/findAny: Optional

특징:

  • 파이프라인 실행
  • 한 번만
  • 결과 반환

5️⃣ 지연 평가의 메커니즘

5.1 지연 평가란

지연 평가 (Lazy Evaluation):

  중간 연산은 즉시 실행 X
  최종 연산 호출 시점에 실행.

이유:
  - 불필요한 계산 회피
  - 단락 평가 가능
  - 무한 Stream 가능

증명:
  Stream<T> stream = data.stream()
      .filter(predicate)   // 실행 X
      .map(function);       // 실행 X
  // 아직 아무것도 실행 안 됨
  
  stream.count();   // ★ 이제 모든 중간 연산 실행

5.2 시각화

// 코드
shipments.stream()
    .filter(s -> {
        System.out.println("Filter: " + s.getId());
        return s.isActive();
    })
    .map(s -> {
        System.out.println("Map: " + s.getId());
        return s.getBlNo();
    })
    .findFirst();

// 데이터: 100개 Shipment
// 첫 active 한 게 5번째라면:

// 출력:
// Filter: 1
// Filter: 2
// Filter: 3
// Filter: 4
// Filter: 5 (active!)
// Map: 5
// (findFirst 종료)

// 6~100 은 처리 X
// 지연 평가의 효과

5.3 vs 전통 방식

// 전통 — 각 단계 완료 후 다음
List<Shipment> filtered = new ArrayList<>();
for (Shipment s : shipments) {
    if (s.isActive()) filtered.add(s);
}
// filter 완료 (100번)

List<String> mapped = new ArrayList<>();
for (Shipment s : filtered) {
    mapped.add(s.getBlNo());
}
// map 완료 (active 만큼)

String first = mapped.get(0);
// findFirst (1번)

// 비효율 — 모든 단계 완료

// Stream 의 지연 평가
shipments.stream()
    .filter(Shipment::isActive)
    .map(Shipment::getBlNo)
    .findFirst();
// 각 요소가 파이프라인을 흐름
// 첫 매칭 시 즉시 종료

5.4 무한 Stream 의 가능성

// 지연 평가 덕에 무한도 OK
Stream<Integer> infinite = Stream.iterate(1, n -> n + 1);

List<Integer> first10 = infinite
    .filter(n -> n % 2 == 0)
    .limit(10)
    .toList();
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

// 10개만 필요 → 무한 Stream 도 종료
// limit 가 단락 평가

5.5 함정 — 부수 효과

// peek 의 함정
List<Integer> nums = new ArrayList<>();

List<Integer> result = Stream.of(1, 2, 3, 4, 5)
    .peek(nums::add)   // 부수 효과
    .filter(n -> n > 2)
    .toList();

// nums 의 내용은? 
// → [1, 2, 3, 4, 5] (모두 추가됨)
// peek 가 모든 요소에 호출

// 하지만 단락 평가:
List<Integer> result2 = Stream.of(1, 2, 3, 4, 5)
    .peek(nums::add)
    .filter(n -> n > 2)
    .findFirst();

// nums 의 내용?
// → [1, 2, 3] (3 에서 findFirst 종료)
// 4, 5 는 처리 X

// 지연 평가의 정확한 이해 필요

5.6 fusing — 연산 융합

fusing:

  여러 중간 연산을 한 번의 반복으로 처리.

예:
  .filter(p1).filter(p2).map(f).filter(p3)
  
  내부:
    for each element:
      if (!p1(e)) continue;
      if (!p2(e)) continue;
      m = f(e);
      if (!p3(m)) continue;
      // ...
  
  한 번의 반복으로 모든 연산
  성능 ↑

5.7 단락 평가의 효과

// 첫 매칭만 필요
Stream.iterate(0, n -> n + 1)   // 무한
    .filter(n -> n > 1000000)
    .findFirst();
// 1000001 에서 종료
// 무한 Stream 이지만 종료

// limit + 무한
Stream.generate(Math::random)
    .filter(d -> d > 0.99)
    .limit(5)
    .toList();
// 약 500번 generate 후 5개 발견 → 종료

5.8 ILIC 활용

public class LazyEvaluationExamples {
    
    // 1. 효율적 검색
    public Optional<Shipment> findFirstUrgent() {
        return shipments.stream()
            .filter(Shipment::isUrgent)
            .findFirst();
        // 첫 urgent 찾으면 종료
    }
    
    // 2. 매칭 확인
    public boolean hasInvalid() {
        return shipments.stream()
            .anyMatch(s -> s.getWeight().compareTo(BigDecimal.ZERO) <= 0);
        // 첫 invalid 시 true 반환
    }
    
    // 3. 무한 스트림
    public List<LocalDate> next30BusinessDays() {
        return Stream.iterate(LocalDate.now(), d -> d.plusDays(1))
            .filter(d -> d.getDayOfWeek() != DayOfWeek.SATURDAY
                && d.getDayOfWeek() != DayOfWeek.SUNDAY)
            .limit(30)
            .toList();
        // 30개 영업일만 (limit 가 단락)
    }
    
    // 4. 페이지네이션 (skip + limit)
    public List<Shipment> getPage(int page, int size) {
        return shipments.stream()
            .skip((long) page * size)
            .limit(size)
            .toList();
        // 필요한 만큼만 처리
    }
}

5.9 자기 점검 답변

지연 평가의 메커니즘은?

:
1. 정의:

  • 중간 연산 즉시 실행 X
  • 최종 연산 시점에 실행
  1. 이유:

    • 불필요한 계산 회피
    • 단락 평가
    • 무한 Stream 가능
  2. 시각화:

    • 각 요소가 파이프라인 흐름
    • findFirst 시 즉시 종료
  3. fusing:

    • 여러 중간 연산 한 번에
    • 성능 ↑
  4. 함정:

    • 부수 효과 (peek)
    • 정확한 실행 횟수 예측 어려움

6️⃣ 단락 평가 (Short-circuit)

6.1 단락 평가의 정의

단락 평가 (Short-circuit Evaluation):

  결과 결정 시 즉시 종료.
  더 처리할 필요 X.

지원 연산:
  중간:
    - limit
    - takeWhile (Java 9+)
    - dropWhile (Java 9+)
  
  최종:
    - findFirst, findAny
    - anyMatch, allMatch, noneMatch

6.2 anyMatch — 첫 매칭에 즉시

List<Integer> nums = Stream.iterate(1, n -> n + 1)
    .limit(1_000_000)
    .toList();

boolean hasNegative = nums.stream()
    .anyMatch(n -> n < 0);
// false
// 모든 요소 확인 (음수 없음)

// 매칭 있으면 즉시 종료
boolean hasMillion = nums.stream()
    .anyMatch(n -> n == 1_000_000);
// true
// 999_999 까지만 확인 (마지막에서)

boolean hasOne = nums.stream()
    .anyMatch(n -> n == 1);
// true
// 1 번째에서 즉시 종료

6.3 limit — N개 도달 시

// 무한 Stream + limit
List<Integer> first5 = Stream.iterate(0, n -> n + 1)
    .filter(n -> n % 2 == 0)
    .limit(5)
    .toList();
// [0, 2, 4, 6, 8]
// 5개 도달 시 즉시 종료

6.4 takeWhile, dropWhile (Java 9+)

// takeWhile — 조건 참 동안 (false 만나면 종료)
List<Integer> nums = List.of(1, 2, 3, 4, 5, 6, 1, 2);
List<Integer> taken = nums.stream()
    .takeWhile(n -> n < 4)
    .toList();
// [1, 2, 3]
// 4 만나면 종료 (그 이후 1, 2 도 무시)

// dropWhile — 조건 참 동안 건너뜀 (false 만나면 그 이후 모두)
List<Integer> dropped = nums.stream()
    .dropWhile(n -> n < 4)
    .toList();
// [4, 5, 6, 1, 2]
// 4 부터 모두 (이후 작은 값도 포함)

// 차이:
// - filter: 모든 요소 확인
// - takeWhile/dropWhile: 첫 false 에서 결정

6.5 findFirst, findAny

// findFirst — 첫 번째
Optional<Shipment> first = shipments.stream()
    .filter(Shipment::isUrgent)
    .findFirst();

// findAny — 임의 (병렬에서 빠름)
Optional<Shipment> any = shipments.stream()
    .filter(Shipment::isUrgent)
    .findAny();

// 둘 다 단락 평가:
// - 매칭 발견 시 즉시 종료
// - 무한 Stream 도 OK

// 차이:
// - findFirst: 순서 보장 (느릴 수 있음)
// - findAny: 빠름 (병렬에서)

6.6 allMatch — 첫 불일치에 즉시

boolean allActive = shipments.stream()
    .allMatch(Shipment::isActive);

// 동작:
// 1. 첫 요소 — active 면 계속
// 2. inactive 발견 → 즉시 false
// 3. 나머지 처리 X

// 모두 active 면 끝까지 확인

6.7 noneMatch — 첫 매칭에 즉시 false

boolean noErrors = shipments.stream()
    .noneMatch(Shipment::hasError);

// 동작:
// 1. 매칭 (에러) 발견 → 즉시 false
// 2. 나머지 처리 X

// 매칭 없으면 끝까지 확인

6.8 단락 평가의 함정

// 함정 — 부수 효과 있을 때
List<Integer> processed = new ArrayList<>();

boolean any = numbers.stream()
    .peek(processed::add)
    .anyMatch(n -> n > 100);

// processed 에 몇 개?
// → 첫 매칭까지의 개수
// 매칭 없으면 모든 요소
// 정확한 수 예측 어려움

// 권장: 부수 효과 안 쓰기

6.9 ILIC 활용

public class ShortCircuitExamples {
    
    // 1. 빠른 검색
    public Optional<Shipment> findUrgent() {
        return shipments.stream()
            .filter(Shipment::isUrgent)
            .findFirst();
        // 첫 urgent 시 종료
    }
    
    // 2. 검증
    public boolean hasInvalid() {
        return shipments.stream()
            .anyMatch(s -> s.getWeight().compareTo(BigDecimal.ZERO) <= 0);
        // 첫 invalid 시 종료
    }
    
    public boolean allValid() {
        return shipments.stream()
            .allMatch(s -> s.getWeight().compareTo(BigDecimal.ZERO) > 0);
        // 첫 invalid 시 종료
    }
    
    // 3. 정렬된 데이터 활용
    public List<Shipment> getFirstActive(int count) {
        return shipments.stream()
            .filter(Shipment::isActive)
            .limit(count)
            .toList();
        // count 개 도달 시 종료
    }
    
    // 4. takeWhile (Java 9+)
    public List<Shipment> getUntilFirstInactive() {
        return shipments.stream()
            .takeWhile(Shipment::isActive)
            .toList();
        // 첫 inactive 만나면 종료
    }
}

6.10 자기 점검 답변

단락 평가의 의미는?

:
1. 정의:

  • 결과 결정 시 즉시 종료
  • 추가 처리 X
  1. 지원 연산:

    • 중간: limit, takeWhile, dropWhile
    • 최종: anyMatch, allMatch, noneMatch, findFirst, findAny
  2. 효과:

    • 성능 ↑
    • 무한 Stream 가능
    • 빠른 검색
  3. 함정:

    • 부수 효과의 정확한 수 예측 어려움
  4. takeWhile vs filter:

    • takeWhile: 첫 false 에서 종료
    • filter: 모두 확인

7️⃣ 병렬 Stream

7.1 병렬 Stream 의 정의

병렬 Stream (Parallel Stream):

  여러 스레드로 동시에 처리하는 Stream.

생성:
  - collection.parallelStream()
  - stream.parallel()

내부:
  - Fork/Join Framework
  - ForkJoinPool.commonPool() 사용
  - 기본 스레드 수: CPU 코어 수

7.2 기본 사용

// 순차
long sum1 = numbers.stream()
    .mapToLong(Integer::longValue)
    .sum();

// 병렬
long sum2 = numbers.parallelStream()
    .mapToLong(Integer::longValue)
    .sum();

// 또는
long sum3 = numbers.stream()
    .parallel()   // 병렬화
    .mapToLong(Integer::longValue)
    .sum();

// 다시 순차
.sequential();

7.3 동작 메커니즘

병렬 Stream 의 동작:

1. 데이터를 청크 (chunk) 로 분할
   - Spliterator 활용
2. 각 청크를 별도 스레드에서 처리
   - ForkJoinPool 의 worker 스레드
3. 결과 병합 (combiner)
   - reduce 등

분할:
  - List: 균등 분할
  - LinkedList: 분할 비효율
  - Set: 보통 균등
  - 무한 Stream: 분할 어려움

7.4 성능 — 언제 빠른가

병렬이 빠른 경우:

1. 큰 데이터
   - 작은 데이터는 오버헤드 ↑

2. 무거운 연산
   - CPU 작업
   - 각 요소 처리 비용 ↑

3. ArrayList, IntStream 같은 분할 쉬운 소스

4. 부수 효과 없음
   - 동시성 안전

5. 순서 의존성 없음
   - findAny, anyMatch 등

병렬이 느린 경우:
  - 작은 데이터
  - 가벼운 연산
  - LinkedList
  - 순서 의존
  - 동기화 비용 큰 작업

7.5 벤치마크

// 1억 개 합계
List<Integer> nums = IntStream.range(0, 100_000_000).boxed().toList();

// 순차
long start = System.nanoTime();
long sum1 = nums.stream()
    .mapToLong(Integer::longValue)
    .sum();
long t1 = System.nanoTime() - start;

// 병렬
start = System.nanoTime();
long sum2 = nums.parallelStream()
    .mapToLong(Integer::longValue)
    .sum();
long t2 = System.nanoTime() - start;

// 예상 (8 코어):
// 순차: ~500ms
// 병렬: ~120ms
// 약 4배 빠름

// 작은 데이터 (1000개):
// 순차: ~0.01ms
// 병렬: ~0.5ms
// 병렬이 더 느림 (오버헤드)

7.6 동시성 문제

// ❌ 위험 — 공유 가변 상태
List<String> result = new ArrayList<>();   // not thread-safe
shipments.parallelStream()
    .map(Shipment::getBlNo)
    .forEach(result::add);   // race condition

// ✓ 안전 — collect
List<String> result2 = shipments.parallelStream()
    .map(Shipment::getBlNo)
    .toList();

// ✓ 안전 — synchronized list
List<String> result3 = Collections.synchronizedList(new ArrayList<>());
shipments.parallelStream()
    .map(Shipment::getBlNo)
    .forEach(result3::add);
// 가능하지만 성능 ↓ (락 경합)

7.7 순서 의존성

// 순서 보장 X 인 작업이 병렬 효율
shipments.parallelStream()
    .forEach(System.out::println);
// 출력 순서 무작위

// 순서 필요시
shipments.parallelStream()
    .forEachOrdered(System.out::println);
// 순서 보장 (병렬 효과 ↓)

// findFirst vs findAny
shipments.parallelStream()
    .filter(Shipment::isUrgent)
    .findFirst();
// 첫 번째 (순서 의존, 느릴 수 있음)

shipments.parallelStream()
    .filter(Shipment::isUrgent)
    .findAny();
// 아무거나 (빠름)

7.8 ForkJoinPool 의 커스터마이징

// 기본은 ForkJoinPool.commonPool() 사용
// 사용자 정의 풀

ForkJoinPool customPool = new ForkJoinPool(16);
try {
    long sum = customPool.submit(() ->
        numbers.parallelStream()
            .mapToLong(Integer::longValue)
            .sum()
    ).get();
} finally {
    customPool.shutdown();
}

// 활용:
// - 격리된 풀 (다른 작업 영향 X)
// - 다른 스레드 수

7.9 ILIC 활용

public class ShipmentParallelProcessing {
    
    private final List<Shipment> shipments = new ArrayList<>();
    
    // 1. 큰 데이터 합계
    public BigDecimal totalWeightParallel() {
        return shipments.parallelStream()
            .map(Shipment::getWeight)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
        // 큰 데이터일 때 효율
    }
    
    // 2. 무거운 변환
    public List<EnrichedShipment> enrichAll() {
        return shipments.parallelStream()
            .map(this::enrichExpensive)   // DB 조회 등
            .toList();
    }
    
    // 3. anyMatch (단락 + 병렬)
    public boolean hasUrgentParallel() {
        return shipments.parallelStream()
            .anyMatch(Shipment::isUrgent);
        // 빠른 발견
    }
    
    // 4. 사용자 정의 풀
    public void processWithCustomPool() throws Exception {
        ForkJoinPool pool = new ForkJoinPool(4);
        try {
            pool.submit(() -> 
                shipments.parallelStream()
                    .forEach(this::process)
            ).get();
        } finally {
            pool.shutdown();
        }
    }
    
    private EnrichedShipment enrichExpensive(Shipment s) {
        // DB 조회, API 호출 등 (느림)
        return new EnrichedShipment(s);
    }
    
    private void process(Shipment s) {
        // 처리
    }
}

7.10 자기 점검 답변

병렬 Stream 의 동작과 주의점은?

:
1. 정의:

  • 여러 스레드로 동시 처리
  • parallelStream() 또는 parallel()
  • ForkJoinPool.commonPool
  1. 메커니즘:

    • Spliterator 로 분할
    • 청크 처리
    • 결과 병합
  2. 언제 빠른가:

    • 큰 데이터
    • 무거운 연산
    • 분할 쉬운 소스
    • 부수 효과 X
  3. 언제 느린가:

    • 작은 데이터
    • 가벼운 연산
    • LinkedList
    • 순서 의존
  4. 주의:

    • 동시성 안전 (공유 상태 X)
    • 순서 의존 작업 X
    • collect 활용
    • 사용자 정의 풀 가능

8️⃣ Stream 의 함정과 실무 패턴

8.1 함정 1 — 재사용 시도

Stream<String> stream = list.stream()
    .filter(s -> s.length() > 3);

stream.toList();   // 첫 번째 사용 OK
stream.count();    // ❌ IllegalStateException
// "stream has already been operated upon or closed"

// 해결: 매번 새로 생성
List<String> list = ...;
list.stream().filter(...).toList();
list.stream().filter(...).count();

// 또는 Supplier
Supplier<Stream<String>> supplier = () -> list.stream().filter(...);
supplier.get().toList();
supplier.get().count();

8.2 함정 2 — null

// null 처리
List<String> list = Arrays.asList("A", null, "B", null, "C");

// ❌ NullPointerException 가능
list.stream()
    .map(String::toUpperCase)   // null 에서 NPE
    .toList();

// ✓ null 필터링
list.stream()
    .filter(Objects::nonNull)
    .map(String::toUpperCase)
    .toList();

// ✓ Optional 활용
list.stream()
    .map(s -> s == null ? "EMPTY" : s.toUpperCase())
    .toList();

8.3 함정 3 — 부수 효과

// ❌ 부수 효과 (특히 병렬)
List<String> result = new ArrayList<>();
shipments.parallelStream()
    .forEach(s -> result.add(s.getBlNo()));   // race condition

// ✓ collect 또는 toList
List<String> result2 = shipments.parallelStream()
    .map(Shipment::getBlNo)
    .toList();

8.4 함정 4 — toList 의 차이

// Java 16+
List<String> list1 = stream.toList();
// 불변 (unmodifiable)
list1.add("X");   // ❌ UnsupportedOperationException

// Java 8+
List<String> list2 = stream.collect(Collectors.toList());
// 가변 (ArrayList)
list2.add("X");   // OK

// Java 10+
List<String> list3 = stream.collect(Collectors.toUnmodifiableList());
// 불변

8.5 함정 5 — 무한 Stream + 비단락

// ❌ 무한 루프
Stream.iterate(0, n -> n + 1)
    .filter(n -> n % 2 == 0)
    .toList();   // 끝없음
// Stream 끝까지 가지 못함

// ✓ limit 추가
Stream.iterate(0, n -> n + 1)
    .filter(n -> n % 2 == 0)
    .limit(100)
    .toList();

8.6 함정 6 — 박싱

// ❌ 박싱 비용
int sum = list.stream()
    .map(s -> s.length())   // Integer 반환 (boxing)
    .reduce(0, Integer::sum);   // 박싱/언박싱

// ✓ IntStream
int sum2 = list.stream()
    .mapToInt(String::length)   // int 직접
    .sum();
// boxing 회피

8.7 함정 7 — 정렬 + 무한 Stream

// ❌ 무한 + sorted
Stream.iterate(0, n -> n + 1)
    .sorted()   // ★ 모든 요소 필요
    .limit(10)
    .toList();
// 무한 루프 (sorted 가 모두 모음)

// sorted 는 모든 요소를 알아야 정렬 가능
// 단락 평가 불가

8.8 실무 권장 패턴

// 1. 일반적 처리
List<Result> result = source.stream()
    .filter(...)
    .map(...)
    .toList();

// 2. 큰 데이터
result = source.parallelStream()
    .filter(...)
    .map(...)
    .toList();

// 3. 통계
IntSummaryStatistics stats = source.stream()
    .mapToInt(Item::getValue)
    .summaryStatistics();

// 4. 그룹핑 (다음 Unit)
Map<String, List<Item>> grouped = source.stream()
    .collect(Collectors.groupingBy(Item::getCategory));

// 5. 페이지네이션
List<Item> page = source.stream()
    .skip(pageSize * pageNumber)
    .limit(pageSize)
    .toList();

// 6. 최댓값
Optional<Item> max = source.stream()
    .max(Comparator.comparing(Item::getValue));

// 7. 검색
Optional<Item> found = source.stream()
    .filter(i -> i.getId().equals(targetId))
    .findFirst();

8.9 ILIC 활용

@Service
public class ShipmentStreamService {
    
    // 1. 안전한 필터 + 변환
    public List<String> getActiveBlNos() {
        return shipmentRepository.findAll().stream()
            .filter(Objects::nonNull)
            .filter(Shipment::isActive)
            .map(Shipment::getBlNo)
            .filter(Objects::nonNull)
            .distinct()
            .toList();
    }
    
    // 2. 페이지네이션
    public List<Shipment> getPage(int page, int size) {
        return shipmentRepository.findAll().stream()
            .skip((long) page * size)
            .limit(size)
            .toList();
    }
    
    // 3. 통계
    public ShipmentStatistics getStatistics() {
        DoubleSummaryStatistics stats = shipmentRepository.findAll().stream()
            .mapToDouble(s -> s.getWeight().doubleValue())
            .summaryStatistics();
        
        return new ShipmentStatistics(
            stats.getCount(),
            stats.getSum(),
            stats.getAverage(),
            stats.getMin(),
            stats.getMax()
        );
    }
    
    // 4. 단락 평가 활용
    public Optional<Shipment> findFirstUrgent() {
        return shipmentRepository.findAll().stream()
            .filter(Shipment::isUrgent)
            .findFirst();
    }
    
    // 5. 검증
    public ValidationResult validate() {
        List<Shipment> all = shipmentRepository.findAll();
        
        boolean hasInvalid = all.stream()
            .anyMatch(this::isInvalid);
        
        if (hasInvalid) {
            List<Shipment> invalid = all.stream()
                .filter(this::isInvalid)
                .toList();
            return ValidationResult.failed(invalid);
        }
        return ValidationResult.passed();
    }
    
    private boolean isInvalid(Shipment s) {
        return s.getWeight() == null 
            || s.getWeight().compareTo(BigDecimal.ZERO) <= 0;
    }
    
    record ShipmentStatistics(long count, double sum, double avg, double min, double max) {}
    record ValidationResult(boolean valid, List<Shipment> invalid) {
        static ValidationResult passed() { return new ValidationResult(true, List.of()); }
        static ValidationResult failed(List<Shipment> invalid) { return new ValidationResult(false, invalid); }
    }
}

8.10 자기 점검 답변

Stream 의 함정과 권장 패턴은?

:
1. 흔한 함정 7가지:

  • 재사용 (IllegalStateException)
  • null (NPE)
  • 부수 효과 (race condition)
  • toList 의 차이 (불변 vs 가변)
  • 무한 + 비단락
  • 박싱 비용
  • 정렬 + 무한
  1. 권장:

    • 매번 새 Stream
    • null 필터링
    • collect/toList
    • IntStream (boxing 회피)
    • 큰 데이터 — 병렬
    • 단락 평가 활용
  2. 실무 패턴:

    • filter + map + toList
    • skip + limit (페이지)
    • summaryStatistics
    • findFirst (검색)

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
Stream 정의?데이터 처리 파이프라인
컬렉션 vs Stream?저장 vs 처리
중간 연산 vs 최종 연산?Stream 반환 vs 결과 반환
지연 평가?최종 연산 호출 시 실행
단락 평가?결과 결정 시 종료
한 번만 사용?최종 연산 후 닫힘
무한 Stream?iterate, generate
병렬 Stream?parallelStream(), ForkJoinPool
병렬이 빠른 경우?큰 데이터 + 무거운 연산
박싱 회피?IntStream, mapToInt
toList vs Collectors.toList?Java 16 불변 vs Java 8 가변
findFirst vs findAny?순서 vs 빠름 (병렬)
anyMatch 의 단락?첫 매칭 시 종료

9.2 자기 점검 체크리스트

Stream 기초

  • 정의와 특성 5가지
  • 컬렉션과 차이
  • 생성 방법

중간 연산

  • filter, map, flatMap
  • distinct, sorted
  • limit, skip, peek
  • mapToInt 등

최종 연산

  • forEach, toList, toArray
  • collect, reduce
  • count, min, max
  • anyMatch, findFirst

지연/단락

  • 지연 평가의 이유
  • 단락 평가의 효과
  • 무한 Stream 활용

병렬

  • parallelStream
  • ForkJoinPool
  • 동시성 안전
  • 언제 빠른가

함정

  • 재사용 X
  • null 처리
  • 부수 효과
  • 박싱
  • toList 차이

9.3 추가 심화 질문

Q1: Stream 의 close?

답:

  • 대부분 자동 close
  • Files.lines 등은 try-with-resources 권장
  • Stream 도 AutoCloseable
try (Stream<String> lines = Files.lines(path)) {
    lines.forEach(...);
}

Q2: Stream.collect vs reduce?

답:

  • collect: mutable reduction (예: List 에 add)
  • reduce: immutable reduction (각 단계 새 객체)
  • collect 가 더 효율적 (객체 재사용)
  • 단, immutable 의 안전성 ↑

Q3: 병렬 Stream 이 동기 메서드?

답:

  • parallelStream() 호출은 빠름 (Stream 생성만)
  • 최종 연산 (toList 등) 이 실제 처리
  • 최종 연산은 동기 (블로킹)
  • 비동기는 CompletableFuture 등 별도

Q4: Stream 의 메모리?

답:

  • Stream 자체 메모리 ↓
  • 중간 연산 메모리 ↓
  • 단, sorted/distinct 는 모든 요소 보관 (메모리 ↑)
  • 큰 데이터 + sorted 는 주의

Q5: Stream API vs Kotlin Sequences?

답:

  • Stream: Java 8+, JVM 전용
  • Sequences: Kotlin
  • 비슷한 개념 (지연 평가)
  • Sequences 는 더 가벼움 (병렬 X)
  • Kotlin 의 Stream 은 자바 Stream 사용 가능

🎯 핵심 요약 — 3줄 정리

1. Stream API

  • 데이터 처리 파이프라인
  • 함수형, 선언적, 지연 평가
  • 한 번만 사용

2. 핵심 패턴

  • 중간 연산 (filter/map/sorted) + 최종 연산 (collect/forEach)
  • 지연 평가 + 단락 평가
  • 무한 Stream + limit

3. 병렬과 함정

  • parallelStream — 큰 데이터 + 무거운 연산
  • 함정: 재사용/null/부수 효과/박싱
  • collect/IntStream 권장

📚 다음으로...

Unit 10.3 — Collectors와 reduce (★ 마스터 깊이)

이번 Unit에서 Stream API 의 전체를 봤다면, 다음은 수집과 축약의 정밀.

  • Collectors 의 모든 정적 메서드
  • groupingBy, partitioningBy
  • reduce 의 3가지 형태
  • 사용자 정의 Collector

Phase 10 진행 상황

🚀 Phase 10 — 함수형 프로그래밍
  ✅ Unit 10.1 람다 표현식
  ✅ Unit 10.2 Stream API ← 여기
  ⏭ Unit 10.3 Collectors와 reduce (★ 마스터 깊이, 50문항)
  ⏭ Unit 10.4 Optional (3주차 완주)

3주차 누적 진행

✅ Phase 1 ~ 9 완주 (42 Unit)
🚀 Phase 10 — 함수형 프로그래밍 (2/4 진행)

총: 44/43 Unit (3주차 거의 완주)

profile
Software Developer

0개의 댓글