스트림(Stream)

Jobmania·2023년 2월 24일
0

JAVA

목록 보기
3/7
post-thumbnail

스트림이란?

🎇 '데이터 처리연산을 지원하도록 소스에서 추출된 연속된 요소'

기존의 Java에서 컬렉션 데이터를 처리할때 특정 조건에 따라 필터링을 하려면 복잡한 과정을 거쳐야 했음.반면에 SQL 문법의 경우 사용자가 원하는 조건의 데이터 목록을 검색할때 명시적이고 간단한 방법을 이용했는데 Java8 에서 새로 추가된 기능인 스트림은 Java의 컬렉션 데이터에 대해 SQL 질의문 처럼 데이터를 처리할수 있는 기능을 가지고 있습니다.

스트림은 '데이터의 흐름’입니다. 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있습니다. 또한 람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있습니다. 즉, 배열과 컬렉션을 함수형으로 처리할 수 있습니다.

기존 루프문 처리의 문제점

기존 Java에서 컬렉션 데이터를 처리할때는 for, foreach 루프문을 사용하면서 컬렉션 내의 요소들을 하나씩 다루었습니다. 간단한 처리나 컬렉션의 크기가 작으면 큰 문제가 아니지만 복잡한 처리가 필요하거나 컬렉션의 크기가 커지면 루프문의 사용은 성능저하를 일으키게 되었습니다.

스트림 구조

스트림에 대한 내용은 크게 세 가지로 나눌 수 있습니다.

  • 생성하기 : 스트림 인스턴스 생성.
    - Source : 컬렉션, 자료 , 데이터 등에 스트림 인스턴스 생성
  • 가공하기 : 필터링(filtering) 및 맵핑(mapping) 등 원하는 결과를 만들어가는 중간 작업
    - 중간연산 : 하지 않거나(0) , 여러개의 중간 연산을 할 수 있다.
    순서는 filter가 처음 순서에 있는다. (조건에 맞는 데이터만 추출) / filter, sort
  • 결과 만들기 : 최종적으로 결과를 만들어내는 작업(terminal operations).
    - 최종연산: forEach() 모든 요소를 하나씩 꺼내서 연산을 실행 / count(), max(), sum(), min() 등

연속된 요소 (Sequence of element)

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

소스 (Source)

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

데이터 처리 연산

스트림은 함수형 프로그래밍에서 지원하는 연산과 데이터베이스의 SQL 질의형과 비슷한 연산을 처리할수 있습니다. filter, sort, map, match 등으로 데이터를 조작할수 있고 순차적 혹은 병렬로 실행할수 있습니다.

파이프라이닝과 내부 반복
또한 스트림에는 파이프라이닝, 내부 반복 이라는 중요한 특징이 있습니다.

파이프라이닝 (Pipelining)

스트림 연산들은 서로 연결하여 큰 파이프 라인을 구성할수 있도록 스트림 자신을 반환합니다. 데이터 소스에 적용하는 데이터베이스 질의문과 비슷합니다.

내부 반복

반복자를 이용하여 명시적으로 반복하는 컬렉션과 다르게 스트림은 내부 반복 기능을 제공합니다.

++ 컬렉션의 경우 foreach 문법을 사용하여 사용자가 반복문을 직접 명시해야 하는데 이를 외부반복 이라 하고 스트림은 라이브러리를 사용하는 내부반복 개념입니다.

스트림 사용에 따른 비교

스트림 사용 X

// 각 필터링 단계마다 코드를 작성해야한다..
// 빨간색 사과 필터링
List<Apple> redApples = forEach(appleList, (Apple apple) -> apple.getColor().equals("RED"));

// 무게 순서대로 정렬
redApples.sort(Comparator.comparing(Apple::getWeight));

// 사과 고유번호 출력
List<Integer> redHeavyAppleUid = new ArrayList<>();
for (Apple apple : redApples)
    redHeavyAppleUid.add(apple.getUidNum());

스트림 사용 O

List<Integer> redHeavyAppleUid = appleList.stream()
        .filter(apple -> apple.getColor().equals("RED"))        // 빨간색 사과 필터링
        .sorted(Comparator.comparing(Apple::getWeight))         // 무게 순서대로 정렬
        .map(Apple::getUidNum).collect(Collectors.toList());    // 사과 고유번호 출력

또한 스트림은 paralleStream 메서드를 통해 별도의 멀티스레드 구현 없이도 병렬처리가 가능

List<Integer> redHeavyAppleUid = appleList.parallelStream()     // 병렬 처리
        .filter(apple -> apple.getColor().equals("RED"))        // 빨간색 사과 필터링
        .sorted(Comparator.comparing(Apple::getWeight))         // 무게 순서대로 정렬
        .map(Apple::getUidNum).collect(Collectors.toList());    // 사과 고유번호 출력

스트림 특징 정리

  • 선언형 : 더 간결하고 가독성이 좋아진다.
  • 함수의 조립 : 유연성이 좋아진다.
  • 병렬화 : 성능이 좋아진다.

