Java

Stream에 대하여

Andrew-Yun 2022. 2. 10. 20:13

자바 8에서 스트림이란 기능이 추가되었다. 스트림이란 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임이다. 유닉스 계열 운영체제의 명령어 중 파이프라인과 유사한 동작을 하는데, 출력의 스트림은 입력 스트림이 될 수 있다.

 

본격적으로 스트림에 대해 알아보자.

명령을 파이프라인으로 처리하면 무엇이 좋길래 추가한걸까 자바에서는 우리가 하려는 작업을 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있는게 목적이라 생각한다. (마치 데이터베이스 질의처럼)

 

연속된 요소: 스트림은 컬렉션과 마찬가지로 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스를 제공한다. 컬렉션은 자료구조이므로 시간과 공간의 복잡성과 관련된 요소 저장 및 접근 연산이 주를 이루지만, 스트림에서는 filter, sorted, map처럼 표현 계산식이 주를 이룬다. 즉, 컬렉션의 주체는 데이터고 스트림의 주체는 계산이다.

 

소스: 스트림은 컬렉션, 배열, I/O 자원 등의 데이터 제공 소스로부터 데이터를 소비한다. 정렬된 컬렉션으로 스트림을 생성하면 정렬이 그대로 유지된다. 즉, 리스트로 스트림을 만들면 스트림의 요소는 리스트의 요소와 같은 순서를 유지한다.

 

데이터 처리 연산: 스트림은 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 데이터베이스와 비슷한 연산을 지원한다. 예를 들어 filter, map, reduce, find, match, sort 등으로 데이터를 조작할 수 있다. 또한 순차, 병렬 실행이 모두 가능하다.

 

filter: 특정 요소를 걸러낸다. filter를 사용하기 위해서 predicate라는 함수형 인터페이스를 구현해야 하는데, 함수형 인터페이스는 뒤에서 다루겠다. boolean을 리턴하여 조건을 표현하는 것만 알면 된다.

List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
//짝수들만 리스트에 담겨 반환된다.
List<Integer> result = nums.stream().filter(i -> i % 2 == 0).collect(Collectors.toList());

 

map: 각 요소에 대해 함수를 매핑시켜 수행 결과를 다음 스트림에 반환한다.

ArrayList<String> list = Arrays.asList("aaa", "bbb", "ccc");
//"AAA", "BBB", "CCC"와 같이 대문자로 변환된 문자열들이 담긴다.
List<String> result = list.stream().map(s -> s.toUpperCase()).collect(Collectors.toList());

 

reduce: 스트림의 요소들을 모아 새로운 결과를 내놓는다.

List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
//1~5의 합이 sum1에 반환된다.
Integer sum1 = nums.stream().reduce((sum, i) -> sum + i);
//초깃값 10에 1~5의 누적합이 더해져 반환된다.
Integer sum2 = nums.stream().reduce(10, (sum, i) -> sum + i);

스트림의 병렬 처리는 parallel 키워드를 붙여 수행할 수 있는데, 주의할 점은 병렬로 처리한다 해서 빨라지지 않을 수도 있다. 스레드를 분할하고 Join하는 과정에서 드는 오버헤드가 더 클 수 있기 때문이다.

작은 크기의 데이터가 상당히 많을 때 사용하면 유리할 것 같단 생각이 들었다.

 

스트림에는 두 가지 주요 특징이 있는데, 커다란 파이프라인을 구성할 수 있도록 스트림 자신을 반환한다. 그 덕분에 lazyness, short-circuit과 같은 최적화도 얻을 수 있다.

 

lazyness: 스트림의 중간 연산은 항상 스트림을 반환하고, 최종 연산을 수행해야 스트림 내부가 수행이 되므로 게으르게 수행된다.

 

short-circuit: 전체 스트림을 처리하지 않더라도 결과를 반환할 수 있다. 예를 들어 여러 and 연산으로 연결된 커다란 불리언 표현식을 보면, 하나라도 거짓인 결과가 나오면 표현식에 결과와는 상관없이 전체 결과도 거짓이 된다. 이러한 상황을 쇼트 서킷이라 부른다.

내부 반복: 반복자 (for)를 이용해서 명시적으로 반복하는 컬렉션과 달리 스트림은 내부 반복을 지원한다.

 

 

의문점: 스트림 vs 내부 반복의 성능 차이 (내용이 길어 첨부)

요지는 벤치마킹에 따라 다르지만, 외부 반복에 비해 대체적으로 성능이 좋진 않다. 하지만 유지보수 비용도 고려하여 트레이드 오프하자.

https://jypthemiracle.medium.com/java-stream-api%EB%8A%94-%EC%99%9C-for-loop%EB%B3%B4%EB%8B%A4-%EB%8A%90%EB%A6%B4%EA%B9%8C-50dec4b9974b

 

참고: 외부 반복이란 for문 등으로 외부에서 반복하는 것을 의미. 내부 반복이란 stream 내부에서 iteraor를 통해 반복하는 것을 의미.

 

개인적인 의문에 대한 자문자답

Q. 함수형 프로그래밍이 주는 이득은 무엇이 있는가

  - 함수의 동작이 명확하기 때문에 가독성이 좋아짐

  - 함수 내부에 대입문이 없기 때문에 참조 투명성을 얻을 수 있음

Q. 자바 8에서는 왜 함수형 프로그래밍이 도입되었는가

  - 멀티코어 CPU가 도입됨에 따라 코어를 병렬적으로 사용해야 함.

  - 기존 자바로는 구현하기 어려워 함수형 인터페이스와 비동기 논블로킹으로 병렬 프로세싱을 구현함.