8.리팩토링, 테스팅, 디버깅

대부분의 프로젝트는 예전 자바로 구현된 기존 코드를 기반으로 새로운 프로젝트를 시작.

기존 코드를 이용해 새로운 프로젝트를 시작하는 상황을 가정.

람다 표현식을 이용해서 가독성과 유연성을 높이면서 기존 코드를 어떻게 리팩토링해야하는지 설명.

람다 표현식으로 전략, 템플릿 메서드, 옵저버, 의무 체인, 팩토리 등의 객체지향 디자인 패턴 간소화 가능.


1. 가독성과 유연성을 개선하는 리팩토리

- 람다 표현식은 익명 클래스보다 코드를 좀 더 간결하게 만듦.

- 동작 파라미터화의 형식을 지원하므로 유연성을 갖춤.


1.1 코드 가독성 개선

- 코드 가독성이 좋다는 것은 추상적 표현으로 정확하게 정의하기 어려움.

- 일반적으로 "어떤 코드를 다른 사람도 쉽게 이해할 수 있음"을 의미.

> 즉, 코드 가독성을 개선한다는 것은 우리가 구현한 코드를 다른 사람이 쉽게 이해하고 유지보수할 수 있게 만드는 것을 의미.

- 자바8에서는 코드 가독성에 도움을 주는 다음과 같은 기능을 새롭게 제공.

> 코드의 장황함을 중려서 쉽게 이해할 수 있는 코드 구현 가능.

> 메서드 레퍼런스와 스트림 API를 이용해 코드의 의도 쉽게 표현 가능.

- 람다, 메서드 레퍼런스, 스트림을 활용한 코드 가독성 세가지 리팩토링.

> 익명 클래스 -> 람다

> 람다 -> 메서드 레퍼런스

> 명령형 데이터 처리 -> 스트림


1.2 익명 클래스를 람다 표현식으로 리팩토링

- 하나의 추상 메서드를 구현하는 익명 클래스는 람다 표현식으로 리팩토링 가능.

- 익명 클래스를 람다로 리팩토링 하는 이유는?

> 람다 표현식이 더 간결하고, 가독성이 좋음.

- 모든 익명 클래스를 람다로 변환할 수 있는 것은 아님.

> 1) 익명 클래스에서 사용한 this와 super는 람다에서는 다른 의미.

* 익명 클래스의 this는 자신을 가리키지만 람다에서 this는 람다를 감싸는 클래스를 가리킴.

> 2) 일명 클래스는 감싸고 있는 클래스의 변수를 가릴수 있음(쉐도우 변수)

* 람다 표현식으로는 변수를 가릴 수 없음.

- 익명 클래스를 람다 표현식으로 바꾸면 컨텍스트 오버로딩에 따른 모호함이 초래될 수 있음.

> 익명 클래스는 인스턴스화할 때 명시적으로 형식이 정해지는 반명 람다의 형식은 컨텍스트 텍스트에 따라 달라지기 때문.


1.3 람다 표현식을 메서드 레퍼런스로 리팩토링

- 람다 표현식을 쉽게 전달할 수 있은 잛은 코드.

- 람다 표현식 대신 메서드 레퍼런스를 이용할 경우 가독성 증가.

- comparing와 maxBy같은 정적 헬퍼 메서드와 메서드 레퍼런스는 조화를 이룸.

- 람다 표현식보다 코드의 의도를 더 명확히 보여줌.


1.4 명령형 데이터 처리를 스트림으로 리팩토링

- 이론적으로는 반복자를 이용한 기존의 모든 컬렉션 처리 코드를 스트림으로 바꿔야 함.

- 스트림 API는 데이터 처리 파이프라인의 의도를 더 명확하게 보여줌.

- 스트림은 쇼트서킷과 게으름이라는 강력한 최적화뿐 아니라 멀티코어 아키텍처를 활용할 수 있는 지름길을 제공.

- 명령형 코드의 break, continue, return 등의 제어 흐름문을 모두 분석해서 같은 기능을 수행하는 스트림 연산으로 유추해야 하므로 명령어 코드를 스트림으로 바꾸는 것은 쉽지 않음.

> 다행히 명령형 코드를 스트림으로 바꾸도록 도움을 주는 도구가 존재.

* http://goo.gl/Ma15w9 참고


1.5 코드 유연성 개선

- 람다 표현식을 이용하면 동작 파라미터화를 쉽게 구현 가능.

> 즉, 다양한 람드를 전달해서 다양한 동작 표현 가능.


2. 람다로 객체지향 디자인 패턴 리팩토링

- 언어에 새로운 기능이 추가되면서 기존 코드 패턴이나 관용코드의 인기가 식기도 함.

- 자바5에서 추가된 for-each루프는 에러 발생률이 적으며 간결하므로 기존의 반복자 코드를 대체.

- 자바7에 추가된 다이아몬드 연산자 <> 때문에 기존으 제네릭 인스턴스를 명시적으로 생성하는 빈도가 줄어듬.

- 다양한 패턴을 유형별로 정리한 것이 디자인 패턴.