스트림과 컬렉션 비교.

스트림과 컬렉션은 둘다 연속된 요소형식의 값을 저장하는 자료구조 인터페이스를 제공합니다. 그렇다면 컬렉션과 스트림은 어떤 차이점이 존재할까요??

비유적인 표현으로 HDD에 저장된 영상파일과 인터넷에있는 유튜브 영상을 비교해볼수 있습니다.

  • HDD에 저장된 영상파일의 경우 내가 보고싶은 지점을 클릭하면 바로 재생되지만 유튜브같은 온라인 스트리밍 동영상 같은 경우는 클릭한 재생 지점 근처만 로딩이 되고 다른 부분을 클릭하면 그때마다 데이터를 다시 읽어서 재생하는 방식입니다.

1. 데이터 계산 시점

이처럼 스트림과 컬렉션의 차이는 데이터를 언제 계산하는지 입니다.

컬렉션 : 모든 요소는 컬렉션에 추가하기전에 계산되어야 한다.
스트림 : 요청할때만 요소를 계산하는 고정된 자료구조

사용자가 요청하는 값만 추출할수 있는 특성때문에 스트림은 컬렉션보다 프로그래밍에 장점이 있습니다.만약 소수의 집합을 만들고 사용자가 원하는 지점의 소수를 알고싶다고 할때 컬렉션의 경우 소수를 만드는 과정에서 무한루프에 빠져버리기 때문에 사용자는 원하는것을 얻지 못할것입니다.
반면 스트림의 경우 사용자가 요청할때만 값을 계산하므로 이러한 문제에 유연하게 대처할수 있습니다.

2. 반복의 일회성

컬렉션과 스트림은 반복처리를 할때도 차이가 있습니다. 컬렉션의 경우 같은 소스에 대하여 여러번
반복 처리를 할수 있지만 스트림은 단 한번만 반복문을 처리할수 있습니다. 스트림에서는 소비(Consumer) 개념을 쓰기 때문에 한번 소비한 요소에 대해서 접근할수 없기 때문입니다.

Stream<Food> s = foodList.stream();
s.forEach(System.out::println); // 정상
s.forEach(System.out::println); // IllegalStateException 발생

만약 위 코드를 실행한다면 stream has already been operated upon or closed 라는 에러와 함께 프로그램이 중단될것입니다.

3. 외부반복, 내부반복

컬렉션의 경우 foreach 문법을 사용하여 사용자가 반복문을 직접 명시해야 하는데 이를 외부반복 이라 하고 스트림은 라이브러리를 사용하는 내부반복 개념입니다.

 [컬렉션]
List<String> foodNameList = new ArrayList<>();
for(Food food : foodList){
   foodNameList.add(food.getName());
}


[스트림]
List<String> foodNameList = foodList.stream()
       .map(Food::getName)
       .collect(Collectors.toList());

컬렉션과 다르게 스트림은 별도의 반복자 없이도 반복문을 처리할수 있습니다. 스트림이 사용하는 내부반복의 장점은 작업을 병렬로 처리할수 있고 더 최적화된 다양한 순서로 처리할수 있다는 점입니다.

파이프 라인 연산

스트림 연산 'java.util.stream.Stream' 에는 스트림 API에서 제공하는 여러가지 연산이 정이 되있는데 스트림 연산들은 크게 중간연산, 최종연산으로 구분할수 있습니다.

중간연산 : 파이프라인으로 연결할수 있는 연산들
최종연산 : 파이프라인을 실행한다음 닫는 연산

List<String> highCaloriesFoodName = foodList.stream()
        .filter(food -> food.getCalories() > 400)	// 중간연산
        .map(Food::getName)							// 중간연산
        .limit(3)									// 중간연산
        .collect(Collectors.toList());				// 최종연산

위 예제코드에서 보면 filter, map, limit은 중간연산이고 collect는 최종연산 입니다.

중간연산

filter나 map 같은 중간연산은 다른 스트림을 반환하기 때문에 여러개의 중간연산을 연결하여 질의를 만들수 있습니다. 중요한 특징은 최종연산을 실행하기 전까지는 아무 연산도 수행하지 않는다는 것입니다.
즉 중간 스트림이 생성될 때, 바로 중간처리를 진행하는 것이 아니라, 최종처리가 시작되기 전까지 중간처리는 Lazy 된다. 최종 처리가 시작되면 그때 컬렉션 요소들이 중간 스트림에서 처리 → 최종 처리가 된다.



List<Food> foodList = new ArrayList<>();
foodList.add(new Food("FlatBread",true,400,Food.Type.OTHER));
foodList.add(new Food("OnionSoup",true,300,Food.Type.OTHER));
foodList.add(new Food("LobsterRisotto",false,520,Food.Type.FISH));
foodList.add(new Food("CaesarSalad",true,200,Food.Type.OTHER));
foodList.add(new Food("BeefWellington",false,670,Food.Type.MEAT));
foodList.add(new Food("FiletMignon",false,600,Food.Type.MEAT));
foodList.add(new Food("CrispySalmon",false,620,Food.Type.FISH));
foodList.add(new Food("StripSteak",false,740,Food.Type.MEAT));
foodList.add(new Food("SearedScallops",false,340,Food.Type.FISH));


