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으로 리팩터링했을 때의 가독성 지표 예시입니다. 실제 수치는 프로젝트/팀 규칙에 따라 달라질 수 있습니다.

벤치마크 힌트(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 스레드풀 사이즈/큐 전략
'Java & Spring' 카테고리의 다른 글
| Java Record vs Lombok DTO 선택 기준: 언제 무엇을 쓸까 (0) | 2025.09.28 |
|---|---|
| java.time 제대로 쓰기: 타임존/포맷 실수 7가지와 안전한 사용법 (0) | 2025.09.28 |
| Optional.orElse vs orElseGet 차이와 NPE 방지: 실무 규칙 7가지 (0) | 2025.09.28 |
| 제네릭 와일드카드 완벽 마스터: PECS(Producer Extends, Consumer Super) 암기팁 (0) | 2025.09.28 |
| ArrayList vs LinkedList: 언제 무엇을 써야하나 (+O(1) 착각 5가지) (0) | 2025.09.27 |