- 디자인 패턴은 공통적인 소프트웨어 문제를 설계할 때 재사용할 수 있는, 검증된 청사진을 제공.

- 디자인 패턴에 람다 표현식이 더해지면 색다른 기능을 발휘.

> 즉, 람다를 이용하면 이전에 디자인 패턴으로 해결하던 문제를 더 쉽고 간단히 해결 가능.


2.1 전략 패턴

- 한 유형의 알고리즘을 보유한 상태에서 런타임에 적절한 알고리즘을 선택하는 기법.

- 다양한 기준을 갖는 입력값을 검증하거나, 다양한 파싱 방법을 사용하거나, 입력 형식을 설정하는 등 다양한 시나리에 전략 패턴 활용 가능.

- 다양한 전략을 구현하는 새로운 클래스를 구현할 필요 없이 람다 표현식을 직접 전달하면 코드가 간결해짐.

- 람다 표현식은 코드 조각(또는 잔략)을 캡슐화 함.


2.2 템플릿 메서드

- 알고리즘의 개요를 제시한 다음에 알고리즘의 일부를 고칠 수 있는 유연함을 제공해야 할때 사용하는 패턴.

> 템플릿 메서드는 "이 알고리즘을 사용하고 싶은데 그대로는 안 되고 조금 고쳐야 하는"상황에 적합

- 예로, 온라인 뱅킹 어플리케이션 동작.

> 은행마다 다양한 온라인 뱅킹 어플리케이션을 사용하며 동작 방법도 조금씩 차이가 있음.

- 람다 표현식을 이용해 템플릿 메서드 디자인 패턴에서 발생하는 자잘한 코드 제거 가능.


2.3 옵저버

- 어떤 이벤트가 발생했을 때 한 객체가 다른 객체 리스트에 자동으로 알림을 보내야 하는 상황에서 옵저버 디자인 패턴 사용.

- GUI 어플리케이션에서 옵저버 패턴이 자주 등장.

- 예로, 다양한 신문매체가 뉴스 트윗을 구독하고 있으며 특정 키워드를 포함하는 트윗이 등록되면 알림을 받아야 하는 경우.

- 람다 표현식을 이용해 불필요한 코드 제거 가능.

> 단, 옵저버가 상태를 가지며, 여러 메서드를 정의하는 등 복잡하다면 람다 표현식보다 기존의 클래스 구현 방식을 고수하는 것이 바람직 할 수도 있음.


2.4 의무 체인

- 작업처리 객체의 체인을 만들 때는 의무 체인 패턴 사용.

- 한 객체가 어던 작업을 처리한 다음에 다른 객체로 결과를 전달하고, 다른 객체도 해얗ㄹ 작업을 처리한 다음에 또 다른 객체로 전달하는 식.

- andThen 메서드를 조합해 체인 생성 가능.


2.5 팩토리

- 인스턴스화 로직을 클라이언트에 노출하지 않고 객체를 만들 때 팩토리 디자인 패턴 사용.

- 예로, 은행에서 취급하는 대출, 채권, 주식 등 다양한 상품을 만들어야 할때 사용 가능.

- 생성자와 설정을 외부로 노출하지 않음으로써 클라이언트가 단순하게 상품을 생상할 수 있음.


3. 람다 테스팅

- 개발자의 최종 업무 목표는 제대로 작동하는 코드를 구현하는 것이지 깔끔한 코드를 구현하는 것이 아님.

- 일반적으로 좋은 소프트웨어 공학자라면 프로그램이 의도대로 동작하는지 확인할 수 있는 단위 테스팅을 진행.

- 소스 코드의 일부가 예상된 결과를 조출할 것이라 단언하는 테스트 케이스를 구현.


3.1 보이는 람다 표현식의 동작 테스팅

- 람다는 익명(결국 익명 함수)이므로 테스트 코드 이름을 호출할 수 없음.

> 따라서 필요하다면 람다를 필드에 저장해서 재사용할 수 있으며 람다의 로직 테스트 가능.

- 람다 표현식은 함수형 인터페이스의 인스턴스를 생성하며 생성된 인스턴스의 동작으로 람다 표현식의 테스팅이 가능.


3.2 람다를 사용하는 메서드의 동작에 집중

- 람다의 목표는 정해진 동작을 다른 메서드에서 사용할 수 있도록 하나의 조각으로 캡슐화 하는 것.

> 세부 구현을 포함하는 람다 표현식을 공개하지 말아야 함.

- 람다 표현식을 사용하는 메서드의 동작을 테스트함으로써 람다를 공새하지 않으면서도 람다 표현식을 검증할 수 있음.


3.3 복잡한 람다를 개별 메서드로 분할

- 람다 표현식을 메서드 레퍼런스로 바꾸는 것.

> 일반 메서드를 테스트하듯 람다 표현식 테스트가 가능해짐.


3.4 고차원 함수 테스팅

- 메서드가 람다를 인수로 받는다면 다른 람다로 메서드의 동작 테스트 가능.


4. 디버깅

- 문제가 발생한 코드를 디버깅할 때 개발자는 다음 주 가지를 가장 먼저 확인.

> 스택 트레이스

> 로깅