List<String> highCaloriesFoodName = foodList.stream()
        .filter(food -> {
            System.out.println("filter : " + food.getName());
            return food.getCalories() > 400;
        })
        .map(food -> {
            System.out.println("map : " + food.getName());
            return food.getName();
        })
        .limit(3)
        .collect(Collectors.toList());

System.out.println(highCaloriesFoodName);
 

[출력]
filter : FlatBread
map : FlatBread
filter : OnionSoup
filter : LobsterRisotto
map : LobsterRisotto
filter : CaesarSalad
filter : BeefWellington
map : BeefWellington
[FlatBread, LobsterRisotto, BeefWellington]

OnionSoup, CaesarSalad는 filter에서 필터링 되었기 때문에 map에서는 찍히지 않고 나머지 음식들이 최종연산되어 출력되는것을 확인할수 있습니다.

최종연산

최종연산은 파이프라인 연산의 결과를 출력합니다. 예제에서는 List 형태로만 결과를 받았지만 이 외에도 Integer, void 등 다양한 형태로 출력할수 있습니다.

스트림 이용하기 요약
스트림을 사용하는 단계는 다음과 같이 3단계에 걸쳐서 진행됩니다.

질의를 수행할 데이터소스 (ex 컬렉션)
스트림 파이프라인을 구성할 중간 연산
스트림 연산을 실행하고 결과로 출력할 최종연산


public class Quiz1 {

   private List<String[]> readCsvLines() throws IOException, CsvException {
       CSVReader csvReader = new CSVReader(new FileReader("user.csv"));
       csvReader.readNext(); // 제목줄 읽기
       return csvReader.readAll();
   }

   public static void main(String[] args) throws IOException, CsvException {
       Quiz1 q1 = new Quiz1();
       q1.readCsvLines().forEach(arr -> System.out.println(String.join(",", arr)));
       System.out.println("================");
       System.out.println(q1.quiz1());
       System.out.println("=============");
       System.out.println(q1.quiz2());
       System.out.println("=============");
       System.out.println(q1.quiz3());


   }

   // 1.1 각 취미를 선호하는 인원이 몇 명인지 계산하여라.
   public Map<String, Integer> quiz1() throws IOException, CsvException {
       List<String[]> csvLines = readCsvLines();
       return csvLines.stream()
               .map(line -> line[1].replaceAll("\\s", ""))  // 공백 제거
               .flatMap(hobbies -> Arrays.stream(hobbies.split(":")))// 개발:축구:농구: -> 개발 /  하나씩 분리하여 스트림으로
               .collect(Collectors.toMap(hobby -> hobby, hobby -> 1, (oldValue, newValue) -> newValue += oldValue));  // 중복된 값은 이전 값을 더해줌 //
       // Map 변환중 중복키가 발생할 경우 ..
       // 기존 값을 유지할 경우
       // .collect(Collectors.toMap(vo->vo, vo->vo, (oldValue, newValue) -> oldValue));
       // 새로운 값을 유지할 경우
       // .collect(Collectors.toMap(vo->vo, vo->vo, (oldValue, newValue) -> newValue));
       //https://osc131.tistory.com/148
   }




//    1.2 위와 같은 데이터를 조회하여 각 취미를 선호하는 정씨 성을 갖는 인원이 몇 명인지 계산하여라.

   public Map<String, Integer> quiz2() throws IOException, CsvException {
       List<String[]> csvLines = readCsvLines();
       return csvLines.stream()
               .filter(line -> line[0].startsWith("정"))
               .map(line -> line[1].replaceAll("\\s", ""))
               .flatMap(hobbies -> Arrays.stream(hobbies.split(":")))
               .collect(Collectors.toMap(hobby -> hobby, hobby -> 1, (oldValue, newValue) -> ++newValue));
   }

//1.3 위와 같은 데이터를 조회하여 소개 내용에 '좋'가 몇번 등장하는지 계산하여라.

	public int quiz3() throws IOException, CsvException {
		List<String[]> csvLines = readCsvLines();
		return csvLines.stream()
				.map(line -> countContains(line[2], 0))
               .reduce(0, Integer::sum);   //  Stream에서 전달되는 요소들의 숫자를 모두 합
	}

   private int countContains(String src, int fromIndex) {
       int index = src.indexOf("좋", fromIndex);  // index =  -1은 값이 없음을 의미
       if (index >= 0) {
           return 1 + countContains(src, index + "좋".length());
       }
       return 0;
   }


}

https://ksr930.tistory.com/237
https://velog.io/@dev_jhjhj/Java-Stream%EA%B3%BC-%EB%B3%91%EB%A0%AC%EC%B2%98%EB%A6%AC-1

profile
HelloWorld에서 RealWorld로

0개의 댓글