꾸준한 스터디
article thumbnail

스트림 API

다량의 데이터 처리 작업(순차적이든 병렬적이든)을 돕고자 자바8에서 추가되었다.

이 API가 제공하는 추상 개념 중 핵심은 두가지다.

첫 번째인 스트림은 데이터 원소의 유한 혹은 무한 시퀀스를 뜻한다.

두번째인 스트림 파이프라인은 이 원소들로  수행하는 연산 단계를 표현하는 개념이다.

스트림 안의 데이터 원소들은 객체 참조나 기본 타입값이다. 기본 타입값으로는 int, long, double 이렇게 세가지를 지원한다.

public static void main(String[] args) {
    List<OnlineClass> springClasses = new ArrayList<>();
    springClasses.add(new OnlineClass(1, "spring boot", true));
    springClasses.add(new OnlineClass(2, "spring data jpa", true));
    springClasses.add(new OnlineClass(3, "spring mvc", false));
    springClasses.add(new OnlineClass(4, "spring core", false));
    springClasses.add(new OnlineClass(5, "rest api development", false));
    
    springClasses.stream() // 소스 스트림
        .filter(onlineClass -> onlineClass.getTitle().startsWith("spring")) // 중간 연산
        .forEach(onlineClass -> System.out.println(onlineClass.getTitle())); // 종단 연산
}

 

Stream

  • sequence of elements supportin sequential and parallel aggregate operations
  • 데이터를 담고 있는 저장소 (컬렉션)이 아니다.
  • Function in nature, 스트림이 처리하는 데이터 소스를 변경하지 않는다.
  • 스트림으로 처리하는 데이터는 오직 한번만 처리한다.
  • 무제한일 수도 있다. (Short Curcuit 메서드를 사용해서 제한할 수 있다.)
  • 손쉽게 병렬 처리할 수 있지만 실제 효용성이 있는 경우는 적다.

스트림 파이프라인

  • 0 또는 다수의 중개 오퍼레이션 (intermediate operation)과 한개의 종료 오퍼레이션 (terminal operation)으로 구성한다.
  • 지연 평가(lazy evaluation)된다. 평가는 종단 연산이 호출될 때 이뤄지며, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다. 이러한 지연 평가가 무한 스트림을 다룰 수 있게 해주는 열쇠다.
  • 스트림의 데이터 소스는 오직 터미널 오퍼레이션을 실행할 때에만 처리한다.

중개 오퍼레이션

  • Stream을 리턴한다.
  • Stateless / Stateful 오퍼레이션으로 더 상세하게 구분할 수도 있다. (대부분은 Stateless지만 distinct나 sorted 처럼 이전 소스 데이터를 참조해야하는 오퍼레이션은 Stateful 오퍼레이션이다.)
  • filter, map, limit, skip, sorted...

종료 오퍼레이션

  • Stream을 리턴하지 않는다.
  • collect, allMatch, count, forEach, min, max...

Fluent API

  • 메서드 연쇄를 지원 -> 파이프라인 하나를 구성하는 모든 호출을 연결하여 하나의 표현식으로 완성 가능
  • 순차적으로 수행
  • 병렬 실행하려면 parallel 메서드 호

 

스트림 API 사용 예제

걸러내기

  • Filter(Predicate)
  • 예) 이름이 3글자 이상인 데이터만 새로운 스트림으로

변경하기

  • Map(Function) 또는 FlatMap(Function)
  • 예) 각각의 Post 인스턴스에서 String title만 새로운 스트림으로
  • 예) List<Stream<String>>을 String의 스트림으로

생성하기

  • generate(Supplier) 또는 Iterate(T seed, UnaryOperator)
  • 예) 10부터 1씩 증가하는 무제한 숫자 스트림
  • 예) 랜덤 int 무제한 스트림

제한하기

  • limit(long) 또는 skip(long)
  • 예) 최대 5개의 요소가 담긴 스트림을 리턴한다.
  • 예) 앞에서 3개를 뺀 나머지 스트림을 리턴한다.

스트림에 있는 데이터가 특정 조건을 만족하는지 확인

  • anyMatch(), allMatch(), nonMatch()
  • 예) k로 시작하는 문자열이 있는지 확인한다. (true 또는 false를 리턴한다.)
  • 예) 스트림에 있는 모든 값이 10보다 작은지 확인한다.

개수 세기

  • count()
  • 예) 10보다 큰 수의 개수를 센다.

스트림을 데이터 하나로 뭉치기

  • reduce(identify, BiFunction), collect(), sum(), max()
  • 예) 모든 숫자 합 구하기
  • 예) 모든 데이터를 하나의 List 또는 Set에 옮겨 담기

 

 