- 하지만 람다 표현식과 스트림은 기존의 디버깅 기법을 무력화 함.


4.1 스택 트레이스 확인

- 예외 발생으로 프로그램 실행이 갑자기 중단되었다면 먼저 어디에서 멈췄고 어떻게 멈추게 되었는지 확인 필요.

> 바로 스택 프레임에서 이 정보 획득 가능.

* 프로그램에서의 호출 위치, 호출할 때의 인수값, 호출된 메서드의 지역변수 등을 포함한 호출 정보가 스택 프레임에 저장됨.

- 프로그램이 멈췄다면 프로그램이 어떻게 멈추게 되었는지 프레임별로 보여주는 스택 트레이스 획득 가능.

- 람다 표현식의 경우 익명이기 때문에 조금 복잡한 스택 트레이스가 생성.

> ex) Debugging.lambda$main$0. // $0은 무슨 의미??

> ex) Debugging$$Lambda%5/28472098.apply(Unknwon Source) //무슨 의미??

- 이상한 문자는 람다 표현식 내부에서 에러가 발생했음을 가리킴.

> 람다 표현식은 이름이 없으므로 컴파일러가 람다를 참조하는 이름을 만들어 낸 것.

- 메서드 레퍼런스를 사용해도 스택 트레이스에 메서드명이 나타나지 않음.

- 메서드 레퍼런스를 사용하는 클래스와 같은 곳에 선언되어 있는 메서드를 참조할 때는 메서드 레퍼런스 이름이 스택 트레이스에 나타남.

> 람다 표현식과 관련한 스택 트레이스는 이해하기 어려울 수 있음.

> 향후 자바 컴파일러가 개선해야 할 부분.


4.2 정보 로깅

- 스트림의 파이프라인 연산을 디버깅 상황 가정.

> forEach로 스트림 결과를 출력

* forEach를 호출하는 순간 전체 스트림이 소비 됨.

* 스트림 파이프라인에 적용된 각각의 연산(map, filter, limit)이 어떤 결과를 도출하는지 확인 필요.

* peek 연산을 활용.

* peek은 스트림의 각 요소를 소비한 것처럼 동작을 실행. forEach처럼 실제로 스트림의 요소를 소비하지 않음.

* peek은 자신이 확인한 요소를 파이프라인의 다음 연산으로 그대로 전달.


5. 요약

- 람다 표현식으로 가독성이 좋고 더 유연한 코드 생성 가능.

- 익명 클래스는 람다 표현식으로 바꾸는 것이 좋음.

> 단, 이때 this, 변수 쉐도우 등 미묘하게 의미상 다른 내용이 있음을 주의.

> 메서드 레퍼런스로 람다 표현식보다 더 가독성이 좋은 코드 구현 가능.

- 반복적으로 컬렉션을 처리하는 루틴은 스트림으로 대채할 수 있을지 고려하는 것이 좋음.

- 람다 표현식으로 전략, 템플릿 메서드, 옵저버, 의무 체인, 팩토리 등의 객체지향 디자인 패턴에서 발생하는 불필요한 코드 제거 가능.

- 람다 표현식도 단위 테스트 수행 가능.

> 단, 람다 표현식 자체를 테스트하는 것보다는 람다 표현식이 사용되는 메서드의 동작을 테스트 하는 것이 바람직.

- 복잡한 람다 표현식은 일반 메서드로 재구현 가능.

- 람다 표현식을 사용하면 스택 트레이스 이해가 어려워짐.

- 스트림 파이프라인에서 요소를 처리할 때 peek 메서드로 중간값 확인 가능.

'Java8 > Java 8 in Action' 카테고리의 다른 글

null 대신 Optional  (0) 2017.07.12
디폴트 메서드  (0) 2017.07.05
병렬 데이터 처리와 성능  (0) 2017.06.29
스트림으로 데이터 수집  (0) 2017.06.29
스트림 활용 예제 소스  (0) 2017.06.26

7. 병렬 데이터 처리와 성능


자바 개발자는 컬렉션 데이터 처리 속도를 높이려고 따로 고민할 필요가 없음.

무엇보다 컴퓨터의 멀티코어를 활용해서 파이프라인 연산을 실행할 수 있음이 가장 중요한 특징.

자바7 이후 더 쉽게 병렬화를 수행하면서 에러를 최소화할 수 있도록 포크/조인 프레임워크 기능 제공.


1. 병렬 스트림

- 컬렉션에 parallelStream을 호출하면 병렬 스트림이 생성.

- 각각의 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림.

- 병렬 스트림을 이용하면 모든 멀티코어 프로세서가 각각의 청크를 처리하도록 할당 가능.


1.1 순차 스트림을 병렬 스트림으로 변환.

- 순차 스트림에 parallel 메서드를 호출하면 기존의 함수형 리듀싱 연산이 병렬 처리.

- 순차 스트림에 parallel을 호출해도 스트림 자체에는 아무 변화가 없음.

- 내부적으로는 parallel을 호출하면 이후 연산이 병렬로 수행해야 함을 의미하는 Boolean 플레그가 설정.

- 반대로 sequential로 병렬 스트림을 순차 스트림으로 변환 가능.


