1. 서론: 명령형에서 선언형으로의 전환
Java 8에서 도입된 Stream API는 자바 프로그래밍 패러다임을 획기적으로 바꾸어 놓았습니다. 기존의 for, while 루프를 사용한 명령형(Imperative) 방식은 “어떻게(How)” 동작하는지에 집중했다면, 스트림은 선언형(Declarative) 방식으로 “무엇을(What)” 할 것인지에 집중합니다.
스트림을 사용하면 코드의 가독성이 높아지고, 병렬 처리를 쉽게 적용할 수 있으며, 복잡한 데이터 처리를 간결한 체이닝으로 해결할 수 있습니다. 이번 가이드에서는 가장 핵심적인 연산인 filter, map, reduce를 중심으로 실무 활용법을 알아봅니다.
2. 스트림의 핵심 연산 3종 세트
2.1. filter: 조건에 맞는 데이터 선별
filter는 스트림의 요소 중 특정 조건(Predicate)을 만족하는 요소만 남기는 중간 연산입니다.
List<String> names = Arrays.asList("Kim", "Lee", "Park", "Choi", "Kang");
// 이름이 'K'로 시작하는 요소만 필터링
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("K"))
.collect(Collectors.toList());
// 결과: [Kim, Kang]
2.2. map: 데이터 변환 및 추출
map은 스트림의 각 요소를 다른 형태의 데이터로 변환하는 중간 연산입니다. 객체에서 특정 필드를 추출하거나, 데이터를 가공할 때 주로 사용합니다.
List<User> users = Arrays.asList(
new User("Alice", 25),
new User("Bob", 30),
new User("Charlie", 22)
);
// 사용자 객체 리스트에서 이름(String)만 추출하여 대문자로 변환
List<String> upperNames = users.stream()
.map(user -> user.getName().toUpperCase())
.collect(Collectors.toList());
// 결과: [ALICE, BOB, CHARLIE]
2.3. reduce: 데이터를 하나로 통합
reduce는 스트림의 모든 요소를 소모하며 하나의 결과값(Optional 또는 단일 값)을 산출하는 최종 연산입니다. 합계, 최댓값, 문자열 결합 등에 사용됩니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 모든 숫자의 합계 계산 (초기값 0 제공)
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
// 결과: 15
3. 실무 시나리오: 복합 연산 체이닝
실제 업무에서는 이 연산들을 조합하여 강력한 데이터 처리 파이프라인을 구축합니다.
요구사항: “20대 사용자들의 점수 평균을 구하라.”
double averageScore = users.stream()
.filter(u -> u.getAge() >= 20 && u.getAge() < 30) // 20대 필터링
.mapToInt(User::getScore) // 점수 추출 (IntStream 변환)
.average() // 평균 계산
.orElse(0.0); // 데이터 없을 시 기본값
4. 스트림 사용 시 주의사항 및 팁
4.1. 지연 연산 (Lazy Evaluation)
스트림의 중간 연산(filter, map 등)은 최종 연산(collect, forEach, reduce 등)이 호출되기 전까지는 실제로 실행되지 않습니다. 이는 불필요한 연산을 방지하고 성능을 최적화하는 데 도움을 줍니다.
4.2. 가독성 vs 성능
스트림은 가독성이 뛰어나지만, 매우 단순한 루프나 기본형(primitive) 배열 작업에서는 전통적인 for 루프보다 약간의 오버헤드가 발생할 수 있습니다. 성능이 극도로 중요한 크리티컬한 구간이 아니라면 유지보수성을 위해 스트림을 권장합니다.
4.3. 상태를 가지는 람다 지양
스트림 내부에서 외부 변수의 상태를 변경하는 작업은 병렬 스트림 실행 시 예측 불가능한 결과를 초래할 수 있습니다. 스트림 연산은 순수 함수(Pure Function)의 성격을 유지하는 것이 안전합니다.
5. 결론: 더 깔끔한 코드를 위하여
Java Stream API는 이제 자바 개발자에게 선택이 아닌 필수입니다. filter, map, reduce의 개념만 명확히 이해해도 대부분의 컬렉션 처리를 우아하게 해결할 수 있습니다. 오늘부터 복잡한 루프를 스트림 파이프라인으로 리팩토링해 보시는 건 어떨까요?