Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 

Repository files navigation

Stream 성능 실험

📌 프로젝트 개요

본 프로젝트는 Stream 사용 시 자주 언급되는 성능/설계 이슈를 실제 코드와 측정 결과로 검증하는 것을 목표로 합니다.

Stream API는 선언적이고 가독성 높은 코드 작성을 가능하게 하지만, 내부 동작을 고려하지 않은 사용은 불필요한 오버헤드로 인해 오히려 성능이 저하될 수 있습니다.
이번 프로젝트에서는 다음 두 가지 이슈를 중심으로 비교 실험을 진행했습니다.

  • Parallel Stream의 임계점(Threshold)

    • 데이터가 작거나 연산이 가벼우면 병렬화 오버헤드가 커져 순차 스트림이 더 빠를 수 있음
    • 데이터가 크고 CPU 집약적 연산일수록 병렬 처리 이점이 커질 수 있음
  • 중간 연산(Intermediate Operations) 과다 체이닝 오버헤드

    • filter()/map()을 불필요하게 여러 번 체이닝하면 파이프라인 단계가 늘어나 오버헤드가 발생할 수 있음
    • Stream Fusion 관점에서 연산을 통합해 실행 시간을 비교함


👥 팀원 소개

서지혜 김종연 강민영


🧰 기술 스택

  • Language: Java (JDK 17)
  • IDE: Eclipse (4.36.0)
  • Version Control: Git / GitHub
  • Tool: GitKraken (선택)

🎯 프로젝트 목표

저희는 Stream API에서 자주 언급되는 7가지 이슈 중, 아래 두 가지 주장을 직접 구현한 비추천 코드 vs 추천 코드 비교를 통해 검증하고자 합니다.

  • 중간 연산(filter, map)을 과도하게 체이닝하면 성능 오버헤드가 발생한다
  • 병렬 처리 시 동일한 데이터를 동시에 수정하면 레이스 컨디션이 발생할 수 있다

이를 단순한 코드 나열이 아니라 측정/재현 가능한 형태로 구현하여 왜 비추천인지와 어떤 방식이 더 안전/효율적인지를 결과로 확인하는 것을 목표로 합니다.

🧪 과제 정리 및 리팩토링 전략

1) Parallel Stream 실험 전략 (Threshold 검증)

✅ 현황 분석 (As-Is: 단순 예시 코드)

  • 문제점: 데이터셋이 매우 작음(예: 5개)에도 parallelStream()을 사용하면, 작업 분할/병합 비용(스레드 스케줄링, ForkJoin 오버헤드 등)이 실제 연산보다 커져 비효율이 발생할 수 있다.
  • 결과 해석: 불필요한 병렬화는 오히려 순차 처리보다 성능을 저하시킬 수 있음을 확인.

✅ 리팩토링 접근 방식 (To-Be: 임계점/조건을 통제한 비교 실험)

단순히 병렬이 빠르다/느리다가 아니라 성능 이득이 발생하는 임계점(Threshold) 을 입증하기 위해 아래 두 변수를 통제하여 실험했다.

  • 변수 1) 데이터셋 크기 (N)

    • 작은 데이터(예: 5개) vs 큰 데이터(1,000,000개)
    • 병렬 처리의 이득이 유의미해지는 데이터 규모를 확인
  • 변수 2) 연산 복잡도 (Q)

    • 가벼운 연산: (예: n×n 같은 단순 계산)
      → 데이터가 많아도 연산이 너무 가벼우면 병렬화 오버헤드가 상대적으로 커질 수 있음
    • 무거운 연산(CPU 집약): 소수 판별(isPrime)
      → 병렬 스트림의 진정한 성능 우위를 확인하기 위해 선택

🎯 리팩토링 목표 / 기대 효과

  • 병렬 스트림 적용 여부는 단순히 “데이터가 많다”가 아니라, N × Q(데이터 수 × 연산 비용) 모델로 판단해야 함을 수치로 입증
  • 무분별한 병렬 처리가 아닌 컨텍스트(데이터 크기, CPU 부하) 기반의 최적 스트림 사용 가이드라인 제시

2) Intermediate Operations 실험 전략 (Stream Fusion 최적화)