1.2 스트림 성능 측정.

- 성능을 최적화할때 세 가지 황금 규칙.

> 1. 측정

> 2. 측정

> 3. 측정...

- 올바른 자료구조를 선택해야 병렬 실행도 최적의 성능을 발휘 가능.

- 병렬화는 완전 꽁짜가 아님.

- 병렬화를 이용하려면 스트림을 재귀적으로 분할, 각 서브스트림을 서로 다른 스레드의 리듀싱 연산으로 할당, 이 결과를 하나의 값으로 합침.

- 멀티코어 간의 데이터 이동은 우리 생각보다 비쌈.

- 코어 간에 데이터 전송 시간보다 훨씬 오래 걸리는 작업만 병렬로 다른 코어에서 수행하는 것이 바람직.


1.3 병렬 스트림의 올바른 사용법.

- 병렬 스트림을 잘못 사용하면서 발생하는 많은 문제는 공유된 상태를 바꾸는 알고리즘을 사용하기 때문.

- 병렬 스트림과 병렬 계산에서는 공유된 가변 상태를 피해야 함.


1.4 병렬 스트림 효과적으로 사용하기.

- 확신이 서지 않는다면 직접 측정.

> 순차 스트림을 병렬 스트림으로 쉽게 변환 가능.

> 무조건 병렬 스트림으로 바꾸는 것이 능사가 아님.

> 따라서, 순차 스트림과 병렬 스트림 중 어떤 것이 좋을지 모르겠다면 적절한 벤치마크로 직접 성능 측정하는 것이 바람직.

- 박싱 주의.

> 자동 박싱과 언박싱은 성능을 크게 저하시킬 수 있는 요소.

> 자바8은 박싱 동작을 피할 수 있도록 기본형 특화 스트림 제공(IntStream, LongStream, DoubleStream)

> 되도록 기본형 특화 스트림을 사용하는 것이 좋음.

- 순차 스트림보다 병렬 스트림에서 성능이 떨어지는 연산 존재.

> limit, findFirst처럼 요소의 순서에 의존하는 연산을 병렬 스트림에서 수행하려면 비싼 비용을 치워야 함.

> findAny는 요소의 순서와 상관없이 연산하므로 findFirst보다 성능이 좋다.

> 정렬된 스트림에 unordered를 호출하면 비정렬된 스트림 획득 가능.

- 스트림에서 수행하는 전체 파이프라인 연산 비용 고려.

- 소량의 데이터에서는 병렬 스트림이 도움되지 않음.

> 병렬화 과정에서 생기는 부가 비용을 상쇄할 수 있을 만큼의 이득을 얻지 못하기 때문.

- 스트림을 구성하는 자료구조가 적절한지 확인.

- 스트림의 특성과 파이프라인의 중간 연산이 스트림의 특성을 어떻게 바꾸는지에 따라 분해 과정의 성능이 달라질 수 있음.

- 최종 연산의 병합 과정 비용 확인.

> 병합 과정의 비용이 비싸다면 병렬 스트림으로 얻은 성능의 이익이 서브스트림의 부분결과를 합치는 과정에서 상쇄될 수 있음.


표 7-1 스트림 소스와 분해성

소스

분해성

ArrayList

훌륭함

LinkedList

나쁨

IntStream.range

훌륭함

Stream.iterate

나쁨

HashSet

좋음

TreeSet

좋음


2. 포크/조인 프레임워크

- 병렬화할 수 있는 작업을 재귀적으로 작은 작업으로 분할한 다음에 서브태스크 각각의 결과를 합쳐서 전체 결과를 만들도록 설계.

- 서브태스크를 스레드 풀(ForkJoinPool)의 작업자 스레드에 분산 할당하는 ExecutorService 인터페이스 구현.


2.1 RecursiveTask 활용

- 스레드 풀을 이용하려면 RecursiveTask<R>의 서브클래스를 생성해야 함.

- R은 병렬화된 태스크가 생성하는 결과 형식 또는 결과가 없을 때는 RecursiveAction 형식.

> RecursiveTask를 정의하려면 추상 메서드 compute 구현 필요.

- compute 메서드는 테스크를 서브태스크로 분할하는 로직과 더 이상 분할할 수 없을 때 개별 서브태스크의 결과를 생산할 알고리즘을 정의.

- 일반적으로 어플리케잇ㄴ에서는 둘 이상의 ForkJoinPool을 사용하지 않음.

- 즉, 소프트웨어의 필요한 것에서 언제든 가져다 쓸 수 있도록 ForkJoinPool을 한 번만 인스턴스화해서 정적 필드에 싱글턴으로 저장.


ForkJoinSumCalculator 실행

- ForkJoinSumCalculator를 ForkJoinPool로 전달하면 풀의 스레드가 ForkJoinSumCalculator의 compute 메서드를 실행하면서 작업 수행.

- compute 메서드는 병렬로 실행할 수 있을만큼 태스크의 크기가 충분히 작아졌는지 확인, 아직 태스트의 크기가 크다고 판단되면숫자 배열의 반으로 분할해서 두 개의 새로운 ForkJoinSumCalculator로 할당.

