Programming Language/Java / / 2025. 3. 5. 12:54

Stream 심화

1. 스트림의 내부 동작 원리

 

(1) 지연 평가(Lazy Evaluation)의 핵심 메커니즘

 

  • 중간 연산의 실제 실행 시점: 최종 연산이 호출될 때까지 연산이 지연됨
  • 최적화 기회: 불필요한 계산 회피 (예: limit()filter() 조합)
  • 예시:
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    numbers.stream()
           .filter(n -> {
               System.out.println("Filtering " + n);
               return n % 2 == 0;
           })
           .map(n -> {
               System.out.println("Mapping " + n);
               return n * 2;
           })
           .limit(1)
           .forEach(System.out::println);
    // 출력: Filtering 1 → Filtering 2 → Mapping 2 → 4


(2) 스트림 파이프라인의 단계별 처리

 

graph LR
    A[Source] --> B[Intermediate Op 1]
    B --> C[Intermediate Op 2]
    C --> D[...]
    D --> E[Terminal Op]

 

 


2. 고급 스트림 기법

 

(1) 커스텀 스트림 생성

  • Stream.generate(): 무한 스트림 생성
  • Stream.generate(Math::random) .limit(5) .forEach(System.out::println);
  • Stream.iterate(): 초기값과 조건 기반 스트림
  • Stream.iterate(0, n -> n < 10, n -> n + 2) .forEach(System.out::println); // 0, 2, 4, 6, 8

 

(2) 병렬 스트림 최적화

  • Spliterator 커스맨십:
    class CustomSpliterator<T> implements Spliterator<T> {
        // 분할 로직과 특성 정의
    }

 

  • 병렬 처리 시 주의점:
    • 공유 자원 사용 금지 (Thread Safety)
    • 순서 보장: forEachOrdered() 사용
      List<Integer> nums = Arrays.asList(1, 2, 3, 4);
      nums.parallelStream()
        .forEachOrdered(System.out::print); // 1234
       

(3) 성능 측정 및 최적화

 

  • JMH(Java Microbenchmark Harness) 활용:
    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    public void streamBenchmark() {
        IntStream.range(0, 1_000_000)
                 .filter(n -> n % 2 == 0)
                 .sum();
    }
  • 병렬 vs 직렬 성능 비교:
    // 직렬: 15ms, 병렬: 8ms (8코어 CPU 기준)

 

 


3. 스트림과 Optional의 고급 조합

 

(1) Optional 스트림 처리

List<Optional<String>> options = Arrays.asList(
    Optional.of("Java"), Optional.empty(), Optional.of("Stream")
);

List<String> values = options.stream()
                             .flatMap(Optional::stream)
                             .toList(); // ["Java", "Stream"]

 

(2) 중첩 Optional 처리

Optional<Optional<Double>> nestedOpt = Optional.of(Optional.of(3.14));
Double result = nestedOpt.flatMap(Function.identity())
                         .orElse(0.0); // 3.14

 

 

 


4. 스트림의 상태 관리

(1) 상태 있는 중간 연산

  • sorted(): 전체 요소를 버퍼에 저장 후 정렬
  • distinct(): 이전 요소 추적 필요
  • 주의: 병렬 스트림에서 상태 관리 복잡성 증가

 

(2) 스트림의 불변성 원칙

  • 원본 데이터 변경 금지:
    List<String> list = new ArrayList<>(Arrays.asList("A", "B"));
    list.stream()
        .peek(s -> list.add("C")) // ⚠️ ConcurrentModificationException 발생 가능
        .forEach(System.out::println);

 

 


5. 고급 수집기(Collectors) 활용

 

(1) 커스텀 수집기 구현

Collector<String, StringBuilder, String> customCollector = 
    Collector.of(
        StringBuilder::new,
        (sb, s) -> sb.append(s).append(","),
        (sb1, sb2) -> sb1.append(sb2),
        sb -> sb.deleteCharAt(sb.length()-1).toString()
    );

String result = Stream.of("A", "B", "C")
                      .collect(customCollector); // "A,B,C"

 

(2) 다중 레벨 그룹화

Map<Department, Map<Boolean, List<Employee>>> grouped = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.partitioningBy(e -> e.getSalary() > 5000)
    ));

 

 


6. 스트림 디버깅 기법

 

(1) 중간 결과 확인

List<Integer> nums = Arrays.asList(1, 2, 3, 4);
nums.stream()
    .peek(n -> System.out.println("Original: " + n))
    .filter(n -> n % 2 == 0)
    .peek(n -> System.out.println("Filtered: " + n))
    .map(n -> n * 2)
    .forEach(System.out::println);

 

(2) 스택 트레이스 분석

  • 스트림 연산 체인 식별:
    Exception in thread "main" java.lang.NullPointerException
        at MyClass.lambda$main$0(MyClass.java:15)
        at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:174)
        at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)

 


7. 성능 최적화 전략

 

(1) 기본형 특화 스트림 활용

// 박싱 오버헤드 제거
IntStream.rangeClosed(1, 1_000_000)
         .average()
         .orElse(0); // DoubleStream, LongStream 동일

 

(2) 병렬 처리 최적화 가이드

 

  • 적용 조건:
    • 데이터 크기 ≥ 10,000 요소
    • 연산이 CPU 집약적일 때
    • 소스가 쉽게 분할 가능할 때 (ArrayList vs LinkedList)

 

  • 병렬 처리 회피:
    • 공유 자원 사용 시
    • 순서 의존적 연산

 

 


결론: 프로덕션 레벨 스트림 활용 전략

 

  1. 데이터 특성에 맞는 스트림 선택: 기본형 스트림 우선 사용
  2. 병렬 처리 신중 적용: 성능 테스트 필수
  3. 복잡한 파이프라인 모듈화: 가독성 유지
  4. 스트림+Optional 조합: Null-safe 코드 설계
  5. 성능 모니터링: JFR(Java Flight Recorder) 활용
// 최적화된 예시: 100만 개 데이터 처리
long start = System.nanoTime();
double avg = IntStream.range(0, 1_000_000)
                     .parallel()
                     .filter(n -> n % 3 == 0)
                     .average()
                     .orElse(0);
long duration = (System.nanoTime() - start)/1_000_000;
System.out.printf("평균: %.2f, 처리시간: %dms%n", avg, duration);

 

스트림의 고급 기능을 마스터하면 데이터 처리 로직의 효율성과 유지보수성을 획기적으로 개선할 수 있습니다. 하지만 남용 시 오히려 성능 저하와 가독성 문제를 초래할 수 있으므로, 항상 실제 사용 사례와 성능 측정을 통해 최적의 접근 방식을 선택해야 합니다.

'Programming Language > Java' 카테고리의 다른 글

Exception handling  (0) 2025.03.05
Optional (예제 주의)  (0) 2025.03.05
Stream  (1) 2025.03.05
람다 표현식 (Lambda Expressions)  (0) 2025.03.05
[Collection] PriorityQueue Guide  (0) 2025.03.04
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유