F-LAB JAVA · 3주차 · Phase 10 · 함수형 프로그래밍
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
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 = 데이터 처리의 파이프라인.
1. Stream 의 정의와 5가지 특성
2. Stream 생성 방법
3. 중간 연산 9가지
4. 최종 연산 10가지
5. 지연 평가의 메커니즘
6. 단락 평가 (Short-circuit)
7. 병렬 Stream
8. Stream 의 함정과 실무 패턴
9. 면접 + 자기 점검
Stream:
데이터의 흐름 (시퀀스) 을 표현하는 객체.
연산을 파이프라인으로 연결.
Java 8+ (2014) 도입.
본질:
- 데이터 저장 X
- 데이터 처리 (변환/필터/집계)
- 함수형 스타일
| 항목 | 컬렉션 | Stream |
|---|---|---|
| 본질 | 데이터 저장 | 데이터 처리 |
| 외부 반복 | for, iterator | X |
| 내부 반복 | X | ✓ (forEach 등) |
| 평가 | 즉시 | 지연 (Lazy) |
| 재사용 | ✓ | ✗ (한 번만) |
| 변경 | 가능 | X (불변) |
| 무한 | X | ✓ (iterate, generate) |
| 병렬 | X | ✓ (parallelStream) |
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. 데이터 저장 X
- Stream 은 데이터 보관 X
- 컬렉션/배열을 처리
2. 함수형 (불변)
- 원본 변경 X
- 새 Stream 반환
3. 지연 평가
- 중간 연산은 즉시 X
- 최종 연산 호출 시 시작
4. 한 번만 사용
- 최종 연산 후 닫힘
- 재사용 → IllegalStateException
5. 무한 가능
- iterate, generate
- 단, limit 등으로 유한화
Stream 의 파이프라인:
데이터 소스 (List, Set, 배열, ...)
↓
.stream()
↓
.filter(predicate) ← 중간 연산 (새 Stream)
↓
.map(function) ← 중간 연산 (새 Stream)
↓
.sorted() ← 중간 연산 (새 Stream)
↓
.collect(Collectors.toList()) ← 최종 연산 (실행 + 결과)
↓
결과
특징:
- 중간 연산: 새 Stream
- 최종 연산: 실행 + 결과
- 지연 평가
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();
}
}
Stream 의 정의와 특성은?
답:
1. 정의:
5가지 특성:
컬렉션과 차이:
연산:
// 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();
// 고정된 값
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();
// 무한 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));
// 오늘, 내일, 모레, ...
// 무한 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();
// 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
// 파일의 줄 단위
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"
// 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();
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) {}
}
Stream 생성 방법은?
답:
1. 컬렉션:
.stream(), .parallelStream()고정 값:
Stream.of(...)Arrays.stream(arr)무한:
Stream.iterate(seed, fn)Stream.generate(Supplier)IntStream:
range(start, end) — end 제외rangeClosed(start, end) — 모두 포함파일:
Files.lines(path)Files.list/walk중간 연산 (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 — 기본 타입 변환
// 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())
// 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();
// 각 요소를 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();
// 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();
// 자연 순서
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();
// 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();
// 디버깅용 — 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
// 기본 타입 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();
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();
}
}
중간 연산 9가지는?
답:
1. filter: Predicate 로 필터
2. map: Function 으로 변환
3. flatMap: 평탄화 (중첩 풀기)
4. distinct: 중복 제거
5. sorted: 정렬
6. limit: 개수 제한
7. skip: 건너뛰기
8. peek: 엿보기 (디버깅)
9. mapToInt/Long/Double: 기본 타입 변환
모두 새 Stream 반환, 지연 평가
최종 연산 (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
// 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);
// 입력 순서로 출력 (병렬 효과 ↓)
// 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+
// 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 에서 정밀
// 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);
// 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();
// 매칭 검사 (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
// 첫 번째 또는 임의 요소
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);
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();
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());
}
}
최종 연산 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
특징:
지연 평가 (Lazy Evaluation):
중간 연산은 즉시 실행 X
최종 연산 호출 시점에 실행.
이유:
- 불필요한 계산 회피
- 단락 평가 가능
- 무한 Stream 가능
증명:
Stream<T> stream = data.stream()
.filter(predicate) // 실행 X
.map(function); // 실행 X
// 아직 아무것도 실행 안 됨
stream.count(); // ★ 이제 모든 중간 연산 실행
// 코드
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
// 지연 평가의 효과
// 전통 — 각 단계 완료 후 다음
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();
// 각 요소가 파이프라인을 흐름
// 첫 매칭 시 즉시 종료
// 지연 평가 덕에 무한도 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 가 단락 평가
// 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
// 지연 평가의 정확한 이해 필요
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;
// ...
한 번의 반복으로 모든 연산
성능 ↑
// 첫 매칭만 필요
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개 발견 → 종료
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();
// 필요한 만큼만 처리
}
}
지연 평가의 메커니즘은?
답:
1. 정의:
이유:
시각화:
fusing:
함정:
단락 평가 (Short-circuit Evaluation):
결과 결정 시 즉시 종료.
더 처리할 필요 X.
지원 연산:
중간:
- limit
- takeWhile (Java 9+)
- dropWhile (Java 9+)
최종:
- findFirst, findAny
- anyMatch, allMatch, noneMatch
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 번째에서 즉시 종료
// 무한 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개 도달 시 즉시 종료
// 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 에서 결정
// findFirst — 첫 번째
Optional<Shipment> first = shipments.stream()
.filter(Shipment::isUrgent)
.findFirst();
// findAny — 임의 (병렬에서 빠름)
Optional<Shipment> any = shipments.stream()
.filter(Shipment::isUrgent)
.findAny();
// 둘 다 단락 평가:
// - 매칭 발견 시 즉시 종료
// - 무한 Stream 도 OK
// 차이:
// - findFirst: 순서 보장 (느릴 수 있음)
// - findAny: 빠름 (병렬에서)
boolean allActive = shipments.stream()
.allMatch(Shipment::isActive);
// 동작:
// 1. 첫 요소 — active 면 계속
// 2. inactive 발견 → 즉시 false
// 3. 나머지 처리 X
// 모두 active 면 끝까지 확인
boolean noErrors = shipments.stream()
.noneMatch(Shipment::hasError);
// 동작:
// 1. 매칭 (에러) 발견 → 즉시 false
// 2. 나머지 처리 X
// 매칭 없으면 끝까지 확인
// 함정 — 부수 효과 있을 때
List<Integer> processed = new ArrayList<>();
boolean any = numbers.stream()
.peek(processed::add)
.anyMatch(n -> n > 100);
// processed 에 몇 개?
// → 첫 매칭까지의 개수
// 매칭 없으면 모든 요소
// 정확한 수 예측 어려움
// 권장: 부수 효과 안 쓰기
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 만나면 종료
}
}
단락 평가의 의미는?
답:
1. 정의:
지원 연산:
효과:
함정:
takeWhile vs filter:
병렬 Stream (Parallel Stream):
여러 스레드로 동시에 처리하는 Stream.
생성:
- collection.parallelStream()
- stream.parallel()
내부:
- Fork/Join Framework
- ForkJoinPool.commonPool() 사용
- 기본 스레드 수: CPU 코어 수
// 순차
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();
병렬 Stream 의 동작:
1. 데이터를 청크 (chunk) 로 분할
- Spliterator 활용
2. 각 청크를 별도 스레드에서 처리
- ForkJoinPool 의 worker 스레드
3. 결과 병합 (combiner)
- reduce 등
분할:
- List: 균등 분할
- LinkedList: 분할 비효율
- Set: 보통 균등
- 무한 Stream: 분할 어려움
병렬이 빠른 경우:
1. 큰 데이터
- 작은 데이터는 오버헤드 ↑
2. 무거운 연산
- CPU 작업
- 각 요소 처리 비용 ↑
3. ArrayList, IntStream 같은 분할 쉬운 소스
4. 부수 효과 없음
- 동시성 안전
5. 순서 의존성 없음
- findAny, anyMatch 등
병렬이 느린 경우:
- 작은 데이터
- 가벼운 연산
- LinkedList
- 순서 의존
- 동기화 비용 큰 작업
// 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
// 병렬이 더 느림 (오버헤드)
// ❌ 위험 — 공유 가변 상태
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);
// 가능하지만 성능 ↓ (락 경합)
// 순서 보장 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();
// 아무거나 (빠름)
// 기본은 ForkJoinPool.commonPool() 사용
// 사용자 정의 풀
ForkJoinPool customPool = new ForkJoinPool(16);
try {
long sum = customPool.submit(() ->
numbers.parallelStream()
.mapToLong(Integer::longValue)
.sum()
).get();
} finally {
customPool.shutdown();
}
// 활용:
// - 격리된 풀 (다른 작업 영향 X)
// - 다른 스레드 수
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) {
// 처리
}
}
병렬 Stream 의 동작과 주의점은?
답:
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();
// 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();
// ❌ 부수 효과 (특히 병렬)
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();
// 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());
// 불변
// ❌ 무한 루프
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();
// ❌ 박싱 비용
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 회피
// ❌ 무한 + sorted
Stream.iterate(0, n -> n + 1)
.sorted() // ★ 모든 요소 필요
.limit(10)
.toList();
// 무한 루프 (sorted 가 모두 모음)
// sorted 는 모든 요소를 알아야 정렬 가능
// 단락 평가 불가
// 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();
@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); }
}
}
Stream 의 함정과 권장 패턴은?
답:
1. 흔한 함정 7가지:
권장:
실무 패턴:
| 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 의 단락? | 첫 매칭 시 종료 |
답:
try (Stream<String> lines = Files.lines(path)) {
lines.forEach(...);
}
답:
답:
답:
답:
1. Stream API
2. 핵심 패턴
3. 병렬과 함정
이번 Unit에서 Stream API 의 전체를 봤다면, 다음은 수집과 축약의 정밀.
🚀 Phase 10 — 함수형 프로그래밍
✅ Unit 10.1 람다 표현식
✅ Unit 10.2 Stream API ← 여기
⏭ Unit 10.3 Collectors와 reduce (★ 마스터 깊이, 50문항)
⏭ Unit 10.4 Optional (3주차 완주)
✅ Phase 1 ~ 9 완주 (42 Unit)
🚀 Phase 10 — 함수형 프로그래밍 (2/4 진행)
총: 44/43 Unit (3주차 거의 완주)