Stream API
JAVA8 이 기존 JAVA와 달라진 부분은 단지 람다표현식이 추가된 것만 있는 것은 아닌 것 같아요.
람다 표현식과 더불어 같이 추가된 것은 컨테이너(Collection)를 사용하는 비지니스 로직을 보다 간결하고(쉽지는 않을듯? 배워야하기 때문이죠.. ㅡㅡ^ ) 성능 좋게 만들 수 있는 Stream API의 등장은 clean code 제작에 많은 도움이 될 것같습니다.
요즘 개발하고 있는 Spring 기반의 back단 작업이나 안드로이드위에서 작성하는 비지니스 로직은 DB를 껴안고 있기 때문에 컨테이너를 자주 사용하게 되고, 당연히 루프를 돌면서 이 작업, 저 작업을 하게 됩니다.
비지니스 로직에 따라 if, break, continue 등 여러 문법 등을 사용하게 되고, 때에 따라 복잡해지는 것을 작성해줘야하지만 Stream API를 사용하면 내가 무슨일을 해야한다는 것을 어떤 규칙에 따라 선언만 해주면 되는 선언형 프로그래밍을 할 수 있습니다. (SQL 질의 처럼 말이죠. SQL 질의어를 사용하면 복잡한 key-value 기반의 테이블에서 효과적으로 데이터를 찾을 수 있죠. 찾는 로직은 몰라도 되고요.)
예를 들면 다음과 같은 코드를 볼 수 있습니다. dataList 라는 컨테이너를 이용해서 같은 로직을 작성해보겠습니다.
1 2 3 4 5 6 7 8 | LinkedList<Integer> dataList = new LinkedList<Integer>() { { Random random = new Random(); for (int i = 0; i < 100; ++i) { add(random.nextInt(100)); } } }; | cs |
아래는 기존 JAVA8 이전 Collection을 loop돌면서 컨테이너 내부의 가장 큰 7의 배수 4개를 찾는 것입니다. (아무리 라인수를 줄일려고 해도 저는 이정도가 한계인가 봅니다. ㅜㅡㅜ)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Collections.sort(dataList, new Comparator<Integer>() { @Override public int compare(Integer arg0, Integer arg1) { // TODO Auto-generated method stub return arg1.compareTo(arg0); } }); for (int i = 0, count = 0, size = dataList.size(); i < size && count < 4; ++i) { if (dataList.get(i) % 7 == 0) { System.out.println(dataList.get(i)); ++count; } } | cs |
다음은 Stream API를 사용한 로직입니다.
1 2 | dataList.stream().filter((a) -> a % 7 == 0).sorted((a, b) -> b.compareTo(a)).limit(4) .forEach(System.out::println); | cs |
stream 내부 메소드를 이용해 질의를 하듯이 코드를 작성하였습니다. 정말 간단해졌죠?
이와같이 Stream API를 이용하면 단순히 비지니스 처리를 선언으로 하기 때문에 쉽게 구현할 수 있으며, 각각의 블럭 (sorted나 filter) 등이 chain 형식을 지원하기 때문에 복잡한 로직에 대해 가독성과 명확성을 확실시 해줄 수 있을 것같습니다. (즉 파이프라인 구조입니다.)
또한 내부적으로는 데이터 처리과정을 병렬화할 수 있기 때문에 성능을 더 좋게 만들 수 있습니다.
(물론 병렬을 위한 thread 처리에 대해서는 신경쓰지 않아도 됩니다.)
이와같은 Stream API의 특징을 JAVA8 IN ACTION에서는 다음과 같이 요약하고 있습니다.
1. 선언형
더 간결하고 가독성이 좋아집니다.
2. 조립할 수 있음
유연성이 좋아집니다.
3. 병렬화
성능이 좋아집니다.
그럼 여기에서, Stream의 구조에 대해서 조금 더 살펴보죠.
Stream 을 사용하는 구조는 크게 데이터 소스를 받는 것과 비지니스 로직에 대한 각종 컨디션을 파이프라인으로 구성하기 위한 중간 연산, 구성할 파이프라인을 실행할 최종 연산정도 있습니다.
위의 작성한 코드를 살펴보면 다음과 같이 나타낼 수 있네요.
1 2 3 4 | dataList.stream() // 데이터 소스 .filter((a) -> a % 7 == 0).sorted((a, b) -> b.compareTo(a)).limit(4) // 중간 연산 .forEach(System.out::println); // 최종 연산 | cs |
위의 역할을 살펴보면, dataList 로 부터 데이터 소스를 받아오고 있으며, 7의 배수, 내림차순, 선착순 4개 등 chain 형식으로 각종 컨디션을 연결한 중간 연산과정이 있으며, 이러한 조건의 값들을 console에 표시하겠다는 최종 연산으로 나누어짐을 알 수 있습니다.
(Stream API에서 제공하는 보다 더 다양한 연산은 다음에 정리하여, 포스팅하도록 하겠습니다. )
이러한 Stream API를 이용한 스트림 요소의 연산은 오직 딱 한번만 사용(소비)할 수 있습니다. 예를들어 이 것은 예외를 일으킵니다.
1 2 3 4 5 6 7 | Stream<Integer> stream = dataList.stream(); stream.filter((a) -> a % 7 == 0).sorted((a, b) -> b.compareTo(a)).limit(4) .forEach(System.out::println); stream.filter((a) -> a % 7 == 0).sorted((a, b) -> a.compareTo(b)).limit(10) .forEach(System.out::println); // IllegalStateException 예외 발생 | cs |
아래는 예외 메시지입니다.
1 2 3 4 5 6 7 8 | Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed at java.util.stream.AbstractPipeline.<init>(Unknown Source) at java.util.stream.ReferencePipeline.<init>(Unknown Source) at java.util.stream.ReferencePipeline$StatelessOp.<init>(Unknown Source) at java.util.stream.ReferencePipeline$2.<init>(Unknown Source) at java.util.stream.ReferencePipeline.filter(Unknown Source) at testClass.main(testClass.java:40) | cs |
이것으로 Stream API 에 대한 Summary 정도는 여기까지 포스팅하겠습니당.
(나머진 필요한 것 따라 메소드를 사용해주면 되지 않을까요? 그래서 Summary 입니다. ㅡㅡ^)
|
'개발이야기 > 함수형 프로그래밍' 카테고리의 다른 글
Stream API 활용편1 (필터와 슬라이싱) (0) | 2016.08.26 |
---|---|
Stream과 Collection 차이 (0) | 2016.08.04 |
람다 조합 (0) | 2016.08.02 |
메서드 레퍼런스 (0) | 2016.08.01 |
람다의 실제 형식 (0) | 2016.07.28 |