2023-12-15 01:12
최근 자바 개발을 하면서 자바8에서 도입된 스트림(Stream) API를 자주 사용하게 되는데, 처음에는 익숙하지 않아서 기존의 for문이나 iterator를 사용하던 습관이 남아있었다. 하지만 스트림을 제대로 이해하고 사용해보니 코드가 훨씬 간결해지고 가독성이 좋아지는 걸 체감하게 됐다.
스트림은 데이터의 흐름을 의미한다. 배열이나 컬렉션 같은 데이터 소스에서 스트림을 생성하고, 이를 통해 데이터를 처리하는 방식이다. 기존의 반복문을 사용한 명령형 프로그래밍과 달리, 스트림은 선언형 프로그래밍을 가능하게 해준다.
스트림은 한 번 사용하면 소비되어 버린다. 다시 사용하려면 새로운 스트림을 생성해야 한다.
List<String> names = Arrays.asList("김철수", "이영희", "박민수", "최지영");
Stream<String> stream = names.stream();
stream.forEach(System.out::println); // 정상 동작
stream.forEach(System.out::println); // IllegalStateException 발생!스트림 연산은 **중간 연산(Intermediate Operation)**과 **최종 연산(Terminal Operation)**으로 나뉜다.
names.stream()
.filter(name -> name.length() > 2) // 중간 연산
.map(String::toUpperCase) // 중간 연산
.forEach(System.out::println); // 최종 연산기존 for문 방식:
List<String> result = new ArrayList<>();
for (String name : names) {
if (name.length() > 2) {
result.add(name.toUpperCase());
}
}스트림 방식:
List<String> result = names.stream()
.filter(name -> name.length() > 2)
.map(String::toUpperCase)
.collect(Collectors.toList());List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 짝수만 필터링하고 제곱한 후 합계 구하기
int sum = numbers.stream()
.filter(n -> n % 2 == 0) // 짝수만 필터링
.mapToInt(n -> n * n) // 제곱
.sum(); // 합계
System.out.println("짝수의 제곱 합: " + sum); // 220 출력class Person {
private String name;
private int age;
// 생성자, getter, setter 생략
public boolean isAdult() {
return age >= 20;
}
}
List<Person> people = Arrays.asList(
new Person("김철수", 25),
new Person("이영희", 18),
new Person("박민수", 30),
new Person("최지영", 16)
);
// 성인만 필터링하고 이름만 추출
List<String> adultNames = people.stream()
.filter(Person::isAdult)
.map(Person::getName)
.collect(Collectors.toList());filter(Predicate<T>): 조건에 맞는 요소만 필터링map(Function<T, R>): 요소를 다른 형태로 변환sorted(): 정렬distinct(): 중복 제거limit(long n): 처음 n개만 선택skip(long n): 처음 n개를 건너뛰기forEach(Consumer<T>): 각 요소에 대해 작업 수행collect(Collector<T, A, R>): 결과를 컬렉션으로 수집reduce(): 요소들을 결합하여 하나의 결과 생성count(): 요소 개수 반환anyMatch(), allMatch(), noneMatch(): 조건 검사스트림이 항상 좋은 것은 아니다. 실제 성능 테스트를 해보면 상황에 따라 차이가 크다.
// 테스트 데이터: 1부터 1,000,000까지의 숫자
List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000)
.boxed()
.collect(Collectors.toList());
// for문 방식
long startTime = System.currentTimeMillis();
int sum1 = 0;
for (Integer num : numbers) {
sum1 += num;
}
long forTime = System.currentTimeMillis() - startTime;
// 스트림 방식
startTime = System.currentTimeMillis();
int sum2 = numbers.stream()
.mapToInt(Integer::intValue)
.sum();
long streamTime = System.currentTimeMillis() - startTime;
System.out.println("for문 시간: " + forTime + "ms");
System.out.println("스트림 시간: " + streamTime + "ms");실행 결과 (평균):
// 복잡한 필터링과 변환 작업
List<String> result1 = new ArrayList<>();
for (Integer num : numbers) {
if (num % 2 == 0 && num > 1000) {
result1.add("Number: " + num);
}
}
List<String> result2 = numbers.stream()
.filter(num -> num % 2 == 0)
.filter(num -> num > 1000)
.map(num -> "Number: " + num)
.collect(Collectors.toList());실행 결과 (평균):
// 병렬 스트림 사용
int sum3 = numbers.parallelStream()
.mapToInt(Integer::intValue)
.sum();실행 결과 (8코어 환경):
스트림은 중간 연산을 위해 추가 메모리를 사용한다:
하지만 가독성과 유지보수성 측면에서는 스트림이 훨씬 유리하다:
// 복잡한 데이터 처리의 경우 - 스트림이 더 유리
List<String> result = data.stream()
.filter(condition1)
.map(transformation1)
.filter(condition2)
.map(transformation2)
.collect(Collectors.toList());결론: 성능이 중요한 경우에는 실제 테스트를 해보고, 대부분의 경우 코드 가독성을 우선시하는 것이 좋다.
스트림 API는 처음에는 익숙하지 않지만, 한 번 익숙해지면 코드 작성이 훨씬 편해진다. 특히 함수형 프로그래밍의 개념을 자바에서도 사용할 수 있게 해주는 강력한 도구다.
처음에는 간단한 예제부터 시작해서 점진적으로 복잡한 로직에 적용해보는 것을 추천한다. 스트림을 잘 활용하면 코드의 가독성과 유지보수성을 크게 향상시킬 수 있다.
참고 자료: