Java stream 개념 정리

Bruce Han·2023년 2월 11일
0

Java8-정리

목록 보기
7/20
post-thumbnail
post-custom-banner

이 포스팅의 코드 및 정보들은 강의를 들으며 정리한 내용을 토대로 작성한 것입니다.

Stream이란?

Sequence of Elements supporting sequential and parallel aggregate operations

연속된 데이터를 처리하는 operation들의 모임/모음이다. 그 자체가 데이터이거나, 데이터를 담고 있는 저장소 자체를 이야기 하는 건 아니다.
stream은 Collection에 데이터를 가지고 있는 것을 소스로 사용해서 처리를 하는 것이다.

stream의 특징

stream은 데이터를 담고 있는 저장소, 컬렉션이 아니다.

Functional in nature

  • functional 하다는 것이다.
  • 즉, stream이 처리하는 데이터 소스를 변경하지 않는다.

stream1
예시로, sports의 목록을 ArrayList로 추가하고 이를 stream()으로 연동시켜보겠다. 현재는 stream()만 걸어놨지 아무 일도 일어나지 않은 상태이다. 여기에 다양한 operator를 추가할 수 있다.

stream2

위 사진처럼 sports.stream().map(String::toUpperCase);의 결과(반환 타입은)는 또 다른 Stream이 된다. stream으로 전달받은 데이터 자체를 대문자로 바꾸는 게 아니다.
stringStream 안에 있는 데이터들은 그대로 소문자로 남아있다.

stream3

출력해보면 대문자로 바뀌지 않은 것을 알 수 있다.

stream으로 처리하는 데이터는 오직 한 번만 처리한다.

무제한일 수도 있다. 이는 Short Circuit 메서드를 사용헤서 제한할 수 있다.

실시간으로 들어오는 것처럼 stream에 데이터가 무제한으로 넘어올 수 있다. 그때는 어떤 것을 단축시키는, 제한하는 Short Circuit이라는 메서드를 활용할 수 있다.

중계 operation은 근본적으로 lazy하다.

stream에다가 줄 수 있는 여러 메서드들이 있는데, 이를 중계 operator, terminal operator로 나눌 수 있다.

lazy하다는 것은 stream을 리턴한다는 것이다.

stream4

이때는 map 메서드를 실행하면서 전달해준 function을 처리해도 출력이 되지 않는다.

stream5

이런 중계형 operator들은 terminal operator가 오기 전까지는 실행하지 않는다. 그냥 정의만 한 것이 된다.
stream 파이프라인(중계 operator를 0, 또는 여러 개 넣을 수 있다)의 끝에는 반드시 terminal, 종료형 operation이 와야 한다.

stream6

위 사진을 보면 stream()으로 collect(Collectors.toList())까지 한 반환 타입이 List인 것을 볼 수 있다. 이는 collect()가 종료형 operator인 것을 알 수 있다.

stream7

이렇게 원본 소스는 바뀌지 않으면서 대문자로 운동 종목들도 출력할 수 있다.

만약 stream을 쓰지 않고 loop를 처리한다면?

stream8

굳이 어렵게 stream을 쓰지 않고 향상된 for문을 사용하면 되지 않을까라고 생각할 수도 있다.

for(String sport : sports) {
    if(sport.startsWith("b")) {
        System.out.println(sport.toUpperCase());
    }
}

지금은 이렇게 간단해보이지만, 조금만 로직이 더 추가된다면 코드가 복잡해질 수 있다. 또한, 근본적으로 for문 같은 loop형 코드는 병렬적으로 처리하기가 어렵다.

stream으로 병렬 처리가 가능하다?

sports.parallelStream()

stream은 for문과 같은 loop형과 달리, parallelStream()을 활용하여 병렬적으로 처리할 수 있다. 이는 jvm이 알아서 병렬적으로 처리를 해주는 것이다. parallelStream() 안에는 spliterator()가 내장되어 있기에, trySplit()으로 stream을 쪼개서 처리를 하게 된다. 그러면 뒤에 오는 operator들이 병렬적으로 처리된다.

stream9

이렇게 collect()를 통해서 모은 후 이를 출력하는 식으로 활용할 수 있다.

추가 실험

stream10

Thread의 이름을 출력하면서 스트림으로 모은 List를 출력한 결과, 중간에 ForkJoinPool을 써서 병렬적으로 처리가 되는 것을 볼 수 있다. 각각의 다른 스레드가 사용된 것을 알 수 있다.

stream11

parallelStream()이 아닌 stream()을 사용하면 동일한 스레드에서 실행된다.

parallelStream()을 써서 비동기 처리된다고 빨라지는 것은 아니다

스레드를 만들고, 이 스레드들에서 병렬적으로 처리하고 수집하는 동안의 비용, 스레드 간에 왔다갔다하는 context switching 비용 등이 오히려 한 스레드에서 처리하는 것보다 더 오래걸릴 수 있다.

그럼에도 병렬 처리가 유용하게 쓰이는 때는 데이터가 정말 방대하게 큰 경우에는 유용하다.
케이스마다 성능 측정해서 parallelStream()으로 바꿨을 때 효과가 있는지 직접 확인하는 게 좋을 것이다.

정리

  • Stream
    • sequence of elements supporting sequential and parallel aggregate operations
    • 데이터를 담고 있는 저장소(컬렉션)가 아니다.
    • Functional in nature, 스트림이 처리하는 데이터 소스를 변경하지 않는다.
    • 스트림으로 처리하는 데이터는 오직 한 번만 처리한다.
    • 무제한일 수도 있다. 이때는 Short Circuit 메서드를 사용해서 제한할 수 있다.
    • 중개 오퍼레이션은 근본적으로 lazy하다.
    • 손쉽게 병렬처리할 수 있다.
  • 스트림 파이프라인
    • 0 또는 다수의 intermediate operation과 한 개의 terminal operation으로 구성한다.
    • 스트림의 데이터 소스는 오직 터미널 오퍼레이션을 실행할 때에만 처리한다.
  • 중개 오퍼레이션
    • Stream을 리턴한다.
    • Stateless / Stateful 오퍼레이션으로 더 상세하게 구분할 수도 있다.
      • 대부분은 Stateless지만, distince나 sorted처럼 이전 소스 데이터를 참조해야 하는 operation은 Stateful operation이다.
    • filter, map, limit, skip, sorted 등이 있다.
  • 종료 오퍼레이션
    • Stream을 리턴하지 않는다.
    • collect, allMAtch, count, forEach, min, max 등이 있다.

Reference

profile
만 가지 발차기를 한 번씩 연습하는 사람은 두렵지 않다. 내가 두려워 하는 사람은 한 가지 발차기를 만 번씩 연습하는 사람이다. - Bruce Lee
post-custom-banner

0개의 댓글