삽질인가 고찰인가

java의 stream에 대하여. 책을 읽어도 이해 안되는거 나만...인가? Stream 동작방식 쉽게 설명해보기.

우당탕 오리의 개발모험 2025. 1. 30. 23:16

 

스트림은 '지연연산'을 한다.

지연연산이란 중간연산들이 아무리 많아도 최종 연산 코드가 나올 때까지 아무것도 하지 않는 것이다.

val strings = listOf("apple", "banana", "cherry")

val result = strings.asSequence()
    .filter { it.length > 5 }   // 필터가 적용된 값을 지연 처리 = 아직 아무 연산도 하지 않음.
    .map { it.toUpperCase() }   // 결과를 지연 처리 = 아직 아무 연산도 하지 않음.
    .toList()                   // 최종 연산. 최종연산이 등장했으니 실제로 연산을 시작한다.

 

스트림은 파이프라인이다.

스트림 생성, 중간연산, 최종연산들을 연결하는 파이프라인을 구성해서 요소들을 하나씩 파이프라인으로 통과 시킨다. 이 때 한개 중간연산에 모든 요소들의 연산결과를 다음 연산으로 넘기는게 아니라, 요소 1개씩 최종 연산까지 전달한다. 즉 순차적으로 모든 요소들이 파이프라인을 통과하는 거라 생각하면 됨.

 

최종연산을 만나서 연산이 시작되면 모든 요소는 하나씩 연산을 실행한다.

위 코드로 예를 들자면, 모든 요소를 한 번에 filter 연산 다 해서 그 결과를 map으로 전달하는게 아니라, 각 요소들이 하나씩 filter->map->toList를 반복한다.

 

  시작--------중간1-------중간2-----최종--

( 🙃              😊                     🥹                       )  <- 요소들이 1명씩(?) 줄서서 파이프라인을 지나가는 이미지~

 

 

중간연산은 신규 스트림 객체를 반환한다.

중간연산을 통과하는 요소가 있으면 신규 스트림 객체가 생성되고 그 객체가 다음 연산으로 연결된다. 기존 스트림을 그대로 사용하지 않는 이유는, 원본 스트림을 유지하기 위해서다.

만약 중간연산을 통과하는 요소가 없다면 신규 객체는 생성되지 않는다. 즉, 통과하는 요소가 1개라도 있어야 신규 객체가 생성된다.
예) filter() 연산자: filter는 조건을 통과하는 요소가 있고 아닌 요소가 있을 것임. 이 때 조건을 통과하는 요소가 하나도 없으면 filter 중간연산자의 응답값이였을 신규 스트림 객체는 리턴되지 않는다.

List<String> strings = List.of("apple", "banana", "cherry");

Stream<String> stream1 = strings.stream();  // 처음 스트림을 생성
Stream<String> stream2 = stream1.filter(s -> s.length() > 5);  // filter 연산 후 새로운 스트림 생성
Stream<String> stream3 = stream2.map(String::toUpperCase);  // map 연산 후 또 새로운 스트림 생성

stream3.forEach(System.out::println);  // 최종 연산

 

 

Q. Iterator와 성능비교?

"작은 데이터는 스트림을 이용하면 신규 객체가 생겨서 비효율 적이니까 객체생성을 하지 않는 Iterator를 이용"하라고?

근데 그렇게 작은 데이터면 스트림을 이용하든, Iterator를 이용하든 의미있는 차이일까?

GPT한테 물어봤는데 내 의견에 동의를 해줬다. 물론 완전 신뢰는 하지 않아야 한다.

작은 데이터셋에서는 스트림을 써도 비효율적이라고 말하기 힘들다는 건 사실이에요.
스트림이 성능이나 메모리 사용에서 중요한 차이를 보이는 건 대규모 데이터를 처리할 때나 복잡한 연산을 할 때죠. 그런데, 작은 데이터셋을 다룰 때는 스트림을 사용해도 비효율적이라고 말할 이유는 거의 없다고 할 수 있습니다.

그럼 언제 이터러블이 더 나을까?
성능을 최우선으로 고려할 때, 특히 매우 작은 데이터셋을 반복하는 경우, 간단한 for 루프나 forEach 같은 이터러블 방식이 오히려 직관적이고 조금 더 빠를 수 있습니다. 하지만 그 차이는 대부분 측정 불가능할 정도로 미세합니다.

결론:
작은 데이터셋에서는 스트림을 사용해도 성능에 큰 차이가 나지 않으며, 오히려 코드 가독성이 더 중요할 때가 많아요.대규모 데이터나 복잡한 연산을 다룰 때 스트림의 성능 최적화가 진가를 발휘합니다.

그래서 사실, 스트림을 써도 비효율적이라고 말할 이유는 거의 없다는 말에 동의합니다. 😊