스트림

mmmhmm·2024년 2월 28일

이것이 자바다

목록 보기
11/11

스트림이란?

스트림은 요소들이 하나씩 흘러가면서 처리된다는 의미를 가지고 있다. List 컬렉션에서 요소를 반복 처리하기 위해 스트림을 사용하면 다음과 같다.

Stream<String> stream = list.stream();
stream.forEach( item -> item 처리 );

List 컬렉션의 stream() 메소드로 Stream 객체를 얻고, forEach() 메소드로 요소를 어떻게 처리할지를 람다식으로 제공한다.

public class StreamExample {
	public static void main(String[] args){
    	//Set 컬렉션 생성
        Set<String> set = new HashSet<>();
        set.add("홍길동");
        set.add("신용권");
        set.add("김자바");
        
        //Stream을 이용한 요소 반복 처리
        Stream<String> stream = set.stream(); // 스트림 얻기
        stream.forEach( name -> System.out.println(name) ); // 람다식 : 요소 처리 방법
    }
홍길동
신용권
김자바

Stream은 Iterator와 비숫한 반복자이지만, 다음과 같은 차이점을 가지고 있다.

1) 내부 반복자이므로 처리 속도가 빠르고 병렬 처리에 효율적이다.

2) 람다식으로 다양한 요소 처리를 정의할 수 있다.

3) 중간 처리와 최종 처리를 수행하도록 파이프 라인을 형성할 수 있다.

내부 반복자

for문과 iterator는 컬렉션의 요소를 컬렉션 바깥쪽으로 반복해서 가져와 처리하는데, 이것을 외부 반복자라고 한다. 반면 스트림은 요소 처리 방법을 컬렉션 내부로 주입시켜서 요소를 반복 처리하는데, 이것을 내부 반복자라고 한다.

외부 반복자일 경우는 컬렉션의 요소를 외부로 가져오는 코드와 처리하는 코드를 모두 개발자 코드가 가지고 있어야 한다. 반면 내부 반복자일 경우는 개발자 코드에서 제공한 데이터 처리 코드(람다식)를 가지고 컬렉션 내부에서 요소를 반복 처리한다.
내부 반복자는 멀티 코어 CPU를 최대한 활용하기 위해 요소들을 분배시켜 병렬 작업을 할 수 있다.
하나씩 처리하는 순차적 외부 반복자보다는 효율적으로 요소를 반복시킬 수 있는 장점이 있다.

다음 예제는 List 컬렉션의 내부 반복자를 이용해서 병렬 처리하는 방법을 보여준다. parallelStream() 메소드로 병렬 처리 스트림을 얻고, forEach() 메소드를 호출할 때 요소 처리 방법인 람다식을 제공한다. 람다식은 처리되는 요소가 무엇이고, 어떤 스레드가 처리하는지를 출력한다.

public class ParallelStreamExample{
	public static void main(String[] args){
    	//List 컬렉션 생성
        List<String> list = new ArrayList<>();
        list.add("홍길동");
        list.add("신자바");
        list.add("스레드");
        list.add("람다식");
        list.add("박병렬");
        
        //병렬처리
        Stream<String> parallelStream = list.parallelStream();
        parallelStream.forEach( name -> {
        	System.out.println( name + ": " + Thread.currentThread().getName()); 
        });
    }
}
실행결과
스레드 : main
람다식 : ForkJoinPool.commonPool-worker-2
홍길동 : ForkJoinPool.commonPool-worker-2
박병렬 : main
신자바 : ForkJoinPool.commonPool-worker-1

중간 처리와 최종 처리

스트림은 하나 이상 연결될 수 있다. 컬렉션의 오리지널 스트림 뒤에 필터링 중간스트림이 연결될 수 있고, 그 뒤에 매핑 중간 스트림이 연결될 수 있다. 이와 같이 스트림이 연결되어 있는 것을 스트림 파이프라인이라고 한다.

오리지날 스트림과 집계 처리 사이의 중간 스트림들을 최종 처리를 위해 요소를 걸러내거나(필터링), 요소를 변환시키거나(매핑), 정렬하는 작업을 수행한다. 최종 처리는 중간 처리에서 정제된 요소들을 반복하거나, 집계( 카운팅, 총합, 평균) 작업을 수행한다.
다음 그림은 Student 객체를 요소로 가지는 컬렉션에서 Student 스트림을 얻고, 중간 처리를 통해 score 스트림으로 변환한 후 최종 집계 처리로 score 평균을 구하는 과정을 나타낸 것이다.

코드로 변환하면 다음과 같다.

//Student 스트림
Stream<Student> studentStream = list.stream();
//score 스트림
IntStream scoreStream = studentStream.mapToInt( student -> student.getScore() ); 
												//Student 객체를 getScore()메소드의 리턴값으로 매핑
//평균 계산
double avg = scoreStream.average().getAsDouble();

