Skip to content

java java.util.stream.Stream #

Find similar titles

6회 업데이트 됨.

Edit

Structured data

Category
Programming

Lambda란? #

Stream에 대해 알아보기 전 간단하게 Lambda에 대해 알아보자. JDK 8에서 추가된 Lambda식은 메소드를 하나의 식으로 표현하여 메소드를 더욱 간결하게 표현할 수 있다는 특징을 가지고 있다. Lambda식을 사용할 경우 불필요한 코드를 줄이고 가독성을 향상하는 데에 큰 기여를 한다. 주로 연산 작업, 반복 작업에 있어 코드를 간결하게 하고 멀티스레드를 활용해 병렬처리를 하는 것에 목적을 두고 사용된다. 이 Lambda식을 활용한 API가 Stream이며 Stream은 어떤 특징이 있을지 알아보자.

Java 8 Stream 이란? #

JDK 8에서 추가된 API로 배열 또는 컬렉션 인스턴스를 다루기 위해 for문 또는 foeach문을 돌면서 요소 하나씩을 꺼내서 다루는 방법을 대체하는 방식으로 람다를 활용할 수 있는 Java Util 중 하나이다. Stream을 활용할 경우 반복에 필요한 for문, 분기 처리에 필요한 if문 등을 사용하지 않아 더욱 직관적인 코드를 작성할 수 있다.

Java 8 Stream 특징 #

Java 8 Stream은 원본 데이터를 읽는 기능만 할 뿐 원본데이터 자체를 변경하지 않는다. 그렇기 때문에 원본 데이터가 변형될 걱정은 하지 않아도 된다. 또한 Java 8 Stream은 일회성이기 때문에 한 번 사용될 경우 재사용이 불가능하다. 즉 필요하다면 정렬된 결과를 배열 혹은 컬렉션에 담아 반환해야 한다. Java 8 Stream도 기존 방식과 마찬가지로 작업을 내부적으로 반복하여 처리한다. 반복 코드는 메소드 내부에 숨어져 있어 코드 상에 노출이 되지 않아 더욱 깔끔한 비즈니스 로직을 설계할 수 있다.

Java 8 Stream 예제 #

Java 8 Stream은 주로 배열 혹은 컬렉션 타입에 많이 사용된다. 몇 개의 간단한 예제를 한 번 살펴보자.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class streamTest {
    List<String> list = Array.asList("a", "b", "c", "d", "e");
}

// 기존 확장 for문
List<String> listUpperCase = new ArrayList<>();
for(String str : list){
    listUpperCase.add(str.toUpperCase());
}

System.out.println(list);  // [a, b, c, d, e]
System.out.println(listUpperCase);  // [A, B, C, D, E]

// Stream문법
List<String> stream = list.stream().map(String::toUpperCase).collect(collectors.toList());
System.out.println(stream);  // [A, B, C, D, E]

// Stream문법을 활용하여 연산 처리
List<Integer> numList = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> numStream = numList.stream().map(n -> n * 3).collect(Collectors.toList());
System.out.println(numStream);  // [3, 6, 9, 12, 15, 18]

Java 8 Stream 단점? #

Java 8 Stream은 lambda를 활용하는 API인 만큼 람다의 단점을 그대로 가지고 있기도 하다. 하지만 여기선 Lambda식보단 Stream 사용 시 주의해야할 단점을 위주로 서술 할 예정이다. 위에 특징에도 재사용이 불가능하다는 단점이 있지만 이는 배열과 컬렉션을 활용해 해결이 가능하기 때문에 치명적인 단점에는 속하지 않는다. 또한 의도치 않은 무한 스트림 생성과 같은 문제는 개발자에 의해 발생되는 문제인데 자바에서 제공하는 Util API를 사용함에 있어 주의사항이 있다는 것 자체가 단점일수도 있을 것이다. 하지만 Java 8 Stream이 갖는 가장 큰 단점은 성능에 있다. 물론 단순한 로직, 데이터의 양이 적다면 for문에 비해 성능이 떨어질 수 밖에 없는 것은 당연하다. 하지만 데이터의 양이 많을 경우에도 for문보다 성능이 떨어지는 경우가 존재한다. 예를 들어 기본 primitive type데이터를 약 100만개 갖는 배열을 테스트 해보자.

// 기존 for-loop
int[] e = millionInt;  // 100만개의 정수를 갖고 있는 배열
int minVal = Integer.MIN_VALUE;
for( int i=0; i < e.length; i++ ){
    if(e[i] > minVal){
        minVal = e[i];
    }
}

