자바 스트림 API
이번에는 Java SE 8에서 추가된 아주 좋은 SteamAPI에 대해서 알아 보도록 하겠습니다.
오역이 있을 수도 있으니 원본 문서도 한번 보는것을 추천 드립니다.
원본 글 : http://www.oracle.com/technetwork/articles/java/ma14-java-se-8-streams-2177646.html
오타 및 오역 지적해주시면 감사하겠습니다.
============================================================================================
복잡한 데이터 처리 질의를 표현하기 위해 Stream을 사용!.
대부분의 자바 애플리케이션들이 데이터를 만들고 처리하기 위해서 컬렉션을 사용한다. 이 컬렉션을 처리하는 로직 또한 같이 구현되어 코드에 포함되어 있고 이는 많은 프로그램의 기본(데이터를 그룹핑 하고 제어 하는 등)이 된다.
예를 들어 은행에서 고객들의 입출력 내역을 가지고 있는 컬렉션을 생성하고 이중에서 고객이 사용한 내역들에 대해 추출하는 로직을 작성할 수 있을 것이다. 이런 은행 관련 중요한 기능들을 컬렉션을 통해 작정할 수 있지만 코드가 그다지 아름답지 않고 지저분하게 보여진다.
컬렉션을 사용하는데는 두가지 단점이 있다.
1. 컬렉션에서 데이터를 처리하는 전형적인 처리 패턴은 SQL에서 데이터를 찾는것과 비슷하거나(처리 내역 중 가장 높은 값을 찾기) 데이터를 묶는것(구매 내역중에 식료품에서 쇼핑한 내역만 찾기)과 비슷하다. 대부분의 데이터베이스들은 이를 명확하게 지원하는데 만약 가장 높은 값을 가지는 것을 찾고 싶으면 "SELECT ID, MAX(VALUE) FROM TRANSACTIONS"를 사용 하면 된다.
위의 예에서 보는것 처럼 최대값을 어떻게 계산해야 하는지에 대해서 직접 구현할 필요가 없이(반복문을 사용해서 가장 높은 값을 찾는 것) 단지 원하는 값을 표현해 주기만 하면 된다. 컬렉션도 데이터베이스처럼 데이터의 집합인데 왜 비슷한 방법으로 처리할 생각을 못하고 이와 비슷한 구현을 하기 위해서 얼마나 많은 시간을 소모해서 복잡한 반복문을 작성하고 또 작성했는지 생각해보자.
2. 만약 큰 사이즈의 컬렉션은 어떻게 처리 해야 할까? 처리 속도를 향상하기 위해서는 병렬 처리를 하도록 코드를 작성해야 되는데 이는 기술적으로도 어렵고 많은 에러가 발생한다.
이러한 단점들은 Java SE 8이 출시되면서 해결할 수 있게 되었다. Stream 이라는 새로운 API가 공개되었고 이를 통해 쿼리를 작성하듯 데이터를 명시적인 방법으로 처리할 수 있게 되었다. 게다가 Stream은 멀티 스레드 관련 코드도 별도로 작성할 필요 없이 멀티코어를 지원할 수 있게 되었다. 좋아 보이지 않는가? 이 문서에서는 이런 내용들에 대하여 살펴볼 것이다.
Stream을 가지고 무엇을 할 수 있는지 알아보기 전에 Java SE 8의 Stream 사용법에 대해 간단히 살펴보자. 식료품점에서 쇼핑한 모든 거래내역중에 거래 ID를 가장 큰 비용이 들어간 순으로 정렬해서 추출하는 것에 대한 코드를 작성해보자.
Listing 1. Java SE 7 이하에서의 처리 방법
ListgroceryTransactions = new ArrayList (); for (Transaction t : groceryTransactions) { if (t.getType() == Transaction.GROCERY) { groceryTransactions.add(t); } } Collections.sort(groceryTransactions, new Comparator () { public int compare(Transaction t1, Transaction t2) { return t2.getValue().compareTo(t1.getValue()); } }); List transactionIds = new ArrayList (); for (Transaction t : groceryTransactions) { transactionIds.add(t.getId()); }
Listing 2. Java SE 8 에서의 처리 방법
ListtransactionsIds = transactions.stream() .filter(t -> t.getType() == Transaction.GROCERY) .sorted(Comparator.comparing(Transaction::getValue).reversed()) .map(Transaction::getId) .collect(Collectors.toList());
아래 그림 Fiqure 1은 Java SE 8에서 Stream이 동작하는 과정을 그린것이다.
먼저 모든 거래내역(List)로 부터 stream()메서드를 사용해서 stream을 가져오고 그 다음에 여러가지 기능(filter, sorted, map,collect)를 체인 처럼 엮어서 데이터를 처리하는 쿼리처럼 보이게 만든다.
Figure 1
병렬 처리 코드 작성은 Java SE 8에서 매우 쉽게할 수 있다. Listing 3. 처럼 그냥 stream()을 parallelStream()으로 변경만 하면 된다. Stream API는 내부적으로 멀티코어로 동작하도록 처리한다.
Listing 3.
ListtransactionsIds = transactions.parallelStream() .filter(t -> t.getType() == Transaction.GROCERY) .sorted(Comparator.comparing(Transaction::getValue).reversed()) .map(Transaction::getId) .collect(Collectors.toList());
처음보는 형태의 코드를 보고 걱정할 필요는 없다. 다음 섹션에서 이것들이 어떻게 동작하는지 살펴볼 것이다. 하지만 람다 표현식(t-> t.gerCategory() == Transaction.GROCERY)의 사용방법이나 메소드 참조(method references, Transaction::getId)에 대해서는 미리 한번 보는것이 좋다.
지금 부터 컬렉션에 저장된 데이터들을 SQL이 동작하는 것 처럼 스트림을 활용해서 처리 하는것에 대해서 살펴볼 것이다. 이 모든 동작들은 람다 표현식과 함께 간단한 파라미터로 처리 될 수 있다.
Java SE 8의 Stream에 대한 이 문서를 다 읽어보면 Stream API를 사용하여 위 예제와 같이 멋진 방식으로 사용할 수 있을 것이다.
Stream 시작 하기
작은 부분부터 시작하도록 해보자. Stream의 정의는 무엇일까? 간단하게 정의해 보면 "집계 연산을 지원하는 요소의 순서(a sequence of elements from a source that supports aggregate operations.)"라고도 할수 있는데 이에 대해 더 알아 보도록 하자.
- Sequence of element : Stream은 정의된 엘리먼트의 속성에 따라서 처리할 수 있는 인터페이스를 제공하지만 실제 엘리먼트들을 저장하지 않고 계산하는데만 쓰인다.
- Source : 스트림은 컬렉션, 배열, I/O 리소스 등에서 제공받은 데이터를 가지고 작업을 처리 한다.
- Aggreate operations : Stream은 SQL 같은 처리를 지원하고 함수형 프로그래밍 같은 처리 방법도 지원한다. (filter, map,reduce, find, match, sorted 등)
- Pipelining : 많은 Stream 기능들이 Stream 자기 자신을 리턴한다. 이 방식은 처리 작업이 체인처럼 연결되어 큰 파이프라인처럼 동작 하도록 한다. 이를 통해 laziness와 short-circuiting 과 같이 최적화 작업을 할 수 있다.
- Internal iteration : 명시적으로 반복작업을 수행해야 되는 Collection과 비교 하면 Stream 작업은 내부에서 처리된다.
List<String> transactionIds = new ArrayList<>(); for(Transaction t: transactions){ transactionIds.add(t.getId()); }
List<Integer> transactionIds = transactions.stream() .map(Transaction::getId) .collect(toList());
- filter, sorted, map은 파이프라인처럼 서로 연결시킬 수 있다.
- collect는 파이프라인을 종료 시키고 결과를 리턴한다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8); List<Integer> twoEvenSquares = numbers.stream() .filter(n -> { System.out.println("filtering " + n); return n % 2 == 0; }) .map(n -> { System.out.println("mapping " + n); return n * n; }) .limit(2) .collect(toList());
filtering 1 filtering 2 mapping 2 filtering 3 filtering 4 mapping 4
- 질의를 할 데이터소스(Collection)같은게 필요하다
- Stream 파이프라인을 형성하는 중간 작업
- Stream 파이프라인을 실행하고 결과를 리턴하는 종료 작업
- filter(Predicated) : 주어진 predicate(java.util.function.Predicate)와 일치하는 stream을 리턴한다.
- distinct : 중복을 제거한 유니크 엘리먼트를 리턴한다.(stream에 포함된 엘리먼트들의 equals()구현에 따라 구분된다.
- limit(n) : 주어진 사이즈(n)에 까지의 stream을 리턴한다.
- skip(n) : 주어진 엘리먼트 길이 까지 제외한 stream을 리턴한다.
boolean expensive = transactions.stream() .allMatch(t -> t.getValue() > 100);
Optional<Transaction> = transactions.stream() .filter(t -> t.getType() == Transaction.GROCERY) .findAny();
transactions.stream() .filter(t -> t.getType() == Transaction.GROCERY) .findAny() .ifPresent(System.out::println);
List<String> words = Arrays.asList("Oracle", "Java", "Magazine"); List<Integer> wordLengths = words.stream() .map(String::length) .collect(toList());
int sum = 0; for (int x : numbers) { sum += x; }
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
- 초기 화 값, 0
- BinaryOperator<T>, 두개의 엘리먼트들을 더해서 새로운 값을 생성
int product = numbers.stream().reduce(1, (a, b) -> a * b); int product = numbers.stream().reduce(1, Integer::max);
int statement = transactions.stream() .map(Transaction::getValue) .sum(); // error since Stream has no sum method
int statementSum = transactions.stream() .mapToInt(Transaction::getValue) .sum(); // works!
IntStream oddNumbers = IntStream.rangeClosed(10, 30) .filter(n -> n % 2 == 1);
Stream<Integer> numbersFromValues = Stream.of(1, 2, 3, 4); int[] numbers = {1, 2, 3, 4}; IntStream numbersFromArray = Arrays.stream(numbers);
long numberOfLines = Files.lines(Paths.get(“yourFile.txt”), Charset.defaultCharset()) .count();
Stream<Integer> numbers = Stream.iterate(0, n -> n + 10);
numbers.limit(5).forEach(System.out::println); // 0, 10, 20, 30, 40
- Steream API는 몇가지 기능들을 활요해서 laziness 및 short-circuiting을 통해 데이터 처리를 최적화 할 수 있다.
- Stream은 병렬 처리를 손쉽게 지원한다.