Java&Web

[Java] Stream과 사용시 주의할 점

프로그래민 2022. 1. 14. 00:25
반응형

Stream이란

Stream(스트림)은 자바 8버전부터 추가된 컬렉션(배열 포함)의 저장 요소를 하나씩 참조해서 람다식(함수적 스타일)으로 처리할 수 있도록 도와주는 반복자이다. 자바 7버전까지는 컬렉션에서 요소를 순차적으로 처리하기 위해 Iterator반복자를 사용하였다. 자바 8버전부터는 Straeam이 등장하였다. Stream은 Iterator와 비슷한 역할을 하는 반복자이지만, 람다식으로 요소 처리 코드를 제공하는 점과 내부 반복자를 사용하기에 병렬 처리가 쉽다는 점 그리고 중간 처리 + 최종 처리의 파이프라인 작업을 수행한다는 차이가 있다. 

 

Stream의 특징

람다식으로 요소 처리 코드 제공

Stream이 제공하는 대부분의 요소 처리 메서드는 함수적 인터페이스 매개 타입을 가지기 때문에 람다식 또는 메서드 참조를 이용해서 요초 처리 내용을 매개 값으로 전달할 수 있다.

 

내부 반복자

외부 반복자와 내부 반복자

외부 반복자는 개발자가 코드로 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴을 말하며 index를 이용하여 for문과 Iterator를 이용하는 While문이 대표적인 외부 반복자이다. 내부 반복자는 컬렉션 내부에서 요소들을 반복시키고, 개발자는 요소당 처리해야할 코드만 제공하는 코드 패턴이다. 

내부 반복자의 장점은 요소를 반복시키는 방식에 대한 것은 컬렉션에 맡기고 개발자는 요소 처리 코드에 만 집중할 수 있다. 내부 반복자는 요소들의 반복 순서를 변경하거나, 멀티 코어 CPU를 활용하기 위해 요소들을 분배시켜 병렬 작업을 할 수 있게 도와주기 때문에 하나씩 처리하는 순차적 외부 반복자보다는 효율적으로 요소를 반복시킬 수 있다.

 

병렬 처리

병렬 처리는 한 가지 작업을 서브 작업으로 나누고, 서브 작업들을 분리된 스레드에서 병렬적으로 처리하는 것이다. 병렬 처리 스트림을 이용하면 런타임 시 하나의 작업을 서브 작업으로 자동으로 나누고, 서브 작업의 결과를 자동으로 결합해서 최종 결과물을 생성한다.

 

중간 처리와 최종 처리

중간 처리 : 매핑(map), 필터링(filter, distinct), 정렬(sorted), 반복(peek) 등
최종 처리 : 반복(forEach), 카운팅(count), 평균(average), 리듀스(reduce) 등

Stream은 데이터를 가공하는 중간 처리와 결과물을 내는 역할을 하는 최종 처리의 파이프라인으로 이루어져있다. 또한 Stream은 해당 데이터로 최종 처리를 해야만 중간 처리 과정을 수행한다. 즉 최종 처리 전까지 연산을 미루는 지연(lazy)되는 성격을 가지고 있다. 

 

Stream 사용시 고려할 점

요소의 수와 요소당 처리시간

요소의 수가 적고 요소당 처리 시간이 짧으면 순차처리가 병렬처리보다 빠를 수 있다. 병렬 처리에는 스레드풀 생성과 스레드 생성이라는 오버헤드가 발생한다.

 

스트림 소스의 종류

ArrayList는 인덱스로 요소를 관리하여 데이터를 쪼개기 쉽지만, HashSet, TreeSet은 요소 분리가 쉽지 않고, LinkedList도 링크를 따라가야 하기에 분리가 쉽지 않다. 요소 분리가 쉽지 않은 자료구조는 상대적으로 병렬 처리가 늦다.

 

코어의 수

싱글 코어 CPU인 경우에는 당연히 순차 처리가 빠르다. 병렬 스트림을 사용할 경우 스레드의 수만 증가하고, 동시성 작업으로 처리되기 때문에 좋지 못한 결과를 낼 수 있다. 코어가 많을수록 병렬 작업 처리 속도는 빨라진다.

 

Stream 사용시 주의할 점

재사용 스트림 문제

IntStream stream = IntStream.of(1, 2, 3);
stream.forEach(x -> System.out.println(x));	//첫번째 stream 사용

stream.forEach(x -> System.out.println(x));	//두번째 stream 사용

다음은 stream을 한번 사용한 후 다시 한번 사용하게 되는 경우이다. 스트림은 오직 한번만 소비할 수 있기에 두번째 사용할 경우 IllegalStateException을 만나게 될 수 있다.

 

range와  rangeClosed

IntStream.range(1, 2).forEach(System.out::println);		//1
IntStream.rangeClosed(1, 2).forEach(System.out::println);	//1 2

range는 열린 범위, rangeClosed는 닫힌 범위를 나타낼때 사용한다. 따라서 적절히 선택하여 사용하는것이 좋다.

 

무한 스트림 생성 문제

Stream.iterate(0, i -> (i + 1) % 2)
	.distinct()
	.limit(10)
	.forEach(System.out::println);