> 그러면 다시 ForkJoinPool이 새로 생성된 ForkJoinSumCalculator를 실행.

> 결국 이 과정이 재귀적으로 반복되면서 주어진 조건을 만족할 떄 까지 태스크 분할을 반복.


2.2 포크/조인 프레임워크를 제대로 사용하는 방법

- join 메서드를 태스크에 호출하면 태스크가 생산하는 결과를 준비될 때까지 호출자를 블록.

> 따라서, 두 서브태스크가 모두 시작된 다음에 join을 호출 해야 함.

> 각각의 서브태스크가 다른 태스크가 끝나길 기다리는 일이 발생하며 원래 순차 알고리즘보다 느리고 복잡한 프로그램이 되어버릴 수 있음.

- RecursiveTask 내에서는 ForkJoinPool의 invoke 메서드를 사용하지 말아야 함.

> 대신 compute나 fork 메서드를 직접 호출 가능.

> 순차 코드에서 병렬 계산을 시작할 때만 invoke를 사용.

- 서브태스크에 fork 메서드를 호출해서 ForkJoinPool의 일정 조절가능.

> 왼쪽 작업과 오른쪽 작업 모두 fork 메서드를 호출하는 것이 자연스러울 것 같지만 한쪽 작업에는 fork를 호출하는 것보다 compute를 호출하는 것이 효율적.

> 두 서브태스크의 한 태스크에는 같은 스레드를 재사용할 수 있으므로 풀에서 불필요한 태스크를 할당하는 오버헤드를 피할 수 있음.

- 병렬 계산은 디버깅하기 어려움.

> 보통 IDE로 디버깅할 때 스택 프레이스로 문제가 일어난 과정을 쉽게 확인 가능하나, 포크/조인 프레임워크에서는 fork라 불리는 다른 스레드에서 compute를 호출하므로 스택 트레이스가 도움되지 않음.

- 멀티코어에 포크/조인 프레임워크를 사용하는 것이 순차 처리보다 무조건 빠를 거라는 생각은 금물.

> 병렬 처리로 성능을 개선하려면 태스크를 여러 독립적인 서브태스크로 분할 가능해야 함.

> 각 서브태스크의 실행시간은 새로운 태스크를 포킹하는 데 드는 시간보다 길어야 함.


2.3 작업 훔치기

- 코어 개수와 관계없이 적절한 크기로 분할된 많은 태스크를 포킹하는 것이 바람직.

- 이론적으로는 코어 개수만큼 병렬화된 태스크로 작업부하를 분할하며 모든 CPU 코어에서 태스크를 실행할 것이고 크기가 같은 각각의 태스크는 같은 시간에 종료될 것이라 생각할 수 있음.

- 하지만 복잡한 시나리오가 사용되는 현실에서는 각각의 서브태스크의 작업완료 시간이 크게 달라질수 있음.

- 분할 기법이 효율적이지 않았기 때문일수도 있고, 예기치 않게 디스크 접근 속도가 저하되었거나 외부 서비스와 협력하는 과정에서 지연이 발생 할 수도 있기 때문.

- 작업 훔치기라는 기법을 통해 위 문제를 해결.

> ForkJoinPool의 모든 스레드를 거의 공정하게 분할함.

> 각각의 스레드는 자신에세 할당된 태스크를 포함하는 이중 연결 리스트를 참조하면서 작업이 끝날 때 마다 큐의 헤드에서 다른 테스크를 가져와 작업을 처리.

> 이때 한 스레드는 다른 스레드 보다 자신에게 할당된 태스크를 더 빨리 처리 가능.

> 즉, 다른 스레드는 바쁘게 일하고 있는데 한 스레드는 할일이 다 떨어진 상황. 이때 할일이 없는 스레드는 유휴 상태로 바뀌는 것이 아니라 다른 스레드 큐의 꼬리에서 작업을 훔쳐옴.

> 모든 태스크가 작업을 끝낼 때 까지 모든 큐가 빌 때까지 이 과정을 반복.

> 따라서, 태스크의 크기를 작게 나누어야 작업자 스레드 간의 작업부하를 비슷한 수준으로 유지 가능.


3. Spliterator

- Spliterarot는 "분할할 수 있는 반복자" 라는 의미

- Iterator처럼 소스의 요소 탐색 기능을 제공하는 점은 같지만 Spliterator는 병렬 작업에 특화.

- Spliterator 인터페이스는 여러 메서드를 정의

> tryAdvance: Spliterator의 요소를 하나씩 순차적으로 소비하면서 탐색해야 할 요소 존재 시 true 반환.

> trySplit:  Spliterator의 일보 요소(자신이 반환한 요소)를 반할해서 두 번쨰 Spliterator를 생성.

> estimateSize: 탐색해야할 요소 수 정보를 제공.


3.1 분할 과정

- 스트림을 여러 스트림으로 분할하는 과정은 재귀적으로 일어남.

- trySplit의 결과가 null이 될때까지 재귀적으로 요소를 분할.

- 이 분할과정은 characteristics 메서드로 정의하는 Spliterator의 특성에 영향을 받음.