// Java 8 Steram
int strmMinVal = Array.stream(millionInt).reduce(Integer.MIN_VALUE, Math::max);  // 위 for문과 동일한 결과를 갖는 Stream 문법

기존 for문은 이 데이터를 처리하는데 0.3 ~ 0.5ms가 소요된다. 반면 Stream문법은 5.3 ~ 6.2ms가 소요된다. 약 12배정도의 성능 차이를 보여주는데 기존 for문을 개선했다고는 볼 수 없는 수치이다. 이러한 이유는 무엇일까? 간단하게 알아보자면 컴파일러가 Stream을 아직 완벽하게 최적화하지 못한 부분에 있다. 비교적 최근(2015년)에 도입된 만큼 컴파일러가 이에 최적화되지 않은 것이다. 그렇다면 Java 8 Stream은 사용할 이유가 없을까? 테스트를 한 번 바꿔보자. 이번엔 primitive type이 아닌 Wrapped type으로, 배열이 아닌 List를 사용하여 마찬가지로 100만개의 데이터를 이용하여 테스트를 진행해보자.

// 기존 for-loop
ArrayList<Integer> e = new ArrayList<Integer>(millionInt);  // Integer Type을 갖는 Array List로 변경
int minVal = Integer.MIN_VALUE;
for( int i=0; i < e.size(); i++ ){
    if(e[i] > minVal){
        minVal = e[i];
    }
}

// Java 8 Steram
int strmMinVal = ArrayList.stream(millionInt).reduce(Integer.MIN_VALUE, Math::max);  // 위 for문과 동일한 결과를 갖는 Stream 문법

기존 for문은 7ms ~ 7.3ms, Stream문법은 8.5ms ~ 9.2ms로 차이가 눈에 띄게 줄은 것을 확인할 수 있다. 여기서 데이터의 양이 많아질 수록 Stream이 점점 for문보다 빨라지게 된다. 마지막으로 한번 더 테스트를 해보자. 위에서 진행한 테스트는 순회비용이 계산비용보다 높을 경우였는데 이번엔 반대로 원소 하나하나에 복잡한 로직을 담은 함수를 두어 계산비용이 높을 경우 결과가 어떨지 테스트 해보자. 테스트에 사용된 메소드는 계산비용이 높은 아파치 라이브러리의 slowSin()이다.

// 기존 for-loop
int[] e = millionInt;  // 메소드가 추가되어 약 10만개의 데이터로 변경
double minVal = Double.MIN_VALUE;
for( int i=0; i < e.length; i++ ){
    double d = Sine.slowSin(a[i]);
    if(e[i] > minVal){
        minVal = d;
    }
}

// Java 8 Steram Arrays.stream(millionInt).mapToDouble(Sine::slowSin).reduce(Double.MIN_VALUE, Math::max);  // 위 for문과 동일한 결과를 갖는 Stream 문법

결과는 기존 for문은 11.7 ~ 11.8ms Stream문법은 11.8ms로 차이가 거의 없어졌다. 이렇듯 복잡한 로직으로 인해 순회하는 비용보다 계산하는 비용이 크다면 Stream의 가치는 더욱 올라간다는 것이다.

java 8 Stream 사용 시기 #

위 내용들을 정리하자면 Java 8 Stream문법은 불필요한 for문과 if문을 줄여주어 코드가 더욱 간결하게 유지될 수 있도록 하며, 그만큼 유지보수에 있어 강점을 가지고 있다. 하지만 남발할 경우 성능 저하 및 가독성이 오히려 떨어지는 악효과가 있어 상황에 따라 적절하게 사용하는 것을 권장한다. 필자는 위 단점 파트에 정리된 것과 같이 복잡한 로직을 가지고 있는 메소드를 사용하게 되어 순회 비용 < 계산 비용일 경우 사용하는 것이 가장 베스트이겠지만, 분할이 잘 이루어질 수 있는 데이터 구조 혹은 연산 작업이 독립적이며 CPU사용이 높은 작업일 경우에도 충분히 사용할 여지가 있다고 생각한다. 이를 판단할 근거는 역시 테스트에 있으며, 테스트를 통해 for문과 Stream문법의 성능을 잘 비교하여 적절하게 사용하면 더욱 직관적인 코드를 작성할 수 있을 것이다.

참고출처 #

Stream (Java Platform SE 8)

0.0.1_20230725_7_v68