[Java] "데이터를 쿼리처럼 다루다" - Java Stream API 실무 심화 가이드
안녕하세요! 오늘은 자바 개발자의 필수 소양인 Stream API를 아주 깊게 파헤쳐 보겠습니다. 단순히 "코드가 짧아진다"는 장점을 넘어, 왜 현대 자바에서 스트림이 표준이 되었는지 그 내부 동작 원리와 실무 꿀팁을 정리했습니다.
1. 스트림의 철학: "어떻게"가 아니라 "무엇을"
전통적인 for-each 문은 외부 반복(External Iteration)입니다. 개발자가 직접 루프를 돌리고, 조건문을 걸고, 변수에 값을 담아야 하죠. 반면 스트림은 내부 반복(Internal Iteration)입니다.
"이 리스트에서 짝수만 골라서(filter), 제곱을 한 뒤(map), 리스트로 만들어줘(collect)"라고 선언만 하면 됩니다. 실제 반복은 자바 엔진이 알아서 처리합니다.
2. 스트림의 마법: 지연 연산 (Lazy Evaluation)
많은 분이 오해하는 것이 "스트림 문장을 만나는 즉시 데이터를 처리한다"는 점입니다. 하지만 스트림은 최종 연산이 호출되기 전까지는 아무 일도 하지 않습니다.
- 중간 연산의 최적화: 만약 1,000만 개의 데이터가 있는데 filter()를 걸고 limit(10)을 걸었다면, 스트림은 1,000만 개를 다 거르지 않습니다. 딱 10개를 찾는 순간 연산을 즉시 종료합니다. 이를 Short-circuiting이라고 부릅니다.
- 루프 합치기(Loop Fusion): 여러 개의 filter와 map이 연결되어 있어도 스트림은 이를 하나의 루프로 합쳐서 처리합니다. 성능 손실을 최소화하는 비결입니다.
3. 실무 핵심 문법 & 예시 (Step by Step)
실무에서 가장 많이 쓰이는 시나리오인 "객체 리스트 가공"을 예로 들어보겠습니다.
A. 필터링과 매핑 (가장 기본)
데이터를 거르고 필요한 부분만 쏙 빼낼 때 사용합니다.
List<String> topPerformers = employees.stream()
.filter(e -> e.getRating().equals("A")) // 성과가 A인 직원만 (Predicate)
.map(Employee::getName) // 이름 필드만 추출 (Function)
.collect(Collectors.toList());
B. 데이터 그룹화 (Grouping)
SQL의 GROUP BY와 같습니다. 부서별로 급여 합계를 낼 때 아주 강력합니다.
Map<String, Integer> sumByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDept, // 그룹화 기준 (부서)
Collectors.summingInt(Employee::getSalary) // 합계 로직
));
C. Optional과 연계 (Null 안정성)
스트림의 결과가 없을 수도 있을 때 NullPointerException을 방지합니다.
double avgSalary = employees.stream()
.filter(e -> e.getDept().equals("R&D"))
.mapToInt(Employee::getSalary)
.average() // 결과가 없을 수도 있음 (OptionalDouble 반환)
.orElse(0.0); // 데이터가 없으면 0.0 반환
4. 병렬 스트림(Parallel Stream)의 빛과 그림자
parallelStream()을 쓰면 쿼드코어, 옥타코어 CPU를 풀가동할 수 있습니다. 하지만 주의점이 있습니다.
- 언제 써야 하나?: 요소의 개수가 최소 수만 개 이상이고, 요소당 처리 로직이 무거울 때 효과적입니다.
- 왜 위험한가?:
- 순서 의존성: findFirst() 같은 작업은 병렬로 하면 순서를 맞추느라 오히려 더 느려집니다.
- Thread Pool 공유: 병렬 스트림은 공용 ForkJoinPool을 사용합니다. 한 곳에서 너무 무거운 병렬 스트림을 돌리면 시스템 전체의 다른 병렬 작업들이 멈출 수 있습니다.
- 상태 변경: 람다 내부에서 외부 변수 값을 바꾸면 멀티 스레드 환경에서 값이 꼬입니다.
5. 실무자라면 꼭 알아야 할 3가지 Golden Rules
- 디버깅 시 peek() 활용: 스트림 중간에 데이터가 어떻게 변하는지 보고 싶을 때 사용하세요.
-
Java
.filter(e -> e.getAge() > 30) .peek(e -> System.out.println("Filter 통과: " + e.getName())) // 로그 출력 .map(...) - Primitive 스트림 사용: Integer 대신 IntStream, LongStream을 쓰세요. 오토박싱(Auto-boxing) 비용을 줄여 성능을 획기적으로 높여줍니다.
- 스트림 재사용 금지: 한 번 collect나 forEach를 한 스트림 변수는 버려야 합니다. 다시 호출하면 에러가 납니다.
결론: 스트림은 '가독성'과 '효율성'의 조화
스트림을 잘 다루는 개발자는 코드가 간결할 뿐만 아니라, 데이터 처리 로직이 한눈에 들어옵니다. 아까 우리가 이야기했던 LEFT JOIN LATERAL이 SQL의 가독성과 성능을 높여주듯, Java Stream은 자바 코드의 수준을 한 단계 높여주는 도구입니다.
https://run-code-rich.tistory.com/entry/postgres-lateral-join-guide
[PostgreSQL] 단순 JOIN으로는 부족할 때? LATERAL JOIN 완벽 가이드
안녕하세요! 오늘은 SQL의 성능과 가독성을 한 단계 업그레이드해 줄 수 있는 강력한 기능, LATERAL JOIN에 대해 알아보려고 합니다.데이터베이스 쿼리를 짜다 보면 "A 테이블의 각 행을 읽으면서, 그
run-code-rich.tistory.com
오늘 정리한 내용을 바탕으로 여러분의 프로젝트에 직접 스트림을 적용해 보세요!
'TECH > 웹개발' 카테고리의 다른 글
| [Java] 메서드 체이닝(Method Chaining) 완벽 가이드: 원리부터 실무 활용까지 (0) | 2026.02.10 |
|---|---|
| [Java] Optional 완벽 가이드: 더 이상 NullPointerException은 없다 (0) | 2026.02.09 |
| [Java] 람다식(Lambda) 완벽 가이드: 개념부터 실무 활용 예시까지 (0) | 2026.02.07 |
| 코드 한 줄 안 고치고 기능을 바꾼다고? (SOLID부터 스프링 컨테이너까지) (0) | 2026.01.20 |
| "왜 다형성만으로는 부족할까? SOLID 원칙과 스프링 DI의 본질" (0) | 2026.01.19 |