Spliterator 특성

- characteristics라는 추상 메서드 정의.

- Characteristics 메서드는 Spliterator 자체의 특성 집합을 포함하는 int 반환.

표 7-2 Spliterator 특성

특성

의미

ORDERED

리스트처럼 요소에 정해진 순서가 있으므로 Spliterator 요소를 탐색하고 분할할 순서에 유의.

DISTINCT

x, y 요소를 방문했을 x.equals(y) 항상 false 반환.

SORTED

탐색된 요소는 미리 정의된 정렬 순서를 따름.

SIZED

크기가 알려진 소스로 Spliterator 생성했으므로 estimatedSize() 정확한 값을 반환.

NONNULL

탐색하는 모든 요소는 null 아님.

IMMUTABLE

Spliterator 소스는 불변. , 요소를 탐색하는 동안 요소를 추가하거나, 삭제하거나, 고칠 없음.

CONCURRENT

동기화 없이 Spliterator 소스를 여러 스레드에서 동시게 고칠 있음.

SUBSIZED

Spliterator 그리고 분할된 모든 Spliterator SIZED 특성을 갖음.


4. 요약

- 내부 반복을 이용하면 명시적으로 다른 스레드를 사용하지 않고도 스트림을 병렬 처리 가능.

- 간단하게 스트림을 병렬로 처리가능. 단, 항상 병렬 처리가 빠른 것은 아님.

> 병렬 소프트웨어 동작 방법과 성능은 직관적이지 않을 때가 많으므로 병렬 처리를 사용했을 때 성능을 직접 측정해봐야 함.

- 병렬 스트림으로 데이터 집합을 병렬 실행할 때 특히 처리해야 할 데이터가 아주 많거나 각 요소를 처리하는 데 오랜 시간이 거릴 때 성능을 높일 수 있음.

- 기본형 특화 스트림을 사용하는 등 올바른 자료구조 선택이 어떤 연산을 병렬로 처리하는 것보다 성능적으로 더 큰영향을 미칠 수 있음.

- 포크/조인 프레임워크에서는 병렬화할 수 있는 태스크를 작은 태스크로 분할한 다음에 분할된 태스크를 각각의 스레드로 실행하며 서브태스크 각각의 결과를 합쳐서 최종 결과 생성.

- Spliterator는 탐색하려는 데이터를 포함하는 스트림을 어떻게 병렬화할 것인지 정의.

'Java8 > Java 8 in Action' 카테고리의 다른 글

디폴트 메서드  (0) 2017.07.05
리팩토링, 테스팅, 디버깅  (0) 2017.07.03
스트림으로 데이터 수집  (0) 2017.06.29
스트림 활용 예제 소스  (0) 2017.06.26
스트림 활용  (0) 2017.06.26

6. 스트림으로 데이터 수집


collect 역시 다양한 요소 누적 방식을 인수로 받아 스트림을 최종 결과로 도출하는 리듀싱 연산 수행 가능.

다양한 요소 누적 방식은 Collector 인터페이스에 정의.

컬렉션(Collection), 컬렉터(Collector), collect를 헷깔리지 않도록 주의.


1. 컬렉터란 무엇인가?

- Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정.


1.1 고급 리듀싱 기능을 수행하는 컬렉터

- 스트림에 collect를 호출하면 스트림의 요소에(컬렉터로 파라미터화된) 리듀싱 연산 수행.

- collect에서는 리듀싱 연산을 이용해 스트림의 각 요소를 방문하면서 컬렉터가 작업 처리.

- 보통 함수를 요소로 변환할 때는 컬렉터를 적용하며 최종 결과를 저장하는 자료구조에 값을 누적.

- Collector 인터페이스의 메서드를 어떻게 구현하느냐에 따라 스트림에 어떤 리듀싱 연산을 수행할지 결정.


1.2 미리 정의된 컬렉터

- groupingBy 같이 Collectors 클래스에서 제공되는 팩토리 메서드 기능 설명.

- Collectors에서 제공하는 메서드의 기능은 크게 3가지로 구분.

> 스트림 요소를 하나의 값을 리듀스 하고 요약

> 요소 그룹화

> 요소 분할


2. 리듀싱와 요약

- 컬렉터로 스트림의 항목을 컬렉션으로 재구성 가능.(컬렉터로 스트림의 모든 항목을 하나의 결과로 합칠수 있음)

- 트리를 구성하는 다수준 맵, 잔순한 정수 등 다양한 형식 도출 가능.

2.1 스트림에서 최대값과 최소값 검색

- Collectors.maxBy, Collectors.minBy 두 개의 메서드를 이용하여 스트림의 최대값, 최소값 계산 가능.

- 두 컬렉터는 스트림의 요소를 비교하는데 사용할 Comparator를 인수로 받음.

- 자바8은 값을 포함하거나 포함하지 않을 수 있는 컨테이너 Optional을 제공.

> max, min값은 반환 값이 포함되지 않을 수 있음.

- 스트림에 있는 객체의 숫자 필드의 합계나 평균 등을 반환하는 연산에도 리듀싱 기능이 자주 사용.