System.out.println("Complete!!");	//무한 스트림으로 인해 도달하지 못함

위의 Stream은 0과 1을 반복적으로 생성후 distinct 연산자를 이용하여 단일 0, 1을 가진 후 요소를 10개로 제한하는 예제이다. 여기서 distinct 연산자는 스트림을 생성하는 iterate 메서드에서 0과 1만 생성된다는것을 알지 못하기에 요소를 10개로 제한하는 limit 연산자에 도달할 수 없게 된다.

따라서 위와 같은 경우는 distinct와 limt 연산자의 위치를 바꾸어 생성될 Stream의 요소의 수를 먼저 제한하고, 그 후 중복제거하는 방식으로 무한 스트림을 피할 수 있다.

Stream.iterate(0, i -> (i + 1) % 2)
	.limit(10)
	.distinct()
	.forEach(System.out::println);

System.out.println("Complete!!");	//무한 스트림이 아니기에 정상 출력

 

지역 변수 접근

int localVariable = 0;

IntStream.range(0, 5).forEach(i -> localVariable += i);		//지역 변수 연산 불가
IntStream.range(0, 5).forEach(i -> staticVariable += i);	//클래스 변수 연산 가능

Stream을 이용하면서 람다나 메서드 참조를 사용하는 경우에는 지역 변수에 접근할 수 없다. 이것은 지역 변수가 스택에 위치하기 때문이다. 람다가 실행되는 스레드에서 지역 변수를 참조할 때는 지역 변수의 읽기전용 복사본(람다 caputre)을 생성하고 참조하게 된다. 만일 람다가 직접 지역 변수가 위치한 스택에 접근한다면, 지역 변수가 할당한 스레드가 사라져 변수 할당이 해제되었는데도 람다를 실행하는 다른 스레드가 해당 변수를 접근하는 상황이 발생하여 동시성 문제를 일으킬수가 있다. 따라서 이를 방지하기 위해 지역변수에 대해 읽기전용 복사본(람다 capture)을 사용한다. 그렇기 때문에 Stream 환경에서도 람다나 메서드 참조를 사용할 때 지역 변수를 접근할 수가 없게 된다.

 

스트림의 동작 순서

Arrays.stream(new String[] {"c", "python", "java"})
	.filter(word -> {
		System.out.println("filter method : " + word);
		return word.length() > 3;
	})
	.map(word -> {
		System.out.println("map method : " + word);
		return word.substring(0, 3);
	})
	.findFirst();

Stream의 동작 순서를 확인하기 위한 예제이다. 3글자가 넘는 요소에 대해서 앞의 3글자만 자르고 그를 만족하는 첫번째 요소를 찾는 Stream이다. 순서를 확인하기 위해 중간에 print를 넣었다. 결과값은 다음과 같다.

3개의 요소에 대해서 각 3번씩 호출이 이루어질 것 같지만 위와 같이 출력이 되었다. Stream에서는 모든 요소가 중간 처리를 수행하는 것이 아니라 요소 하나씩  각 각 모든 파이프라인을 수행하게 된다. 다음과 같은 단계로 수행이 이루어졌다.

  • 배열의 첫 번째 요소 "c"
    • filter 메서드가 실행되지만 length가 3보다 크지 않으므로 다음으로 진행되지 않음
    • "filter method : c 출력"
  • 배열의 두 번째 요소 "python"
    • filter 메서드가 실행되면 length가 3보다 크기때문에 다음으로 진행
    • "filter method : python 출력"
    • map 메서드에 의해 substring 수행
    • "map method : python 출력"
    • 최종 연산인 findFirst 수행
  • 배열의 세 번째 요소 "java"
    • 조건에 맞는 "python"을 찾아 최종 연산을 수행하였기에 "java"에 대한 연산은 어떤것도 수행하지 않음

 

 

 

출처
https://madplay.github.io/post/mistakes-when-using-java-streams
https://ecsimsw.tistory.com/entry/%EC%9E%91%EC%84%B1-%EC%A4%91-%EC%8A%A4%ED%8A%B8%EB%A6%BC-%EC%82%AC%EC%9A%A9-%EC%8B%9C-%EC%A3%BC%EC%9D%98%ED%95%A0-%EC%A0%90
https://leeyongjin.tistory.com/entry/Java8-%EC%9E%90%EB%B0%948-Stream-API-%EC%A3%BC%EC%9D%98%EC%82%AC%ED%95%AD
https://digitalbourgeois.tistory.com/68
https://darkstart.tistory.com/199
https://otrodevym.tistory.com/entry/java-%EA%B8%B0%EC%B4%88-16-%EC%8A%A4%ED%8A%B8%EB%A6%BC%EA%B3%BC-%EB%B3%91%EB%A0%AC-%EC%B2%98%EB%A6%AC
https://ddoongmause.blogspot.com/2021/01/chapter16.html
https://velog.io/@jakeseo_me/%EC%9D%B4%EA%B2%83%EC%9D%B4-%EC%9E%90%EB%B0%94%EB%8B%A4-%EC%A0%95%EB%A6%AC-16-%EC%8A%A4%ED%8A%B8%EB%A6%BC
반응형