mapToInt() 메소드는 객체를 int 값으로 매핑해서 intStream으로 변환시킨다. 어떤 객체를 어떤 int 값으로 매핑할 것인지는 람다식으로 제공해야 한다. student -> student.getScore()는 Student 객체를 getScore()의 리턴값으로 매핑한다. IntStream은 최종 처리를 위해 다양한 메소드를 제공하는데, average() 메소드는 요소들의 평균 값을 계산한다.

메소드 체이닝 패턴을 이용하면 앞의 코드를 다음과 같이 더 간결하게 작성할 수 있다.

double avg = list.stream()
	.mapToInt(student -> student.getScore())
    .average()
    .getAsDouble();

스트림 파이프라인으로 구성할 때 주의할 점은 파이프라인의 맨 끝에서 반드시 최종 처리 부분이 있어야 한다는 것이다. 최종 처리가 없다면 오리지널 및 중간 처리 스트림은 동작하지 않는다. 즉, 위 코드에서 average() 이하를 생략하면 stream(), mapToInt()는 동작하지 않는다.

리소스로부터 스트림 얻기

java.util.stream 패키지에는 스트림 인터페이스들이 있다. BaseStream 인터페이스를 부모로 한 자식 인터페이스들은 다음과 같은 상속 관계를 이루고 있다.


BaseStream에는 모든 스트림에서 사용할 수 있는 공통 메소드들이 정의되어 있다. Stream은 객체 요소를 처리하는 스트림이고, IntStream, LongStream, DoubleStream은 각각 기본 타입인 int, long, double 요소를 처리하는 스트림이다.
이 스트림 인터페이스들의 구현 객체는 다양한 리소스로부터 얻을 수 있다. 주로 컬렉션과 배열에서 얻지만, 다음과 같은 리소스로부터 스트림 구현 객체를 얻을 수도 있다.

컬렉션으로 부터 스트림 얻기

java.util.Collection 인터페이스는 스트림과 parallelStream() 메소드를 가지고 있기 때문에 자식 인터페이스인 List와 Set 인터페이스를 구현한 모든 컬렉션에서 객체 스트림을 얻을 수 있다. 다음 예제는 List<Product> 컬렉션에서 Product 스트림을 얻는 방법을 보여준다.

Product.java

public class Product {
	private int pno;
    private String name;
    private String company;
    private int price;
    
    public Product(int pno, String name, String company, int price){
    	this.pno = pno;
        this.name = name;
        this.company = company;
        this.price = price;
    }
    
    public int getPno() { return pno; }
    public String getName() { return name; }
    public String getCompany() { return company; }
    public int getPrice() { return proce; }
    
    @Override
    public String toString() {
    	return new StringBuilder()
        	.append("{")
            .append("pno:"+pno+", ")
            .append("name:"+name+", ")
            .append("company:"+company+", ")
            .append("price:"+price+", ")
            .append("}")
            .toString();
    }
}
StreamExample.java
public class StreamExample{
	public static void main(String[] args){
    	//List 컬렉션 생성
        List<Product> list = new ArrayList<>();
        for(int i = 1; i<=5; i++){
        	Product product = new Product(i, "상품"+i, "멋진 회사 ", (int)
								(10_000*Math.random()));
			list.add(product);
        }
        
        //객체 스트림 얻기
        Stream<Product> stream = list.stream();
        stream.forEach( p -> System.out.println(p));
    }
}
실행결과
[pno:1, name:상품1, company:멋진 회사, price:2029}
[pno:2, name:상품2, company:멋진 회사, price:2534}
[pno:3, name:상품3, company:멋진 회사, price:8868}
[pno:4, name:상품4, company:멋진 회사, price:9487}
[pno:5, name:상품5, company:멋진 회사, price:1245}

배열로부터 스트림 얻기

String[] strArray = { "홍길동", "신용건", "김미나"};
Stream<String> strStream = Arrays.stream(strArray);
strStream.forEach( item -> System.out.println(item + ","));

int[] intArray = { 1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(intArray);
intStream.forEach( item -> System.out.println(item + ","));
System.out.println();
실행결과
홍길동,신용권,김미나
1,2,3,4,5,

그 외에 숫자 범위로 스트림 얻기, 파일로부터 스트림 얻기등 다양한 형태가 있다.

그리고 요소를 걸러낼수 있는(필터링) 중간 처리 기능도 있다.

또 요소 변환(매핑)을 해주는 중간 처리 기능도 있다.

요소를 복수개의 요소로 변환도 가능하다.

나머지 요소 처리 방법은 따로 기술해놓지 않겠다.

마무리

스트림의 특징

  • 자료의 대상과 관계없이 동일한 연산을 수행 ( 사용방법을 일관되게 )
  • 한 번 생성하고 사용한 스트림은 재사용할 수 없다.
  • 스트림의 연산은 기존 자료를 변경하지 않는다.
  • 스트림의 연산은 중간 연산과 최종 연산이 있다. 중간 연산이 호출되어도 최종 연산이 호출되지 않으면 결과를 가져올수 없다.(지연연산(Lazy evaluation)) 최종 연산이 호출되어야 비로소 연산이 시작된다.
profile
어라? 금지

0개의 댓글