> 이러한 연산을 요약(summarization)연산 이라 부름.


2.2. 요약 연산

- Collectors 클래스는 Collectors.summingInt라는 특별한 요약 팩토리 메서드 제공.

> summingInt는 객체를 int로 매핑하는 함수를 인수로 받음.

> summingInt의 인수로 전달된 함수는 객체를 int로 매핑하는 컬렉터를 반환.

- 단순 합계 외에 평균값 계산 등의 연산도 요약 가능.

- 두 개 이상의 연산을 한번에 수행할 경우 펙토리 메서드 summarizaingInt가 반환하는 컬렉터 사용 가능.


- int뿐 아니라 long, double에 대응하는 메서드도 존재함.


2.3 문자열 연결

- 컬렉터에 joining 팩토리 메서드를 이용하면 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환.

- joining 메서드는 내부적으로 StringBuilder를 이용해 문자열을 하나로 만듦.



2.4 범용 리듀싱 요약 연산

- 지금까지 살펴본 모든 컬렉터는 reducing 팩토리 메서드로도 정의 가능.

> 즉, 범용 Collectors.reducing으로 구현 가능.

- 범용 팩토리 메서드 대신 특화된 컬렉터를 사용하는 이유는 프로그래밍적 편의성 때문.



- reducing은 세 개의 인수를 받음

1) 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때는 반환값임.

2) 변환 함수

3) 같은 종류의 두 항목을 하나의 값으로 더한ㄴ BinaryOperator.

- 한 개의 인수를 갖는 reducing 팩토리 메서드는 세 개의 인수를 갖은 reducing 메서드에서 스트림의 첫 번째 요소를 시작요소, 즉 첫 번째 인수로 받으며, 자신을 그대로 반환하는 항등 함수를 두 번째 인수로 받는 상황에 해당.

> 즉, 한 개의 인수를 갖는 reducing 컬렉터는 시작값이 없으므로 빈 스트림이 넘겨졌을 때 시작값이 설정되지 않은 상황.

> 한 개의 인수를 갖는 reducing은 Optional<?> 객체를 반환.


컬렉션 프레임워크 유연성: 같은 연산도 다양한 방식으로 수행 가능!

- 람다 표현식 대신 Integer 클래스의 sum 메서드 레퍼런스등을 이용하면 코드 단순화 가능.


자신의 상황에 맞는 최적의 해법 선택

- 함수형 프로그래밍에서는 하나의 연산을 다양한 방법으로 해결할 수 있음을 보여줌.

- 스트림 안터페이스에서 직접 제공하는 메서드를 이용하는 것에 비해 컬렉터를 이용하는 코드가 더 복잡.

- 코드가 복잡한 대신 재사용성과 커스터마이즈 가능성을 제공하는 높은 수준의 추상화와 일반화를 획득.

- 문제를 해결할 수 있는 다양한 해결 방법을 확인한 다음 가장 일반적으로 문제데 특화된 해결책을 고르는 것이 바람직.


3. 그룹화

- 데이터 집합을 하나 이상의 특성으로 분류해서 그룹화하는 연산도 데이터베이스에서 많이 수행되는 작업.

- 명령형으로 그룹화를 구현하려면 까다롭고, 할일이 많으며, 에러도 많이 발생.

- 함수형을 이용하면 가독성 있는 한 줄의 코드로 그룹화를 구현.

- Collectors.groupingBy를 이용하여 쉽게 그룹화 가능.

- 이 함수를 기준으로 스트림이 그룹화되므로 이를 분류 함수(classification function)이라고 부름.

- 단순한 속성 접근자 대신 더 복잡한 분류 기준이 필요한 상황에서는 메서드 레퍼런스를 분류 함수로 사용 불가.

> 이 경우, 람다 표현식으로 필요 로직 구현 가능.


3.1 다수준 그룹화

- 두 인수를 받는 팩토리 메서드 Collectors.groupingBy를 이용해 항목을 다수준으로 그룹화 가능.

- Collectors.groupingBy는 일반적인 분류 함수와 컬렉터를 인수로 받음.

- 바깥쪽 groupingBy 메서드에 스트림의 항목을 분류할 두 번째 기준을 정의하는 내부 groupingBy를 전달해서 두 수준으로 스트림의 항목 그룹화 가능


3.2 서브그룹으로 데이터 수집

- 첫 번째 groupingBy로 넘겨주는 컬렉터의 형식은 제한이 없음.


컬렉터 결과를 다른 형식에 적용

- 그룹화 연산에서 맵의 모든 값을 Optional처리할 필요 없음.

- 팩토리 메서드 Collectors.collectingAndThen으로 컬렉터가 반환한 결과를 다른 형식으로 활용 가능.

- 팩토리 메서드 collectingAndThen은 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환.

- 반환되는 컬렉터는 기존 컬렉터의 래퍼 역활을 하며 collect의 마지막 과정에서 변환 함수로 자신이 반환하는 값을 매핑.


groupingBy와 함께 사용하는 다른 컬렉터 예제

- 일반적으로 스트림에서 같은 그룹으로 분류된 모든 요소에 리듀싱 작업을 수행할 때는 팩토리 메서드 groupingBy에 두 번째 인수로 전달한 컬렉터를 사용.

