본문 바로가기
Java & Spring

Stream API: for→stream 리팩터링 10가지(성능/가독 균형)

by yamoojin83 2025. 9. 28.

 

Stream API: for→stream 리팩터링 10가지(성능/가독 균형)

반복문이 길어질수록 분기와 임시 변수, break/continue가 늘어나 가독성이 떨어집니다.

여기서는 현업에서 가장 자주 마주치는 for문을 기준으로 stream 리팩터링 10가지를 묶었습니다.

각 항목은 before → after 순서이며, 마지막에 간단한 벤치마크 팁도 덧붙였습니다.

 

한 줄 기준: 변환(map) → 필터(filter) → 정렬(sorted) → 수집(collect) 순서로
파이프라인을 정리하면 읽기 쉽습니다.

1) 필터링 + 변환 + 수집

before

List emails = new ArrayList<>();
for (User u : users) {
  if (u.isActive() && u.getEmail() != null) {
    emails.add(u.getEmail().toLowerCase());
  }
}
  

after

List emails = users.stream()
    .filter(User::isActive)
    .map(User::getEmail)
    .filter(Objects::nonNull)
    .map(String::toLowerCase)
    .toList(); // Java 16+
  

2) anyMatch / allMatch / noneMatch

before

boolean hasAdmin = false;
for (User u : users) {
  if (u.getRoles().contains("ADMIN")) { hasAdmin = true; break; }
}
  

after

boolean hasAdmin = users.stream()
    .anyMatch(u -> u.getRoles().contains("ADMIN"));
  

3) 첫 요소 찾기: findFirst / findAny

before

Order found = null;
for (Order o : orders) {
  if (o.isPaid()) { found = o; break; }
}
  

after

Optional found = orders.stream()
    .filter(Order::isPaid)
    .findFirst();
  

4) 그룹핑: groupingBy / partitioningBy

before

Map<String, List> map = new HashMap<>();
for (User u : users) {
  map.computeIfAbsent(u.getTeam(), k -> new ArrayList<>()).add(u);
}
  

after

Map<String, List> map =
    users.stream().collect(Collectors.groupingBy(User::getTeam));
  

5) Map 만들기: toMap(키 충돌 해결)

before

Map<Long, User> byId = new HashMap<>();
for (User u : users) {
  byId.put(u.getId(), u); // 충돌 시 덮어쓰기
}
  

after

Map<Long, User> byId = users.stream().collect(
  Collectors.toMap(User::getId, u -> u, (a, b) -> a) // 충돌 시 a 유지
);
  

6) 평탄화: flatMap

before

List allTags = new ArrayList<>();
for (Post p : posts) {
  for (String t : p.getTags()) allTags.add(t);
}
  

after

List allTags = posts.stream()
    .flatMap(p -> p.getTags().stream())
    .toList();
  

7) 정렬: Comparator.comparing 체인

before

users.sort((a,b) -> {
  int c = a.getTeam().compareTo(b.getTeam());
  if (c != 0) return c;
  return a.getName().compareTo(b.getName());
});
  

after

List sorted = users.stream()
    .sorted(Comparator.comparing(User::getTeam)
                      .thenComparing(User::getName))
    .toList();
  

8) 중복 제거: distinct / toSet

before

List unique = new ArrayList<>();
for (String s : names) {
  if (!unique.contains(s)) unique.add(s); // O(n^2)
}
  

after

List unique = names.stream().distinct().toList();
// 또는 Set으로 바로 수집: names.stream().collect(Collectors.toSet());
  

9) 합/평균/요약 통계

before

int sum = 0;
for (Order o : orders) sum += o.getAmount();
double avg = (orders.size() == 0) ? 0 : (double) sum / orders.size();
  

after

int sum = orders.stream().mapToInt(Order::getAmount).sum();
double avg = orders.stream().mapToInt(Order::getAmount).average().orElse(0);
  

10) 조건부 수집: filtering / mapping (Collectors)

Java 9+의 Collectors.filtering, Collectors.mapping으로 그룹 내 조건 수집을 깔끔하게 표현할 수 있습니다.

Map<String, List> onlyActiveByTeam = users.stream()
  .collect(Collectors.groupingBy(
    User::getTeam,
    Collectors.filtering(User::isActive, Collectors.toList())
  ));
  
parallelStream 주의: CPU 바운드 + 비공유 상태 + 큰 데이터에서만 검토하세요.
IO 블로킹이나 동기화가 많으면 오히려 느릴 수 있습니다.

전/후 비교 차트(예시)

아래 막대 그래프는 for-loop에서 stream으로 리팩터링했을 때의 가독성 지표 예시입니다. 실제 수치는 프로젝트/팀 규칙에 따라 달라질 수 있습니다.

전/후 비교 차트
코드 줄 수, 분기 수(복잡도), 실행시간(ms). 낮을수록 좋음.

벤치마크 힌트(JMH 스켈레톤)

@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3) @Measurement(iterations = 5) @Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class StreamBench {
  List data;

  @Setup public void setup() {
    data = IntStream.range(0, 1_000_000).boxed().toList();
  }

  @Benchmark public long imperativeSum() {
    long s = 0; for (int v : data) s += v; return s;
  }

  @Benchmark public long streamSum() {
    return data.stream().mapToLong(i -> i).sum();
  }
}
  

실무 체크 포인트

  • 박싱/언박싱 주의: mapToInt/mapToLong 등 원시 스트림을 적극 사용.
  • 불필요한 생성 최소화: collect(toList())가 꼭 필요한지 점검.
  • 명확성 우선: 파이프라인이 너무 길어지면 중간 변수/메서드 추출.
  • 내부 링크: 자료구조 선택 고민이라면 ArrayList vs LinkedList, 제네릭 사용 원칙은 PECS 정리 글을 참고하세요.

 

 

 

👉 1편: ArrayList vs LinkedList: 언제 무엇을 쓰나 (+O(1) 착각 5가지)

👉 2편: 제네릭 와일드카드 완전정복(PECS 암기팁 포함)

👉 3편: Stream API: for→stream 리팩터링 10가지(성능/가독 균형)

👉 4편: Optional.orElse vs orElseGet 차이와 NPE 방지

👉 5편: java.time 제대로 쓰기(타임존/포맷 실수 7가지)

👉 6편: Java Record vs Lombok DTO 선택 기준

👉 7편: NIO.2로 폴더 스캔/감시 구현(FileVisitor/WatchService)

👉 8편: ExecutorService 스레드풀 사이즈/큐 전략

👉 9편: CompletableFuture allOf/anyOf로 외부 API 병렬화

👉 10편: 예외 처리 베스트 프랙티스(체크/언체크, 경계 설계)