스트림API는 다재다능하여 사실상 어더한 계산이라도 해낼 수 있다.

하지만 할 수 있다는 뜻이지, 해야한다는 뜻은 아니다. 스트림을 제대로 사용하면 프로그램이 짧고 깔끔해지지만, 잘못 사용하면 읽기 어렵고 유지보수도 힘들어진다.

 

스트림을 사용하지 않은 anagram 예시

  • 아나그램이란 철자를 구성하는 알파벳이 같고 순서만 다른 단어
  • 'staple' 과 'petals' 는 동일한 글자로 이루어진 단어이다.

txt 파일을 만들어서 바탕화면에 저장한다.
원소수가 2개 이상인 anagram 그룹 출력

이 프로그램은 사전 파일에서 단어를 읽어 사용자가 지정한 문턱값보다 원 수가 많은 아나그램 그룹을 출력한다.

사용자가 명시한 사전 파일에서 각 단어를 읽어 맵에 저장한다. 맵의 키는 그 단어를 구성하는 철자들을 알파벳순으로 정렬한 값이다.

즉 "staple"의 키는 "aelpst"가 되고 "petals"의 키도 "aelpst"가 된다.

맵의 값은 같은 키를 공유한 단어들을 담은 집합이다. 사전 하나를 모두 처리하고 나면 각 집합은 사전에 등재된 아나그램들을 모두 담은 상태가 된다. 마지막으로 이 프로그램은 맵의 values() 메서드로 아나그램 집합들을 얻어 원소 수가 문턱값보다 많은 집합들을 출력한다.

 

 

스트림을 과하게 사용한 anagram 예시

스트림을 과용하면 프로그램이 읽거나 유지보수하기 어려워진다.

 

 

스트림을 적절히 사용한 anagram 예시

try-with-resources 블록에서 사전 파일을 열고 파일의 모든 라인으로 구성된 스트림을 얻는다.

스트림 이름을 words로 지어 스트림 안의 각 원소가 단어임을 명확히 했다.

이 스트림의 파이프라인에는 중간 연산은 없으며, 종단 연산에서 모든 단어를 수집해 맵으로 모은다.

이 맵은 단어들을 아나그램끼리 묶어놓은 것으로 앞선 두 프로그램이 생성한 맵과 실질적으로 같다.

그 다음 이 맵의 values()가 반환한 값으로부터 새로운 Stream<List<String>> 스트림을 연다.

리스트 중 원소가 minGroupSize보다 적은 것은 필터링돼 무시되고 forEach에서 살아남은 리스트를 출력한다.

 

alphabetize 메서드도 스트림을 사용해 다르게 구현할 수 있지만 명확성이 떨어지고 잘못 구현할 가능성이 커진다.

심지어 느려질수도 있는데 자바가 기본 타입인 char용 스트림을 지원하지 않기 때문이다.

char원소가 반환하는 스트림의 원소는 int여서 형변환을 명시적으로 해줘야 해서 char 값들을 처리할 때는 스트림을 삼가하는 편이 낫다.

 

스트림을 처음 쓰기 시작하면 모든 반복문을 스트림으로 바꾸고 싶은 유혹이 일지만 기존 코드는 스트림을 사용하도록 리팩토링하되, 새 코드가 나아보일 때만 반영하자

 

함수객체로는 할 수 없지만 코드블록으로 할 수 있는 일

  • 코드 블록에서는 범위 안의 지역변수를 읽고 수정할 수 있다. 하지만 람다에서는 final이거나 사실상 final인 변수만 읽을 수 있고, 지역변수를 수정하는 건 불가능하다.
  • 코드 블록에서는 return 문을 사용해 메서드에서 빠져나가거나, break나 continue 문으로 블록 바깥의 반복문을 종료하거나 반복을 한 번 건너뛸 수 있다. 또한 메서드 선언에 명시된 검사 예외를 던질 수 있다. 하지만 람다로는 이 중 어떤 것도 할 수 없다.

 

스트림이 권장되는 경우

  • 원소들의 시퀀스를 일관되게 변환한다.
  • 원소들의 시퀀스를 필터링한다.
  • 원소들의 시퀀스를 하나의 연산을 사용해 결합한다.(더하기, 연결하기, 최솟값 구하기등).
  • 원소들의 시퀀스를 컬렉션에 모은다.
  • 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다.

 

 

핵심정리

스트림과 반복 중 어느쪽이 나은지 확신하기 어렵다면 둘 다 해보고 더 나은쪽을 택하라

 

 

참고

https://jake-seo-dev.tistory.com/452

 

profile

꾸준한 스터디

@StudyRecord

포스팅이 유익하셨다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!