✅ 현황 분석 (As-Is: Original)

  • 문제점: 동일한 데이터를 처리하면서 filter()와 map() 같은 중간 연산을 불필요하게 여러 번 체이닝(Chaining)함
  • 결과 해석: 각 단계마다 내부 반복/람다 호출/객체 생성 등 파이프라인 처리 과정에서 불필요한 성능 오버헤드가 누적될 수 있음

✅ 리팩토링 접근 방식 (To-Be: Stream Fusion)

단순히 코드를 줄이는 수준이 아니라, 스트림의 Stream Fusion(연산 융합) 관점에서 파이프라인 단계를 최적화했다.

  • Filter 통합:
    filter(A) → filter(B) 를
    filter(A && B) 형태로 합쳐 중간 연산 단계 축소

  • Map 통합:
    대문자 변환 + 문구 추가를 하나의 람다에서 처리하여 연산 횟수 축소

  • 객관적 입증:
    동일한 대량 데이터(1,000,000개)를 대상으로
    Original(과한 중간 연산) vs Optimized(융합된 연산) 실행 시간을 ns 단위로 측정하여 수치로 비교

🎯 리팩토링의 의의

  • 파이프라인 단계를 줄여 실행 시간이 단축되는 방향을 확인
  • 코드 가독성을 높이는 동시에, 대규모 데이터 처리에서 JVM이 스트림 파이프라인을 더 효율적으로 최적화할 수 있도록 구조 개선


💡 기술적 목표

  • 같은 입력 데이터(크기 동일)에서
  • (1) 중간 연산을 여러 번 나누는 방식(Original) vs 조건을 합쳐 중간 연산 개수를 줄이는 방식(Optimized)
    • 위 두 방식을 동일 조건으로 반복 실행하여 실행 시간 비교
  • (2) parallelStream()이 항상 성능 우위를 점하는지 확인하기 위해 **연산 비용(Q)**이 다른 두 가지 케이스를 대조군으로 설정함 (단순 연산 vs 복잡 연산)
    • 단순 연산: 이미지 예시와 같은 단순 곱셈(n × n) 연산을 수행하여, 병렬화 오버헤드가 성능을 저하시키는 현상을 확인
    • 복잡 연산: CPU 집약적인 소수 판별(isPrime) 연산을 수행하여, 병렬 처리가 실질적인 성능 이득을 제공하는 상황을 입증


🚀 트러블 슈팅

1) 워밍업(Warm-up) 없이 측정하면 결과가 흔들림 (JIT / 캐시 영향)

문제 상황

  • Stream 성능을 System.nanoTime()으로 측정했는데 실행할 때마다 시간이 들쭉날쭉하거나 첫 실행이 유난히 느리게 나오는 현상이 발생함
  • Java는 실행 중에 JIT(Just-In-Time) 컴파일이 적용되고 자주 실행되는 코드가 최적화되므로 첫 실행 결과를 그대로 비교하면 성능 비교가 왜곡될 수 있음

원인

  • 첫 실행에는 JIT 최적화가 아직 충분히 적용되지 않았고 클래스 로딩/람다 초기화/캐시 미스 등의 초기 비용이 포함될 수 있음

해결 방법

  • 측정 전에 동일 로직을 여러 번 실행(워밍업)하여 JVM이 최적화할 시간을 준 뒤, 그 다음 실행부터 시간을 측정하도록 변경함
  • (권장) 워밍업과 측정 모두 동일 조건(같은 데이터/같은 횟수) 으로 맞춰 공정하게 비교함

⚙ 실행 결과 (시간 비교)

1) Parallel Stream (isPrime) — ms 통일

데이터 범위 순차(ms) 병렬(ms) 시간 단축(ms) 개선율
1 ~ 1,000,000 111.0 60.0 51.0 45.95%

2) Intermediate Ops 최적화(Stream Fusion)

데이터 Optimized(ms) Original(ms) 시간 단축(ms) 개선율
1,000,000~5,000,000 312.1 341.9 29.8 8.72%


📂 프로젝트 구조

streamTest/
├─ README.md
└─ src/
   └─ step02/
      ├─ ParallelEfficiencyTest.java
      └─ StreamOptimizationTest.java

About

[FISA6기 과정] stream 성능 test

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages