본문 바로가기
BackEnd/Java

Stream

by 규난 2023. 8. 20.
728x90

Stream 이란?

데이터 처리 연산을 지원하도록 데이터 소스에서 추출된 연속된 요소라고 정의할 수 있습니다.

 

거의 모든 자바 애플리케이션은 collection으로 데이터를 그룹화하고 그룹에서 특정 값을 고르거나 연산하는 과정을 포함합니다.

이러한 과정을 밑의 SQL query 문처럼 구현 코드 대신 질의(선언) 형식으로 표현하고 

select * from member m where m.gender = 'M';

멀티 코어 아키텍처 환경에서 멀티 스레드 코드를 구현하지 않아도 collection에 대해서 병렬 처리를 할 수 있게 자바 8부터 새롭게 도입된 기능입니다.

 

Stream의 간단한 예제

public class Main {
	public static void main(String[] args) {
        List<String> lowMemberNames = members.stream()
            .filter(member -> member.getAge() < 30) // 회원에서 30살 미만의 회원만 필터링
            .sorted(Comparator.comparing(Member::getAge)) // 30살 미만의 회원 나이를 오름차순으로 정렬
            .map(Member::getName) // 회원의 이름 추출
            .collect(Collectors.toList()); // 스트림 -> 컬렉션으로 변환
    }
}

위 예제는 데이터 소스인 member를 통해 연속된 요소를 스트림에 제공하고 filter, sorted, map, collect로 이어지는 데이터 처리 연산을 적용한 코드입니다.

filter, sorted, map과 같은 중간 연산은 Stream을 반환하여 파이프라인을 구성하게 되는데 이때 중간 연산은 파이프라인만 구성할 뿐 데이터 처리에 대한 결과를 얻을 수 없습니다. 데이터 처리에 대한 결과를 얻으려면 collect와 같은 최종 연산이 호출되어야 하고 최종 연산이 호출되면 파이프라인을 처리해서 스트림이 아닌 데이터 처리에 대한 결과를 반환하게 됩니다.

 

Stream의 중간 연산(intermediate operation)은 Stream을 반환하는 메서드인 filter, map, limit, sorted, distinct 등이 있고

최종 연산(terminal operation)은 forEach(void), count(long), collect(List, Map..)등의 메소드가 있습니다.

 

Stream의 장점

  1. loop와 if 조건문 등의 제어 블록을 사용해서 동작을 직접 구현할 필요가 없이 "30살 미만의 회원을 선택한 후 선택된 데이터를 나이 오름차순으로 정렬하고 최종적으로 회원의 이름을 추출해라"와 같은 동작 수행을 선언 형식으로 코드를 구현하고 데이터를 처리할 수 있습니다. 
  2. 람다 표현식을 사용해서 기존에 30살 미만을 필터링하던 코드를 30살 이상을 필터링하는 코드로 쉽게 변경이 가능해지므로 변경된 요구사항에 대해서 쉽게 대응이 가능해집니다. 
  3. 위 간단한 예제처럼 filter, sorted, map, collect와 같이 여러 메소드를 연결해서 복잡한 데이터 처리에 대한 파이프라인을 만들 수 있습니다.

스트림  메서드

filter, distinct 메서드를 이용한 필터링

filter메서드는 함수 디스크립터가 T -> boolean인 Predicate를 파라미터로 받아서 Predicate와 일치하는 모든 요소를 스트림을 반환하며 distinct 메서드는 고유 요소로 이루어진 스트림을 반환합니다.

 

distinct 메소드가 어떤 방식으로 객체에 중복 여부에 대해서 판단할까요?? 바로 동등성 비교 시 사용되는 hashcode와 equals 메서드를 이용하여 객체 중 동일한 값을 가지는 객체들에 대한 필터링을 하게 됩니다. 따라서 distinct 메서드를 사용 시 꼭 hashcode와 equals 메서드의 대한 재정의를 해줘야 원하는 결과를 얻을 수 있게됩니다.

List<Member> lowMembers = members.stream()
    .filter(member -> member.getAge() < 30)
    .distinct()
    .collect(toList());

 

Predicate를 이용한 슬라이싱

자바 9에서는 조건을 만족하지 않는 첫 번째 요소를 만는 경우 내부 반복을 중단하고 스트림을 반환하는 takeWhile, dropWhile 메서드를 지원합니다.

 

takeWhile과 dropWhile 메서드는 조건을 만족하지 않는 첫 번째 요소를 만다는 경우 내부 반복을 중단한다는 게 공통점이며 차이점은 

takeWhile 메서드는 주어진 조건을 만족하는 동안의 요소만 포함하는 스트림을 반환합니다.

dropWhile 메서드는 주어진 조건을 만족하는 동안의 요소를 제외한 나머지 요소를 포함하는 스트림을 반환합니다.

 

이 두 메서드는 정렬된 데이터에서 원하는 범위 내에 결과를 찾을 때 효율적인 메서드라고 생각이 됩니다.

List<Member> members = Arrays.asList(
    new Member(20, "member1"),
    new Member(25, "member2"),
    new Member(27, "member3"),
    new Member(29, "member4"),
    new Member(30, "member5"),
    new Member(35, "member6")
);

// member1, member2, member3, member4가 리스트에 담김
List<Member> lowMembers = members.stream()
    .takeWhile(member -> member.getAge() < 30)
    .collect(toList());
    
// member5, member6이 리스트에 담김
List<Member> lowMembers = members.stream()
    .dropWhile(member -> member.getAge() < 30)
    .collect(toList());

 

중간 연산

연산 반환 형식 함수형 인터페이스 함수 디스크립터 특징
filter Stream<T> Predicate<T> T -> boolean  
distinct  Stream<T>     stateful operation
takeWhile Stream<T> Predicate<T> T -> boolean stateful operation, shor-circuiting
dropWhile Stream<T> Predicate<T> T -> boolean stateful operation
skip Stream<T> long   stateful operation
limit Stream<T> long   stateful operation, short-circuiting
map Stream<R> Function<T, R> T -> R  
flatMap Stream<R> Function<T, Strea<R>> T -> Stream<R>  
sorted Stream<T> Comparator<T> (T, T) -> int stateful operation

 

최종 연산

연산 반환 형식 함수형 인터페이스 함수 디스크립터 특징
anyMatch boolean Predicate<T> T -> boolean short-circuiting
noneMatch boolean Predicate<T> T -> boolean short-circuiting
allMatch boolean Predicate<T> T -> boolean short-circuiting
findAny Optional<T>     short-circuiting, 순서가 상관 없을 경우
findFirst Optional<T>     short-circuiting, 순서가 상관 있을 경우
forEach void Consumber<T> T -> void  
collect R Collector<T, A, R>    
reduce Optional<T> BinaryOperator<T> (T, T) -> T stateful operation
count long      

 

예제를 통한 Stream 문법 알아보기

초기 데이터 셋팅

// 거래자 생성
Trader raoul = new Trader("Raoul", "Cambridge");
Trader mario = new Trader("Mario", "Milan");
Trader alan = new Trader("Alan", "Cambridge");
Trader brain = new Trader("Brain", "Cambridge");

// 거래 목록 생성
transactions = Arrays.asList(
        new Transaction(brain, 2011, 300),
        new Transaction(raoul, 2012, 1000),
        new Transaction(raoul, 2011, 400),
        new Transaction(mario, 2012, 710),
        new Transaction(mario, 2012, 700),
        new Transaction(alan, 2012, 950)
);

 

2011년도에 일어난 모든 거래를 찾아 거래 금액을 오름차순으로 정렬하는 예제

 

중간 연산인 filter 메서드를 사용해서 거래 날짜가 2011년도인 거래를 필터링하고 sorted 메서드를 사용해서 거래 금액을 기준으로 오름차순하고 최종 연산인 collect 메서드를 사용해서 스트림을 거래 목록 리스트로 반환합니다.

List<Transaction> tr2011 = transactions.stream()
    .filter(transaction -> transaction.getYear() == 2011)
    .sorted(Comparator.comparing(Transaction::getAmount))
    .collect(Collectors.toList());

 

거래자가 근무하는 모든 도시의 중복 제거

 

중간 연산인 map 메서드를 사용해서 거래에서 거래자의 도시를 추출하고 distinct 메서드를 사용해서 같은 도시에 대해 중복을 제거 후 최종 연산인 collect 메소드를 사용해서 스트림을 도시 리스트로 반환합니다.

List<String> cities = transactions.stream()
    .map(transaction -> transaction.getTrader().getCity())
    .distinct()
    .collect(Collectors.toList());

 

밀라노에 거래자가 있는지 확인

 

최종 연산인 anyMatch메서드를 사용해서 밀라노에 거래자가 존재하는지 여부를 반환합니다.

anyMatch는 함수 디스크립터가 T -> boolean인 Predicate를 파라미터로 받고 스트림에서 적어도 한 개 이상의 요소가 일치하는 확인하는 메서드입니다.

boolean isMilan = transactions.stream()
    .anyMatch(transaction -> transaction.getTrader().getCity().equals("Milan"));

 

케임브리지에 거주하는 거래자의 모든 거래 금액을 출력

 

중간 연산인 filter 메서드를 사용해서 거래자가 Cambridge에 거주하는 거래를 필터링하고 map 메서드를 사용해서 거래 금액을 추출한 후 최종 연산인 forEach 메서드를 사용해서 거래 금액을 출력합니다.

transactions.stream()
    .filter(transaction -> transaction.getTrader().getCity().equals("Cambridge"))
    .map(Transaction::getAmount)
    .forEach(System.out::println);

 

전체 거래 중 최댓값 찾기

 

중간 연산인 mapToInt 메소드를 사용해 거래 금액을 추출하고 모든 스트림 요소를 처리해서 값으로 도출하는 최종 연산인 reduce 메서드를 사용해서 최대 금액을 반환합니다.

 

예제에서 map 메서드 대신에 mapToInt 메서드를 사용한 이유는 map 메서드를 사용하면 반환 값이 Stream<Integer>이며 스트림의 요소는 Integer 타입입니다. 이때 최종 연산인 reduce에서 Integer.max(int a, int b) 메서드를 사용해서 최댓값을 찾게 되는데 스트림의 요소는 Integer인 wrapper class이므로 primitive type으로 언박싱하는 비용이 발생하게 됩니다. 이러한 언박싱에 대한 비용을 피할 수 있도록 자바 8은 기본형 특화 스트림인 IntStream, DoubleStream, LongStream을 제공하고 있습니다. IntStream은 mapToInt 메서드를 사용해서 얻을 수 있기 때문에 mapToInt 메서드를 사용하였습니다.

Integer maxTransaction = transactions.stream()
    .mapToInt(Transaction::getAmount)
    .reduce(0, Integer::max);

 

728x90

'BackEnd > Java' 카테고리의 다른 글

자바 Enum 타입 속 모든 비밀: 정의, 컴파일러, 싱글톤  (0) 2023.10.25
BigDecimal 사용 이유  (0) 2023.10.19
Java Virtual Machine  (2) 2023.10.14
equals() 메서드와 hashCode() 메서드  (0) 2023.09.16
람다 표현식  (0) 2023.08.16