- 이 외에도 mapping 메서드로 만들어진 컬렉터도 groupingBy와 자주 사용.

- mapping 메서드는 스트림의 인수를 변환하는 함수와 변환 함수의 결과 객체를 누적하는 컬렉터를 인수로 받음.

- mapping은 입력 요소를 누적하기 전에 매핑 함수를 적용해서 다양한 형식의 객체를 주어진 형식의 컬렉터에 맞게 변환하는 역할.


4. 분할

- 분할 함수(patitioning function)라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능.

- 불린을 반환하므로 맵의 키 형식은 boolean.

> 결과적으로 그룹화 맵은 최대 두 개의 그룹으로 분리(true or false).


4.1 분할의 장점

- 분할 함수가 반환하는 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다는 것이 분할의 장점.


5. Collector 인터페이스

- 리듀싱 연산(즉, 컬렉터)을 어떻게 구현할지 제공하는 메서드 집합으로 구성.

> T는 수집될 스트림 항목의 제네릭 형식

> A는 누적자. 즉, 수집 과정에서 중간 결과를 누적하는 객체의 형식.

> R은 수집 연산 결과 객체의 형식.


5.1 Collector 인터페이스 메서드

- 다섯가지의 메서드가 존재.

- 네 개의 메서드는 collect 메서드에서 실행하는 함수 반환.

- 다섯 번째 메서드 characteristics는 collect 메서드가 어떤 최적화를 이용해서 리듀싱 연산을 수행할 것인지 결정하도록 돕는 힌트 특성 집합 제공.


supplier 메서드: 새로운 결과 컨테이너 생성.

- 빈 결과로 이루어진 Supplier 반환해야 함.

> 즉, 수집과정에서 빈 누적자 인터페이스를 만드는 파라미터가 없는 함수.

- ToListCollector 처럼 누적자를 반환하는 컬렉터에서는 빈 누적자가 비어있는 스트림의 수집 과정의 결과가 될수 있음.


accumulator 메서드: 결과 컨테이너에 요소 추가.

- 리듀싱 연산을 수행하는 함수를 반환.

- 스트림에서 n번째 요소를 탐색할 때 두 인수, 즉, 누적자(스트림의 첫 n-1개 항목을 수집한 상태)와 n번째 요소를 함수에 적용.

- 함수의 반환값은 void.

> 즉, 요소를 탐색하면서 적용하는 함수에 의해 누적자 내부 상태가 바뀌므로 누적자가 어떤 값일지 단정할 수 없음.


finisher 메서드: 최종 변환값을 결과 컨테이너로 적용.

- 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환.

- 때로는 누적자 객체가 이미 최정 결좌인 상황도 존재. (ex- toListCollector)

> 이 경우 변환 과정이 필요하지 않으므로 finisher 메서드는 항등 함수를 반환.


combiner 메서드: 두 결과 컨테이너 병합

- 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의.

- 스트림의 리듀싱을 병렬로 수행 가능.

> 스트림의 리듀싱을 병렬로 수행할 때 포크.조인 프레임워크와 Spliterator를 사용. (다음 챕터에서 추가 설명.)


Characteristics 메서드

- 컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환.

- 스트림을 병렬로 리듀스할 것인지 그리고 병렬로 리듀스한다면 어떤 최적화를 선택할지 힌트 제공.

- 다음 세 항목을 포함하는 열거형임.

> UNORDERED

* 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향 받지 않음.

> CONCURRENT

* 다중 스레드에서 accumulator 함수를 동시에 호출 가능.

* 스트림의 병렬 리듀싱 수행 가능.

* 컬렉터의 플래그에 UNORDERED를 함꼐 설정하지 않았다면 데이터 소스가 정룔되어 있지 않은 상황에서만 병렬 리듀싱 수행 가능.

> IDENTITY_FINISH

* finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략 가능.

* 따라서 이듀싱 과정의 최종 결과로 누적자 객체를 바로 사용 가능.


6. 요약

- collect는 스트림의 요소를 요약 결과로 누적하는 다양한 방법을 인수로 갖는 최종 연산.

- 스트림의 요소를 하나의 값으로 리듀스하고 요약하는 컬렉터뿐 아니라 최소값, 최대값, 평균값을 계산하는 컬렉터 등이 미리 정의되어 있음.

- 미리 정의된 컬렉터인 groupingBy로 스트림의 요소를 그룹화하거나, partitioningBy로 스트림의 요소 분할 가능.

- 컬렉터는 다수준의 그룹화, 분할, 리듀싱 연산에 적합하게 설계.

- Collector 인터페이스에 정의된 메서드를 구현해서 커스텀 컬렉터 개발 가능.


'Java8 > Java 8 in Action' 카테고리의 다른 글

리팩토링, 테스팅, 디버깅  (0) 2017.07.03
병렬 데이터 처리와 성능  (0) 2017.06.29
스트림 활용 예제 소스  (0) 2017.06.26
스트림 활용  (0) 2017.06.26
스트림 소개  (0) 2017.06.16

+ Recent posts