12. 새로운 날짜와 시간 API

자바 API는 복잡한 어플리케이션을 만드는 데 필요한 여러가지 유횽한 컴포넌트 제공.

자바 API가 항상 완벽한 것은 아님.

대부분의 자바 개발자가 지금까지의 날짜와 시간 관련 기능에 불만족.

- 자바 8에서 지금까지의 문제를 개선하는 새로운 날짜와 시간 API 제공.

자바1.0에서는 java.util.Date 클래스 하나로 날짜와 시간 관련 기능 제공.

- 날짜를 의미하는 Date라는 클래스의 이름과 달리 Date 클래스는 특정 시점을 날짜가 아닌 밀리초 단위로 표현.

- 게다가 1900년을 기준으로 하는 오프셋, 0에서 시작하는 달 인덱스 등 모호한 설계로 유용성 저하.

- Date는 JVM 기본시간대인 CET, 즉, 중앙 유럽 시간을 사용.

- Date클래스가 자체적으로 시간대 정보를 알고 있는 것도 아님.

자바1.0의 Date클래스에 문제가 있다는 점에는 의문의 여지가 없지만 과거 버전과 호환성을 깨뜨리지 않으면서 이를 해결할 방법이 없음.

- 자바1.1에서는 Date클래스의 열 메서드를 사장(Deprecated) 시키고 java.util.Calendar라는 클래스를 대안으로 제공.

- 안타깝게도 Calendar 클래스 역시 쉽게 에러를 일으키는 설계 문제를 갖고 있음.

DateFormat에도 문제가 있었음.

- DateFormat은 스레드에 안전하지 않음. 즉, 두 스레드가 동시에 하나의 포메터로 날짜를 파싱할 때 예기치 못한 결과 발생 가능.

Date와 Calendar는 모두 가변 클래스.

부실한 날짜와 시간 라이브러리 때문에 많은 개발자는 Joda-Time같은 써드파티 날짜와 시간 라이브러리를 사용해옴.

오라클은 좀 더 훌륭한 날짜와 시간 API 제공을 결정.

- 자바8에서는 Joda-Time의 많은 기능을 java.time 패키지로 추가.


1 LocalDate, LocalTime, Instant, Duration, Period

- java.time 패키지는 LocalDate, LocalTime, LocalDateTime, Instant, Duration, Period등 새로운 클래스 제공.


1.1 LocalDate와 LocalTime 사용

- 새로운 날짜와 시간 API를 사용할 때 처음 접하게 되는 것은 LocalDate.

- LocalDate 인스턴스는 시간을 제외한 날짜를 표현하는 불변 객체.

> 특히 LocalDate 객체는 어떤 시간대 정보도 포함하지 않음.

- 정적 팩토리 메서드 of로 LocalDate 인스턴스 생성 가능.

- 팩토리 메서드 now는 시스템 시계의 정보를 이용해서 현재 날짜 정보를 얻음.

- get 메서드에 TemporalField를 전달해서 정보를 얻는 방법도 존재.

> TemporalField는 시간 관련 객체에서 어떤 필드의 값에 접근할지 정의하는 인터페이스.

> 열거자 CheoneField는 TemporalField인터페이스를 정의하므로 ChronoField의 열저가 요소를 이용해 원하는 정보를 얻을 수 있음.

- 13:45:20 같은 시간은 LocalTime 클래스로 표현 가능.

- 날짜와 시간 문자열로 LocalDate와 LocalTime의 인스턴스를 만드는 방법도 존재.

> parse 정적 메서드 사용 가능.

* ex) LocalDate date = LocalDate.parse("2017-07-24");

* ex) LocalTime time = LocalTime.parse("13:45:20");

- parse 메서드에 DateTimeFormatter 전달 가능.

- DateTimeFormatter의 인스턴스는 날짜, 시간 객체의 형식을 지정.

> DateTimeFormatter는 java.util.DateFormat 클래스를 대채하는 클래스.

- 문자열을 LocalDate나 LocalTime으로 파싱할 수 없을 때 parse 메서드는 DateTimeParseException(RuntimeException을 상속 받는 예외) 발생.


1.2 날짜와 시간 조합

- LocalDateTime은 LocalDate와 LocalTime을 쌍으로 갖는 복합 클래스.

- 날짜와 시간 모두 표현이 가능하며 날짜와 시간 조합도 가능.

- LocalDate의 atTime 메서드에 시간을 제공하거나 LocalTime의 atDate 메서드에 날짜를 제공해서 LocalDateTime 생성 가능.


1.3 Instant: 기계의 날짜와 시간

- 사람은 보통 주, 날짜, 시간, 분으로 날짜와 시간을 계산.

- 하지만 기계에서는 이와 같은 단위로 시간을 표현하기 어려움.

- 기계의 관점에서는 연속된 시간에서 특정 지점을 하나의 큰 수로 표현하는 것이 가장 자연스러운 시간 표현 방법.

- 새로운 java.time.Instant 클래스에서는 이와 같은 기계적인 관점에서 시간을 표현.

> 즉, Instant 클래스는 유닉스 에포크 시간을 기준으로 특정 지점까지의 시간을 초로 표현.

* Unix epoch time(1970년 1월1일 0시 0분 0초 UTC)

- LocalDate등을 포함하여 사람이 읽을 수 있는 날짜 시간 클래스에서 그랬던 것 처럼 Instant클래스도 사람이 확인 가능한 시간표시 메서드 now 제공.

> 하지만, Instant는 기계 전용의 유틸리티라는 점을 상기.

> 즉, Instant는 초와 나노초 정보를 포함.

> 따라서 Instant는 사람이 읽을 수 있는 시간 정보를 제공하지 않음.

- Instant에서는 Duration과 Period 클래스를 함께 사용 가능.


1.4 Duration과 Period 정의

- Temporal 인터페이스는 특정 시간을 모델링하는 객체으 값을 어떻게 읽고 조작할지 정의.

- Duration 클래스의 정적 팩토리 메서드 between으로 두 시간의 객체 사이의 지속시간 생성 가능.

- LocalDateTime은 사람이 사용, Instant는 기계가 사용하도록 만들어진 클래스로 두 인스턴스는 서로 혼합 불가.

- 또한 Duration 클래스는 초와 나노초로 시간 단위를 표현하므로 between 메서드에 LocalDate 전달 불가.

- 년,월,일로 시간을 표현할 때는 Period 클래스를 사용.

> 즉, Period 클래스의 팩토리 메서드 between을 이용하면 두 LocalDate의 차이 확인 가능.

- Duration과 Period 클래스는 자신의 인스턴스를 만들 수 있도록 다양한 팩토리 메서드 제공.


표 12-1 간격을 표현하는 날짜와 시간 클래스의 공통 메서드

메서드

정적

설명

between

시간 사이의 간격을 생성함.

from

시간 단위로 간격을 생성함.

of

주어진 구성 요소에서 간격 인스턴스를 생성함.

parse

문자열을 파싱해서 간격 인스턴스를 생성함.

addTo

아니요

현재값의 복사본을 생성한 다음에 지정된 Temporal 객체에 추가함.

get

아니요

현재 간격 정보값을 읽음.

isNegarive

아니요

간격이 음수인지 확인함.

isZero

아니요

간격이 0인지 확인함,

minus

아니요

현재값에서 주어진 시간을 복사본을 생성함.

multipliedBy

아니요

현재값에서 주어진 값을 곱한 복사본을 생성함,

negated

아니요

주어진 값의 부호를 반전한 복사본을 생성함.

plus

아니요

현재값에서 주어진 시간을 더한 복사본을 생성함.

subtractFrom

아니요

지정된 Temporal 객체에서 간격을 .

- 지금까지 살펴본 모든 클래스는 불변.

> 불변 클래스는 함수형 프로그래밍 그리고 스레드 안전성과 도메인 모델의 일관성을 유지하는데 좋은 특징.

> 하지만, 새로운 날짜와 시간 API에서는 변경된 객체 버전을 만들 수 있는 메서드를 제공.


2 날짜 조정, 파싱, 포매팅

- withAttribute 메서드로 기존의 LocalDate를 바꾼 버전을 직접 간단하게 생성 가능.

- Temporal 인터페이스는 LocalDate, LocalTime, LocalDateTime, Instant처럼 특정 시간을 정의.

> 정확히 표현하자면 get과 with메서드로 Temporal 객체의 필드값을 읽거나 수정 가능.

- 어떤 Temporal 객체가 지정된 필드를 지원하지 않으면 UnsupportedTemporalTypeExcetion이 발생.

- 메서드의 인수에 숫자와 TemporalUnit 활용 가능.

- ChronoUnit 열거형은 TemporlaUnit 인터페이스를 쉽게 활용할 수 있는 구현을 제공.

- LocalDate, LocalTime, LocalDateTime, Instant등 날짜와 시간을 표현하는 모든 클래스는 서로 비슷한 메서드를 제공.

표 12-2 특정 시점을 표현하는 날짜 시간 클래스의 공통 메서드

메서드

정적

설명

from

주어진 Temporal 객체를 이용해서 클래스의 인스턴스를 생성함.

now

시스템 시계로 Temporal 객체를 생성함.

of

주어진 구성 요소에서 Temporal 객체의 인스턴스를 생성함.

parse

문자열을 파싱해서 Temproal 객체를 생성함.

atOffset

아니요

시간대 오프셋과 Temporal 객체를 합침.

atZone

아니요

시간대와 Temporal 객체를 합침.

format

아니요

지정된 포매터를 이용해서 Temporal 객체를 문자열로 변환함.(Instant 미지원)

get

아니요

Temporal 객체의 상태를 읽음.

minus

아니요

특정 시간을 Temporal 객체의 복사본을 생성함.

plus

아니요

특정 시간을 더한 Temporal 객체의 복사본을 생성함.

with

아니요

일부 상태를 바꾼 Temporal 객체의 복사본을 생성함.


2.1 TemporalAdjusters 사용하기

- 좀 더 다양한 동작을 수행할 수 있는 기능을 제공하는 TemporalAdjuster를 통해 다양한 문제 해결 가능.

- 날짜와 시간 API는 다양한 상황에서 사용할 수 있도록 다양한 TemporalAdjuster 제공.

표 12-3 TemporalAdjusters 클래스의 팩토리 메서드

메서드

설명

dayOfWeekInMonth

3월의 둘째 화요일처럼 서수 요일에 해당하는 날짜는 반환하는 TemporalAdjuster 반환함.

firstDayOfMonth

현재 달의 번째 날짜를 반환하는 TemporalAdjuster 반환함.

firstDayOfNextMonth

다음 달의 번째 날짜를 반환하는 TemporalAdjuster 반환함,

firstDayOfNextYear

내년의 번째 날짜를 반환하는 TemporalAdjuster 반환함.

firstDayOfYear

올해의 번째 날짜를 반환하는 TemporalAdjuster 반환함.

firstInMonth

3월의 번째 화요일처럼 현재 달의 번째 요일에 해당하는 날짜를 반환하는 TemporalAdjuster 반환함.

lastDayOfMonth

현재 달의 마지막 날짜를 반환하는 TemporalAdjuster 반환함.

lastDayOfNextMonth

다음 달의 마지막 날짜를 반환하는 TemporalAdjuster 반환함.

lastDayOfYear

올해 마지막 날짜를 반환하는 TemporalAdjuster 반환함.

lastInMonth

3월의 마지막 화요일처럼 현재 달의 마지막 요일에 해당하는 날짜를 반환하는 TemporalAdjuster 반환함.

next

현재 날짜 이후로 지정한 요일이 처음으로 나타나는 날짜를 반환하는 TemporalAdjuster 반환함.(현재 날짜는 미포함)

previous

현재 날짜 이후로 역으로 날짜를 거슬러 올라가며 지정한 요일이 처음으로 나타나는 날짜를 반환하는 TemporalAdjuster 반환함(현재 날짜 미포함)

nextOrSame

현재 날짜 이후로 지정한 요일이 처음으로 나타나는 날짜를 반환하는 TemporalAdjuster 반환함(현재 날짜 포함)

previousOrSame

현재 날짜 이후로 역으로 날짜를 거슬로 올라가며 지정한 요일이 처음으로 나타나는 날짜를 반환하는 TemporalAdjuster 반환함(현재 날짜 포함)

- TemporalAdjuster를 이용하면 좀 더 복잡한 날짜 조정 기능을 직관적으로 해결 가능.

> 추가로 필요한 기능이 정의되어 있지 않을 때 비교적 쉽게 커스텀 TemporalAdjuster 구현 가능.

* TemporalAdjuster 인터페이스는 하나의 메서드만 정의함.(하나의 메서드만 정의하므로 함수형 인터페이스)

* TemporalAdjuster 인터페이스 구현은 Temporal 객체를 어떻게 다른 Temporal 객체로 변환할지 정의.


2.2 날짜와 시간 객체 출력과 파싱

- 날짜와 시간 관련 작업에서 포매팅과 파싱은 서로 떨어질 수 없는 관계.

> 심지어 포매팅과 파싱 전용 패키지인 java.time.format이 새로 추가.

> 해당 패키지에서 가장 중요한 클래스는 DateTimeFormatter.

- DateTimeFormatter 클래스는 BASIC_ISO_DATE와 ISO_LOCAL_DATE 등의 상수를 미리 정의.

- DateTimeFormatter를 이용해서 날짜나 시간을 특정 형식의 문자열로 생성 가능.

- 반대로 날짜나 시간을 표현하는 문자열을 파싱해서 날짜 객체 다시 생성 가능.

- 날짜와 시간 API에서 특정 시점이나 간격을 표현하는 모든 클래스의 팩토리 메서드 parse를 이용해서 문자열을 날짜 객체로 생성 가능.

- 기존의 java.util.DateFormat 클래스와 달리 모든 DateTimeFormatter는 스레드에서 안전하게 사용할수 있는 클래스.

- DateTimeFormatterBuilder 클래스로 복합적인 포매터를 정의하므로써 좀 더 세부적으로 포매터 제어 가능.

> 즉, DateTimeFormatterBuilder 클래스로 대소문자를 구분하는 파싱, 관대한 규칙을 적용하는 파싱(정해진 형식과 정확하게 일치하지 않는 입력을 해석할 수 있도록 체험적 방식의 파서 사용), 패딩, 포매터의 선택사항 등을 활용.


3 다양한 시간대와 캘린더 활용 방법

- 새로운 날짜와 시간 API의 큰 편리함 중 하나는 시간대를 간단하게 처리할 수 있다는 점.

- 기존의 java.util.TimeZone을 대체할 수 있는 java.time.ZoneId 클래스가 새롭게 등장.

- 새로운 클래스를 이용하면 서머타임 같은 복잡한 사항이 자동으로 처리.

- 날짜와 시간 API에서 제 공하는 다른 클래스와 마찬가지로 ZoneId는 불변 클래스.

- 표준 시간이 같은 지역을 묶어서 시간대로 규정.

- ZoneRules 클래스에는 약 40개 정도의 시간대가 존재.

> ZoneId의 getRules()를 이용해서 해당 시간대의 규정 획득 가능.


3.1 UTC/GMT 기준의 고정 오프셋

- 때로는 UTC(Universal Time Coordinated)(협정 세계 시) / GMT(Greenwich Mean Time)(그리니치 표준시)를 기준으로 시간대를 표현하기도 함.

- 예로 "뉴욕은 런던보다 5시간 느리다"라고 표현 가능.

> ZoneId의 서브클래스인 ZoneOffset 클래스로 런던의 그리니치 0도 자오선과 시간값의 차이 표현 가능.

* ZoneOffset newYorkOffseet = ZoneOffset.of("-05:00");

> 실제로 미국 동부 표준시의 오프셋값은 -05:00.

* 하지만 서머타임을 제대로 처리할 수 없으므로 비권장 방식.


3.2 대안 캘린더 시스템 사용하기

- ISO-8601 캘린더 시스템은 실질적으로 전 세계에서 통용.

- 자바8에서는 추가로 4개의 캘린더 시스템을 제공.

- thaiBuddhistDate, MinguoDate, JapaneseDate, HijrahDate 4개의 클래스가 각각의 캘린더 시스템을 대표.

- 4개의 틀래스와 LocalDate 클래스는 ChronoLocalDate 인터페이스를 구현하는데, ChronoLocalDate는 임의의 연대기에서 특정 날짜를 표현할 수 있는 기능을 제공하는 인터페이스.

- 날짜와 시간 API의 설계자는 ChronoLocalDate보다는 LocalDate를 사용하라고 권고.

- 프로그램의 입출력을 지역화하는 상황을 제외하고는 모든 데이터 저장, 조작, 비즈니스 규칙 해석등의 작업에서 LocalDate를 사용해야 함.


이슬람력

- 자바8에 추가된 새로운 캘린더 중 HijrahDate(이슬람력)가 제일 복잡.

> 이슬람력에는 변형이 있기 때문.

- HijrahDate 캘린더 시스템은 태음월(lunar month)에 기초.

- 새로운 달을 결정할 떄 새로운 달을 전 세계 어디에서나 볼 수 있는지 아니면 사우디아라비아에서 처음으로 새로운 달을 볼 수 있는지 등의 변형 방법을 결정하는 메서드 존재.

- withVariant 메서드로 원하는 변형 방법 선택 가능.

- 자바8에서는 HijrahDate의 표준 변형 방법으로 Umm Al-Qura를 제공.


4 요약

- 자바8 이전 버전에서 제공하는 기존의 java.util.Date 클래스와 관련 클래스에서는 여러 불일치점들과 가변성, 어설픈 오프셋, 기본값, 잘못된 이름 결정 등의 설계 결함 존재.

- 새로운 날짜와 시간 API에서 날짜와 시간 객체는 모두 불변.

- 새로운 API는 각각 사람과 기계가 편리하게 날짜와 시간 정보를 관리할 수 있도록 두 가지 표현 방식 제공.

- 날짜와 시간 객체를 절대적인 방법과 상대적인 방법으로 처리 가능, 기존 인스턴스를 변환하지 않도록 처리 결과로 새로운 인스턴스가 생성.

- Temporaldjuster를 이용하면 단순히 값을 바꾸는 것 이상의 복잡한 동작 수행 가능. 자신만의 커스텀 날짜 변환 기능 정의 가능.

- 날짜와 시간 객체를 특정 포맷으로 출력하고 파싱하는 포매터 정의 가능. 패턴을 이용하거나 프로그램으로 포매터를 만들 수 있으며 포매터는 스레드 안정성을 보장.

- 특정 지역/장소에 상대적인 시간대 또는 UTC/GMT 기준의 오프셋을 이용해서 시간대 정의 가능. 이 시간대를 날짜와 시간 객체에 적용해서 지역화 가능.

- ISO-8601 표준 시스템을 준수하지 않는 캘린더 시스템도 사용 가능.

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

null 대신 Optional  (0) 2017.07.12
디폴트 메서드  (0) 2017.07.05
리팩토링, 테스팅, 디버깅  (0) 2017.07.03
병렬 데이터 처리와 성능  (0) 2017.06.29
스트림으로 데이터 수집  (0) 2017.06.29

10. null 대신 Optional

1965년 토이호어가 알골을 설계하면서 처음 null 레퍼런스가 등장.

그는 "구현하기 쉬웠기 때문에 null을 도입했다"라고 당시를 회상.

"컴파일러의 자동 확인 기능으로 모든 레퍼런스를 안정하게 사용할 수 있을 것"을 목표로 정함.

당시 null 레퍼런스 및 예외로 값이 없는 상황을 가장 단순하게 구현 할 수 있다고 판단.

결과적으로 null 및 관련 예외 탄생.

여러 해가 지난 후 호어는 당시 null 및 예외를 만든 결정을 가리켜 "억만 달러짜리 실수"라고 표현.


자바를 포험해 최근 수십 년간 탄생한 대부분의 언어 설계에는 null 레퍼런스 개념을 포함.

예전 언어와 호환성을 유지하려는 목적도 있겠지만 호어의 말 처럼 "구현하기 쉬웠기 때문에" null 레퍼런스 개념을 포함했을 것.


1. 값이 없는 상황 처리?

1.1 보수적인 자세로 NullPointerException 줄이기

- NullPointerException을 피하려면?

> 대부분의 프로그래머는 필요한 곳에 다양한 null 확인 코드를 추가해 null 예외문제를 해결.

> 모든 변수가 null인지 의심할 수 있으므로 변수를 접근할 때 마다 if가 추가되면서 코드 양 증가 우려.

* 이와 같은 반복 패턴 코드를 "깊은 의심"이라 부름.


1.2 null 때문에 발생하는 문제

- 에러의 근원

> NullPointerException은 자바에서 가장 흔히 발생하는 에러.

- 코드를 어지럽힘.

> 때로는 중첩된 if 확인 코드를 추가헤야 하므로 null 때문에 코드 가독성 저하.

- 아무 의미가 없음.

> null은 아무 의미도 포함하지 않음. 특히 정적 형식 언어에서 값이 없음을 표현하는 방법으로는 부적절.

- 자바 철학에 위배.

> 자바는 개발자로부터 모든 포인터를 숨김. 하지만 예외가 있으며 그 것이 바로 null 포인터임.

- 형식 시스템에 구멍을 만듦.

> null은 무형식이며 정보를 포함하고 있지 않으므로 모든 레퍼런스 형식에 null을 할당할 수 있음.

> 이런 식으로 null이 할당되기 시작하면서 시스템의 다른 부분에 null이 퍼졌을 때 애초에 null이 어떤 의미로 사용되었는지 알 수 없음.


1.3 다른 언어는 null 대신 무엇을 사용?

- 최근 그루비 같은 언어에서는 안전 내비게이션 연산자(?.)를 도입해 null 문제 해결.

- 하스켈, 스칼라 등의 함수형 언어는 아예 다른 관점에서 null 문제에 접근.

- 하스켈은 선택형 값을 저장할 수 있는 MayBe라는 형식 제공.

> MayBe는 주어진 형식의 값을 갖거나 아니면 아무 값도 갖지 않을 수 있음.

* 따라서 null 레퍼런스 개념이 자연스럽게 사라짐.

- 스칼라도 T 형식의 값을 갖거나 아무 값도 갖지 않을 수 있는 Option[T]라는 구조 제공.

> Option형식에서 제공하는 연산을 사용해서 값이 있는지 여부를 명시적으로 확인.

* 즉, null 확인

> 형식 시스템에서 이를 강제하므로 null과 관련한 문제가 일어날 가능성이 줄어듦.

- 자바8은 "선택형값" 개념의 영향을 받아 java.util.Optional<T>라는 새로운 클래스 제공.


2. Optional 클래스 소개

- 자바8은 하스켈과 스칼라의 영향을 받아 java.util.Optional<T>라는 새로운 클래스 제공.

- Optional은 선택형값을 캡슐화 하는 클래스.

- 값이 있으면 Optional 클래스는 값을 감싸며 값이 없으면 Optional.empty 메서드로 Optional을 반환.

> Optional.empty는 Optional의 특별한 싱글턴 인스턴스 반환 정적 팩토리 메서드.

- null 레퍼런스와 Optional.empty()의 차이점

> 의미상 비슷하지만 차이점 발생

> null을 참조하려면 NullPointerException이 발생. Optional.empty()는 Optional객체이므로 다양한 방식으로 활용 가능.

- Optional 클래스를 사용하면서 모델의 의미가 더 명확해짐.

- Optional을 이용하면 값이 없는 상황이 우리 데이터에 문제가 있는 것인지 아니면 알고리즘의 버그인지 명확하게 구분 가능.

- 모든 null 레퍼런스를 Optional로 대치하는 것은 바람직하지 않음.

- Optional은 더 이해하기 쉬운 API를 설계하도록 돕는 것.


3. Optional 적용 패턴

3.1 Optional 객체 생성.

빈 Optional

- 정적 팩토리 메서드 Optional.empty()로 빈 Optional 객체 생성 가능.


null이 아닌 값으로 Optional 만들기

- 정적 팩토리 메서드 Optional.of로 null이 아닌 값을 포함하는 Optional 생성 가능.

> 이 경우 NullPointerException이 발생할 수 있음.


null값으로 Optional 만들기

- 정적 팩토리 메서드 Optional.ofNullable로 null값을 저장할 수 있는 Optional 생성 가능.


- get 메서드를 이용해 Optional 값 획득 가능.

> Optional이 비어있으면 get을 호출했을 때 예외 발생.

* 즉, Optional을 잘못 하용하면 결국 null을 사용했을 때와 같은 문제 발생.

3.2 맵으로 Optional의 값을 추출하고 변환.

- Optional은 map 메서드 지원.

- Optional의 map 메서드는 스트림의 map 메서드와 개념적으로 비슷.

- 스트림의 map은 스트림의 각 요소에 제공된 함수를 적용하는 연산.

> 여기서 Optional객체를 최대 요소의 개수가 한 개 이하인 데이터 컬렉션으로 생각 가능.

- Optional이 값을 포함하면 map의 인수로 제공된 함수가 값을 바꿈.

> Optional이 비어있으면 아무 일도 일어나지 않음.


3.3 flatMap으로 Optional 객체 연결

- faltmap 메서드를 통해 이차원 Optional이 일차원 Optional로 변환.

도메인 모델에 Optional을 사용했을 때 데이터를 직렬화할 수 없는 이유

- Optional로 도메인 모델에서 값이 꼭 있어야 하는지 아니면 값이 없을 수 있는지 여부를 구체적으로 표현 가능.

- Optional 클래스의 설계자는 이와는 다른 용도로만 Optional 클래스를 사용할 것을 가정.

- 브라이언 고츠(자바 언어 아키텍트)는 Optional의 용도가 선택형 반환값을 지원하는 것이라고 명확하게 명시.

- Optional 클래스는 필드 형식으로 사용할 것을 가정하지 않았으므로 Serializable 인터페이스를 구현하지 않음.

> 따라서 도메인 모델에서 Optional을 사용한다면 직렬화 모델을 사용하는 도구나 프레임워크에 문제 발생 가능.

- 이런 문제에도 불구하고 여전히 Optional을 사용해서 도메인 모델을 구성하는 것이 바람직하다고 판단됨.

> 특히, 객체 그래프에서 일부 또는 전체가 null일 수 있는 상황이라면 더욱 그렇다고 판단됨.

- 직렬화 모델이 필요할 경우 Optional로 값을 반환받을 수 있는 메서드를 추가하는 방식 권장.


3.4 디폴트 액션과 Optional 언랩

- Optional이 비어있을 때 디폴트값을 제공할 수 있는 orElse 메서드로 값 획득 가능.

- Optional 클래스는 Optional 인스턴스에서 읽을 수 있는 다양한 인스턴스 메서드 제공.

> get()은 값을 읽는 가장 단순한 메서드면서 동시에 가장 안전하지 않은 메서드.

* 래핑된 값이 있으면 해당 값을 반환, 없으면 NoSuchElementException 발생.

* Optional 값이 반드시 있다고 가정할 수 있는 상황이 아니면 get 메서드 사용은 바람직 하지 않음.

> orElse(T other) 메서드를 이용하면 Optional이 값을 포함하지 않을 때 디폴트값 제공 가능.

> orElseGet(Supplier<? extends T)는 orElse 메서드에 대응하는 게으른 버전의 메서드.

* Optional에 값이 없을 때만 Supplier가 실행되기 떄문.

* 디폴트 메서드를 만드는 데 시간이 걸리거나 Optional이 비어있을 때만 디폴트값을 생성하고 싶다면 orElseGet(Supplier<? extends T) 사용.

> orElseThrow(Supplier<? extends X> exceptionSupplier)는 Optional이 비어있을 때 예외를 발생시킨다는 점에서 get과 비슷.

* 하지만 이 메서드는 발생시킬 예외의 종류 선택 가능.

> ifPresent(Consumer<? super T> consumer)를 이용하면 값이 존재할 때 인수로 넘겨준 동작을 실행 가능. 값이 없으면 아무일도 일어나지 않음.


3.5 필터로 특정값 거르기

- Optional 객체가 값을 가지며 프레디케이드와 일치하면 filter 메서드는 그 값을 반환하고 그렇지 않으면 빈 Optional 객체를 반환.

- Optional은 최대 한 개의 요소를 포함할 수 있는 스트림과 같다고 설명. 이 사실을 적용하면 filter연산의 결과를 쉽게 이해 가능.

- Optional이 비어있다면 filter연산은 아무동작도 하지 않음.


표 10-1 Optional 클래스의 메서드

메서드

설명

empty

Optional 인스턴스 반환.

filter

값이 존재하며 프레디케이트와 일치하면 값을 포함하는 Optional 반환하고, 값이 없거나 프레디케이트와 일치하지 않으면 Optional 반환.

flatMap

값이 존재하면 인수로 제공된 함수를 적용한 결과 Optional 반환하고, 값이 없으면 Optional 반환.

get

값이 존재하면 Optional 감싸고 있는 값을 반환하고, 값이 없으며 NoSuchElementException 발생.

ifPresent

값이 존재하면 지정된 Consumer 실행하고, 값이 없으면 아무 일도 일어나지 않음.

isPresent

값이 존재하면 true 반환하고, 값이 없으면 false 반환.

map

값이 존재하면 제공된 매핑 함수를 적용.

of

값이 존재하면 값을 감싸는 Optional 반환하고, 값이 null 이면 NullPointException 발생.

ofNullable

값이 존재하면 값을 감싸는 Optional 반환하고, 값이 null 이면 Optional 반환.

ofElse

값이 존재하면 값을 반환하고, 값이 없으면 디폴트값을 반환.

ofElseGet

값이 존재하면 값을 반환하고, 값이 없으면 Supplier에서 제공하는 값을 반환.

ofElseThrow

값이 존재하면 값을 반환하고, 값이 없으면 Supplier에서 생성한 예외를 발생.


4. Optional을 사용한 실용 예제

- 자바8에서 새롭게 제공하는 Optional 클래스를 효과적으로 사용하려면 값이 없는 상황을 처리하던 기존의 알고리즘과는 다른 관점에서 접근해야 함.

> 즉, 코드 구현만 바꾸는 것이 아니라 네이티브 자바 API와 상호작용하는 방식도 바꿔야 함.


4.1 잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기

- null을 반환하는 것보다는 Optional을 반환하는 것이 더 바람직.

- Map의 get 메서드 시그니처는 수정불가, 단 get 메서드의 반환값은 Optional로 감쌀 수 있음.

- if-then-else를 추가하거나 Optional.ofNullable을 이용하는 두가지 방법 존재.


4.2 예외와 Optional

- 자바 API는 어떤 이유에서 값을 제공할 수 없을 때 null을 반환하는 대신 예외를 발생시킬 때도 존재.

- 전형적인 예로 문자열을 정수로 변환하는 Integer.parseIng(String) 정적 메서드임.

> 문자열을 정수로 바꾸지 못할 때 NumberFormatException 발생.

> 즉, 문자열이 숫자가 아니라는 사실을 예외로 알리는 것.

- 정수를 변환할 수 없느 문자열 문제를 빈 Optional로 해결 가능.

> 즉, parseInt가 Optional을 반환하도록 모델링 가능.

기본형 Optional과 이를 사용하지 말아야 하는 이유

- 스트림처럼 Optional도 기본형으로 특화된 OptionalInt, OptionalLong, OptionalDouble등의 클래스 제공.

- Optional의 최대 요소 수는 한 개이므로 Optional에서 기본형 특화 클래스로 성능 개선 불가.

- 기본형 특화 Optional은 map, flatMap, filter등의 메서드를 지원하지 않으므로 사용할 것을 권장하지 않음.


5. 요약

- 역사적으로 프로그래밍 언어에서는 null 레퍼런스로 값이 없는 상황을 표현해옴.

- 자바8에서는 값이 있거나 없음을 표현할 수 있는 클래스 java.util.Optional<T>를 제공.

- 팩토러 메서드 Optional.empty, Optional.of, Optional.ofNullable 등을 이용하여 Optional 객체 생성 가능.

- Optional 클래스는 스트림과 비슷한 연산 수행. map, flatMap, filter등의 메서드 제공.

- Optional로 값이 없는 상황을 적절하게 처리하도록 강제 가능.

> 즉, Optional로 계상치 못한 null 예외 방지 가능.

- Optional을 활용하면 더 좋은 API 설계가 가능.

> 즉, 사용자는 메서드의 시그니처만 보고도 Optional값이 사용되거나 반환되는지 예측 가능.

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

새로운 날짜와 시간  (0) 2017.08.07
디폴트 메서드  (0) 2017.07.05
리팩토링, 테스팅, 디버깅  (0) 2017.07.03
병렬 데이터 처리와 성능  (0) 2017.06.29
스트림으로 데이터 수집  (0) 2017.06.29

9. 디폴트 메서드


전통적인 자바에서 인터페이스와 관련 메서드는 한 몸처럼 구성.

인터페이스를 구현하는 클래스는 인터페이스에서 정의하는 모든 메서드 구현을 제공하거나 아니면 슈퍼클래스의 구현을 상속 받아야 함.

라이브러리 설계자 입장에서 인터페이스에 새로운 메서드를 추가하는 등 인터페이스를 변경하고 싶을 때는 문제가 발생함.

> 인터페이스를 바꾸면 이전에 해당 인터페이스를 구현했던 모든 클래스의 구현도 고쳐야 하기 때문.

자바8에서는 기본 구현을 포함하는 인터페이스를 정의하는 두 가지 방법을 제공.

1) 인터페이스 내부에 정적 메서드 사용

2) 인터페이스의 기본 구현을 제공할 수 있도록 디폴트 메서드 기능 사용.

> 즉, 자바8은 메서드 구현을 포함하는 인터페이스 정의 가능.

기존 인터페이스를 구현하는 클래스는 자동으로 인터페이스에 추가된 새로운 메서드의 디폴트 메서드를 상속.

default 키워는 해당 메서드가 디폴트 메서드임을 가리킴.

디폴트 메서드를 사용 하는 이유는?

- 디폴트 메서드를 이용하면 자바 API의 호환성을 유지하면서 라이브러리 수정 가능.

디폴트 메서드를 이용하면 인터페이스의 기본 구현을 그대로 상속하므로 인터페이스에 자유롭게 새로운 메서드 추가 가능.

다중 상속 동작이라는 유연성을 제공하면서 프로그램 구성에 도움을 줌.


정적 메서드와 인터페이스

보통 자바에서는 인터페이스 그리고 인터페이스의 인스턴스를 활용할 수 있는 다양한 정적 메서드를 정의하는 유틸리티 클래스를 사용.

> ex) Collections는 Collection 객체를 활용 할 수 있는 유틸리티 클래스.

자바8에서는 인터페이스에 직접 정적 메서드를 선언할 수 있으므로 유틸리티 클래스를 없애고 직접 인터페이스 내부에 정적 메서드 구현 가능.

> 그럼에도 불구하고 과거 버전과의 호환성을 유지할 수 있도록 자바 API에는 유틸리티 클래스가 남아있음.


1. 변화하는 API

- 공개된 API를 수정하면 기존 버전과의 호환성 문제 발생.

> 이러한 이유로 공식 자바 컬렉션 API 같은 기존의 API는 수정하기 어려움.

> API를 바꿀 수 있는 몇 사지 대안이 존재하지만 완벽한 해결책은 될 수 없음.

> ex) 자신만의 API를 별도로 만든 다음에 예전 버전과 새로운 버전을 직접 관리하는 방법등...

- 디폴트 메서드로 이 모든 문제들 해결 가능.

> 디폴트 메서드를 이용해 API 수정 시 새롭게 바뀐 인터페이스에서 자동으로 기본 구현을 제공하므로 기존 코드를 수정하지 않아도 됨.


바이너리 호환성, 소스 호환성, 동작 호환성

자바 프로그램을 바꾸는 것과 관련된 호환성 문제는 크게 바이너리 호환성, 소스 호환성, 동작 호환성 3가지로 분류 가능.

인터페이스에 메서드를 추가했을 때는 바이너리 호환성을 유지하지만 인터페이스를 구현하는 클래스를 재컴파일하면 에러가 발생.

즉, 다양한 호환성이 있다는 사실을 이해해야 함.


바이너리 호환성: 뭔가를 바꾼 이후에도 에러가 없이 기존 바이너리가 실행될 수 있는 상황.

> 바이너리 실행에는 인증, 준비, 해석 등의 과정이 포함.

> 예로 인터페이스에 메서드를 추가했을 때 추가된 메서드를 호출하지 않는 한 문제가 발생하지 않는데 이를 바이너리 호환성이라 함.


소스 호환성: 코드를 고쳐도 기존 프로그램을 성공적으로 재컴파일할 수 있음을 의미.

> 예로 인터페이스에 메서드를 추가하면 소스 호환성이 아님.

> 추가한 메서드를 구현하도록 클래스를 고쳐야 하기 때문.


동작 호환성: 코드를 바꾼 다음에도 같은 입력값이 주어지면 프로그램이 같은 동작을 실행한다는 의미.

> 예로 인터페이스에 메서드를 추가하더라도 프로그램에서 추가된 메서드를 호출할 일이 없으므로 동작 호환성 유지.

> 혹 우현히 구현 클래스가 이를 오버라이드 한 경우도 존재.


2. 디폴드 메서드란 무엇인가?

- 인터페이스는 자신을 구현하는 클래스에서 메서드를 구현하지 않을 수 있는 새로운 메서드 시그니처를 제공.

- 인터페이스를 구현하는 클래스에서 구현하지 않은 메서드는 인터페이스 자체에서 기본 제공.

- default라는 키워드로 시작하며 다른 클래스에 선언된 메서드처럼 메서드 바디를 포함.

- 인터페이스에 디폴트 메서드를 추가하면 소스 호환성이 유지.

- 함수형 인터페이스는 오직 하나의 추상 메서드를 포함. 디폴트 메서드는 추상 메서드에 해당하지 않음.


추상 클래스와 자바 8의 인터페이스

추상 클래스와 인터페이스의 차이점

1) 클래스는 하나의 추상 클래스만 상속 받을 수 있지만 인터페이스를 여러 개 구현 가능.

2) 추상 클래스는 인스턴스 변수(필드)로 공통 상태를 가질 수 있음. 하지만 인터페이스는 인스턴스 변수를 가질 수 없음.


3. 디폴트 메서드 활용 패턴

- 우리가 만드는 인터페이스에도 디폴트 메서드 추가 가능.

> 디폴트 메서드 이용하는 두 가지 방식

1) 선택형 메서드

2) 동작 다중 상속


3.1 선택형 메서드

- 디폴트 메서드를 이용하면 기본 구현을 제공할 수 있으므로 인터페이스를 구현하는 클래스에서 빈 구현을 제공할 필요가 없음.


3.2 동작 다중 상속

- 자바에서 클래스는 한 개의 다른 클래스만 상속할 수 있지만 인터페이스는 여러 개 구현할 수 있음.


다중 상속 형식

- ArrayList는 한 개의 클래스를 상속 받고, 여섯 개의 인터페이스를 구현

> 결과적으로 AbstractList, List, RandomAccess, Cloneable, Serializable, Iterable, Collection의 서브 형식이 됨.

> 따라서 디폴트 메서드를 사용하지 않아도 다중 상속을 활용.

- 자바8에서는 인터페이스가 구현을 포함할 수 있으므로 클래스는 여러 인터페이스에서 동작(구현 코드)를 상속 받을 수 있음.

- 중복되지 않는 최소한의 인터페이스를 유지한다면 우리 코드에서 동작을 쉽게 재사용하고 조합 가능.


인터페이스 조합

- 인터페이스에 디폴트 구현을 포함시키면 또 다른 장점 발생.

> 디폴트 메서드 덕분에 인터페이스를 직접 고칠 수 있고 구현하는 모든 클래스도 자동으로 변경한 코드를 상속 받음.

* 구현 클래스에서 메서드를 정의하지 않은 상황에 한해서.


옳지 못한 상속

상속으로 코드 재사용 문제를 모드 해결할 수 있는 것은 아님.

> 한 개의 메서드를 재사용하려고 100개의 메서드와 필드가 정의되어 있는 클래스를 상속 받는 것은 좋지 않은 생각.

> 이 경우 델리게이션(delegation), 즉 멤버 변수를 이용해서 클래스에서 필요한 메서드를 직접 호출하는 메서드를 작성하는 것이 좋음.

> 종종 final로 선언된 클래스 확인 가능.

* 다른 클래스가 이 클래스를 상속받지 못하게 함으로써 원래 동작이 바뀌지 않길 원하기 때문.

우리의 디폴트 메서드에도 이 규칙 적용 가능.

필요한 기능만 포함하도록 인터페이스를 최소한으로 유지한다면 필요한 기능만 선택할 수 있으므로 쉽게 기능 조립 가능.


4. 해석 규칙

- 자바8에는 디폴트 메서드가 추가되었으므로 같은 시그니처를 갖는 디폴트 메서드를 상속받는 상황 발생 가능.


4.1 알아야 할 세 가지 해결 규칙

1) 클래스가 항상 이김. 클래스나 슈퍼클래스에서 정의한 메서드가 디플트 메서드보다 우선권을 갖음.

2) 1번 규칙 이외의 상황에서 서브인터페이스가 이김. 상속관계를 갖는 인터페이스에서 같은 시그니처를 갖는 메서드를 정의할 때는 서브 인터페이스가 이김.

> 즉, B가 A를 상속 받는다면 B가 A를 이김.

3) 여전히 디폴트 메서드의 우선순위가 결정되지 않았다면 여러 인터페이스를 상속받는 클래스가 명시적으로 디폴트 메서드를 오버라이드하고 호출해야 함.


4.2 디폴트 메서드를 제공하는 서브인터페이스가 이긴다.

4.3 충돌 그리고 명시적인 문제 해결

충돌 해결

- 클래스와 메서드 관계로 디폴트 메서드를 선택할 수 업슨 상황에서 선택할 수 있는 방법이 없음.

- 개발자가 직접 클래스에서 사용하려는 메서드를 명시적으로 선택해야 함.

> 자바8에서는 X.super.m(...) 형태의 새로운 문법을 제공.

* 여기서 X는 호출하려는 메서드 m의 슈퍼인터페이스.


5. 요약

- 자바8의 인터페이스는 구현 코드를 포함하는 디폴트 메서드, 정적 메서드 정의 가능.

- 디폴트 메서드의 정의는 default 키워드로 시작하며 일반 클래스 메서드처럼 바디를 갖음.

- 공개된 인터페이스에 추상 메서드를 추가하면 소스 호환성이 깨짐.

- 디폴트 메서드 덕분에 라이브러리 설계자가 API를 바꿔도 기존 버전과 호환성 유지 가능.

- 선택형 메서드와 동작 다중 상속에도 디폴트 메서드 사용 가능.

- 클래스가 같은 시그니처를 갖는 여러 디폴트 메서드를 상속하면서 생기는 충동 문제를 해결 하는 규칙 존재.

- 클래스나 슈퍼클래스에 정의된 메서드가 다른 디폴트 메서드 정의보다 우선.

> 이 외의 상황에서는 서브인터페이스에서 제공하는 디폴트 메서드가 선택됨.

- 두 메서드의 시그니처가 같고, 상속관계로도 충돌 문제를 해결 할 수 없을 때 디폴트 메서드를 사용하는 클래스에서 메서드를 오버라이드해서 어떤 디폴트 메서드를 호출할지 명시적으로 지정해야 함.

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

새로운 날짜와 시간  (0) 2017.08.07
null 대신 Optional  (0) 2017.07.12
리팩토링, 테스팅, 디버깅  (0) 2017.07.03
병렬 데이터 처리와 성능  (0) 2017.06.29
스트림으로 데이터 수집  (0) 2017.06.29

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

스트림 활용 에제 Model

- Transaction


- Trader

- 예제 소스

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

병렬 데이터 처리와 성능  (0) 2017.06.29
스트림으로 데이터 수집  (0) 2017.06.29
스트림 활용  (0) 2017.06.26
스트림 소개  (0) 2017.06.16
람다 표현식  (0) 2017.06.15

5. 스트림 활용

데이터를 어떻게 처리할지는 스트림 API가 관리.

편리하게 데이터 관련 작업 가능.

스트림 API 내부적으로 다양한 최적화가 이루어질 수 있음.

스트림 API는 내부 반복 뿐 아니라 코드를 병렬로 실행할지 여부도 결정 가능.

스트림 API가 지원하는 연산을 이용하여 필터링, 슬라이싱, 매핑, 검색, 매칭, 리듀싱 등 다양한 데이터 처리 질의 표현 가능.


1. 필터링과 슬라이싱

1.1 프레디케이트로 필터링

- filter 메서드는 프레디케이드(boolean을 반환하는 함수)를 인수로 받아 프레디케이트와 일치하는 모든 요소를 포함하는 스트림 반환.


1.2 고유 요소 필터링

- 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드 지원.

> 고유 여부는 스트림에서 만든 객체의 hashCode, equals로 결정됨.


1.3 스트림 축소

- 주어진 사이즈 이하의 크기를 갖는 새로운 스트림을 반환하는 limit 메서드 지원.

- 스트림이 정렬되어 있으면 최대 n개의 요소 반환 가능.


1.4 요소 건너뛰기

- 처음 n개의 요소를 제외한 스트림을 반환하는 skip 메서드 지원.

- limit과 skip은 상호 보완적 연산을 수행.


2. 매핑

- 특정 객체에서 특정 데이터를 선택하는 작업.

- 스트림 API의 map과 flatMap 메서드는 특정 데이터를 선택하는 기능 제공.


2.1 스트림의 각 요소에 함수 적용

- 스트림은 함수를 인수로 받는 map 메서드 지원.

- 인수로 제공된 함수는 각 요소에 적용. 함수를 적용한 결과가 새로운 요소로 매핑.

> 이 과정은 기존의 값을 "고친다 modify"라는 개념 보다 "새로운 버전 생성"이라는 개념에 가까움.

> "변환 transforming"에 가까운 "매핑 mapping"이라는 단어 사용.


2.2 스트림 평면화

- 스트림의 각 값을 다른 스트림으로 만든 다음 모든 스트림을 하나의 스트림으로 연결하는 메서드를 flatMap 메서드라 함.

- flatMap은 객 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑함.

> 즉, map과 달리 faltMap은 하나의 평면화된 스트림을 반환.


3. 검색과 매칭

- 특정 속성이 데이터 집합에 있는지 여부를 검색

- allMatch, anyMatch, noneMatch, findFirst, findAny등 다양한 유틸리티 메서드 제공.


3.1 프레디케이트가 적어도 한 요소와 일치하는지 확인

- 프레디케이트가 주어진 스트림에 적어도 한 요소와 일치하는지 확인할 때 anyMatch 메서드 이용.

- anyMatch는 boolean 반환하는 최종 연산.


3.2 프레디케이트가 모든 요소와 일치하는지 확인

- 프레디케이트가 주어진 스트림의 모든 요소와 일치하는지 확인 할 때 allMatch 메서드 이용.

- 프레디케이드가 주어진 스트림의 일치하는 요소가 없는 확인 할 때 noneMatch 메서드 이용. (allMatch 와 반대 연산)

- anyMatch, allMatch, noneMatch 세 가지 메서드는 스트림 쇼트서킷 기법, 즉 자바의 &&, ||와 같은 연산.


쇼트서킷이란?

- 여러 and 연산으로 연결된 커다란 boolean 표현식을 평가한다고 가정할 때, 표현식에서 하나라도 거짓이라는 결과가 나오면 나머지 표현식의 결과와 상관없이 전체 결과도 거짓이 됨.

> 이러한 상황을 쇼트서킷이라 부름.


3.3 요소 검색

- 현재 스트림에서 임의 요소 반환하는 매서드는 findAny.

- findAny 메서드를 다른 스트렘 연산과 연결해서 사용 가능.


Optional이란?

- 값의 존재나 부재 여부를 표현하는 컨테이너 클래스.

- fnidAny는 아무 요소도 반환하지 않을 수 있음.

- null은 쉽게 에러를 발생 시킬 수 있으므로 자바 8 라이브러리 설계자는 Optional<T> 기능 구현.


1) isPresent() - Optional이 값을 포함하는 true 반환. 포함하지 않으면 false 반환.

2) ifPresnet(Consumer<T> block) - 값이 있으면 주어진 블록 실행. T 형식의 인수를 받으며 void를 반환하는 람다 전달 가능.

3) T get() - 값이 존재하면 값 반환. 값이 없으면 NoSuchElementException 발생.

4) T orElse(T Other) - 값이 있으면 값 반환. 값이 없으면 기본값 반환.


3.4 첫번째 요소 찾기

- 리스트 또는 정렬된 연속 데이터로부터 생성된 스트림처럼 일부 스트림에는 논리적 아이템 순서가 정해져있을 수 있음.

- 이런 스트림의 첫 번재 요소를 찾는 findFirst 메서드 제공.


4. 리듀싱

- 모든 스트림 요소를 처리해 값으로 도출하는 질의를 리듀싱 연산이라 함.

- 함수형 프로그래밍 언어 용어로는 이 과정이 마치 종이를 작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드(fold)라 부름.


표 5-1 작성.


5. 실전 예제

- 추가 포스팅에서 예제 제공.


6. 숫자형 스트림

- 숫자 스트림을 효율적으로 처리할 수 있도록 스트림 API는 기본형 특화 스트림 제공.


6.1 기본형 특화 스트림

- 박싱 비용을 피할 수 있도록 'int', 'double', 'long' 요소에 특화된 스트림 제공

> IntStream, DoubleStream, LongStream

- 각각의 인터페이스는 숫자 스트림의 합계를 계산할 수 있는 sum, 최댓값 요소를 검색하는 max등 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드 제공.

- 필요할 때 다시 객체 스트림으로 복원하는 기능도 제공.

- 특화 스트림은 오직 박싱과정에서 일어나는 효율성과 관련 있으며 스트림에 추가 기능을 제공하지 않음.

- boxed()를 통해 특화 스트림을 일반 스트림으로 변환 가능.

- Optional -> OptionalInt, OptionalDouble, OpionalLong 특화 스트림 버전 제공.


6.2 숫자 범위

- IntStream, LongStream에 range와 rangeClosed 두 가지 정적 메서드 제공.


7. 스트림 만들기

- stream 메서드로 컬렉션에서 스트림 획득 가능.


7.1 값으로 스트림 만들기

- 임의의 수를 인수로 받는 정적 메서드 Stream.of를 이용해 스트림 생성 가능.


7.2 배열로 스트림 만들기

- 배열을 인수로 받는 정적 메서드 Arrays.stream을 이용해 스트림 생성 가능.


7.3 파일로 스트림 만들기

- java.nio.file.Files의 많은 정적 메서드가 스트림을 반환.


7.4 함수로 무한 스트림 만들기

- 함수에서 스트림을 만들 수 있는 두 개의 정적 메서드 Stream.iterate와 Stream.generate 제공.

- iterate와 generate에서 만든 스트림은 요청할 때 마다 주어진 함수를 이용해 값을 생성.

> 따라서 무제한으로 값 계산 가능.

- iterate

> 초깃값과 람다를 신수로 받아 새로운 값을 끊임없이 생산 가능.

> 요청할 떄 마다 값을 생산 할 수 있으며 무한 스트림 생성 가능.

* 이러한 스트림을 언바운드 스트림이라 표현.

> 일반적으로 연속된 일련의 값을 만들때 iterate 사용.

- generate

> 요구할 떄 값을 계산하는 무한 스트림 생성 가능.

> iterate와 달리 생산된 각 값을 연속적으로 계산하지 않음.

> Supplier<T>를 인수로 받아 새로운 값을 생산.


8. 요약

- 복잡한 데이터 처리 질의 표현 가능.

- filter, distinct, skip, limit 메서드로 스트림을 필터링 하거나 자르기 가능.

- map, flatMap 메서드로 스트림 요소를 추출하거나 변환 가능.

- findFirst, findAny 메서드로 스트림의 요소 검색 가능.

- allMatch, noneMatcj, anyMatch 메서드를 이용해서 주어진 프레디케이트와 일치하는 요소를 스트림에서 검색 가능.

> 이들 메서드를 쇼트서킷, 즉 결과를 찾는 즉시 반환, 전체 스트림을 처리하지 않음.

- reduce 메서드로 스트림의 모든 요소를 반복 조합하며 값 도출 가능.

- filter, map등은 상태를 지정하지 않는 상태없는 연산.

- reduce 같은 연산은 계산하는데 필요한 상태 저장.

- sorted, distinct 등의 메서드는 새로운 스트림을 반환하기에 앞서 스트림의 모든 요소를 버퍼에 저장.

> 이런 메서드를 상태 있는 연산이라 부름.

- IntStream, DoubleStram, LongStream은 기본형 특화 스트림.

> 이들 연산은 기본형에 맞게 특화.

- 컬렉션뿐 아니라 값, 배열, 파일, iterate, generate 같은 메서드로도 스트림 생성 가능.

- 크기가 정해지지 않은 스트림을 무한 스트림이라 함.

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

스트림으로 데이터 수집  (0) 2017.06.29
스트림 활용 예제 소스  (0) 2017.06.26
스트림 소개  (0) 2017.06.16
람다 표현식  (0) 2017.06.15
동작 파라미터화 예제 소스  (0) 2017.06.13

4. Chapter4 - 스트림 소개


컬렉션은 자바에서 가장 많이 사용하는 기능 중 하나.

거의 모든 자바 어플리케이션은 컬렉션을 만들고 처리하는 과정을 포함.

컬렉션으로 데이터를 그룹화하고 처리 가능.

대부분의 자바 어플리케이션에서 컬렉션을 많이 사용하지만 완벽한 컬렉션 관련 연산을 지원 하기에는 부족.


1. 스트림이란 무엇인가?

- 선언형으로 컬렉션 데이터 처리가 가능한 기능.

> 선언형: 데이터를 처리하는 임시 구현 코드 대신 질의로 표현.

- 간단하게는 데이터 컬렉션 반복을 처리하는 기능.

- 스트림을 이용하면 멀티 스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬처리 가능.

- 스트림의 소프트웨어공학적으로 아래와 같은 다양한 이득을 제공.

> 선언형으로 코드 구현 가능.

* 루프와 if 조건문 등의 제어 블록을 사용하지 않고도 같은 동작의 수행 지정 가능.

* 선언형 코드와 동작 파라미터화를 활용하면 변화하는 요구사항에 쉽게 대응.

> filter, sorted, map, collect같은 여러 빌딩 블록 연산을 연결해서 복잡한 데이터 처리 파이프라인 생성 가능.

> 여러개의 파이프라인으로 연결되도 가독성과 명확성이 유지.

- 스트림은 filter(또는 sorted, map, collect) 같은 연산은 고수준 빌딩 블록으로 이루어져 있음.

- 특정 스레딩 모델에 제한되지 않고 자유롭게 어떤 상황에서든 사용 가능.

- 결과적으로 데이터 처리 과정을 병렬화하면서 스레드와 락을 걱정할 필요 없음.

- 스트림 API의 특징

> 선언형

* 더 간결하고 가독성 향상.

> 조립 가능

* 유연성 향상.

> 병렬화

* 성능 향상.


2. 스트림 시작하기

- 컬렉션은 스트림 작업 중 가장 간단한 작업 중 하나.

- 자바 8의 컬렉션에서는 스트림을 반환하는 stream 메서드 추가.

> 스트림 인터페이스 정의 : java.util.stream.Stream 참고

- 스트림이란?

> "데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소"로 정의 가능.


> 연속된 요소

* 컬렉션과 마찬가지로 스트림은 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스 제공

* 컬렉션은 자료구조로 시간과 공간의 복장성과 관련된 요소 저장 및 접근 연산이 주를 이룸.

* 스트림은 filter, sorted, map처럼 표현 계산식이 주를 이룸.

* 즉, 컬렉션의 주제는 데이터고 스트림의 주제는 계산.


> 소스

* 스트림은 컬렉션, 배열, I/O 자원등의 데이터 제공 소스로부터 데이터를 소비.


> 데이터 처리 연산

* 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 데이터베이스와 비슷한 연산 지원.

* filter, map, reduce, find, match, sort등 데이터 조작 가능.


> 파이프라이닝

* 스트림 연산끼리 연결해 커다란 파이프라인을 구성할 수 있도록 스트림 반환.

* 그로 인해 게으름(laziness), 쇼티서킷(short-circuiting:단선) 같은 최적화도 얻음.

* 연산 파이프라인은 데이터베이스 질의와 비슷.


> 내부 반복

* 반복자를 이용하여 명시적 반복 -> 외부 반복

* 반복을 알아서 처리하고 결과 스트림 값을 어딘가에 저장 -> 내부 반복

- 예제에 사용될 데이터

- 예제

> filter

* 람다를 인수로 받아 스트림에서 특정 요소를 제외.


> map

* 람다를 이용해 한 요소를 변환하거나 정보를 추출.


> limit

* 정해진 개수 이상의 요소가 스트림에 저장되지 못하게 스트림의 크기 축소.


> collect

* 스트림을 다른 형식으로 변환.

* 다양한 변환 방법을 인수로 받아 스트림에 누적된 요소를 특정 결과로 변환.


3. 스트림과 컬렉션

- 컬렉션과 스트림 모두 연속된 요소 형식의 값을 저장하는 자료구조 인터페이스를 제공.

- 연속된(Sequenced)이라는 표현은 순서와 상관없이 아무 값이나 접속 하는 것이 아니라 순차적으로 접근한다는 의미.


- 컬렉션과 스트림의 차이

> ex) 영화 DVD - 컬렉션, 인터넷 스트리밍 - 스트림

> 데이터를 "언제" 계산하느냐가 가장 큰 차이점.

> 컬렉션은 현재 자료구조가 포함하는 "모든"값을 메모리에 저장하는 구조

* 즉, 모든 요소는 컬렉션에 추가하기 전에 계산되어야 함.

> 스트림은 이론적으로 "요청할 때만 요소를 계산"하는 고정된 자료구조.

* 사용자가 요청한 값만 스트림에서 추출한다는 것이 핵심.

> 스트림은 생산자와 소비자 관계를 형성.

> 스트림은 게으르게 만들어지는 컬렉션과 같음.

* 즉, 사용자가 데이터를 요청할 때만 값을 계산.

> 반면, 컬렉션은 적극적으로 생성.

* 무제한의 소수를 추출할 경우 컬렉션은 모든 소수를 포함하려 할 것이므로 무한루프를 돌면서 새로운 소수를 계산 및 추가하기를 반복.

* 결국 영원히 결과를 볼 수 없게됨.

> 스트림의 한 예로 구글 검색이 있음.


3.1 딱 한번만 검색 가능.

- 반복자와 마찬가지로 스트림도 한 번만 탐색 가능.

> 즉, 탐색된 스트림의 요소는 소비.

- 반복자와 마찬가지로 한 번 탐색한 요소를 다시 탐색하기 위해서는 초기 데이터 소스에서 새로운 스트림을 얻어야 함.


3.2 외부 반복과 내부 반복

- 컬렉션 인터페이스를 사용하려면 사용자가 직접 요소를 반복해야 함.

> 이를 외부 반복이라 함.

- 스트림 라이브러리는 내부 반복을 사용함.

> 반복을 알아서 처리하고 결과 스트림값을 어딘가에 저장해줌.

> 함수에 어떤 작업을 수행하지만 지정하면 모든 것이 알아서 처리 됨.

- 내부 반복이 좋은 2가지 이유

> 작업을 투명하게 병렬 처리 가능.

> 더 최적화된 다양한 순서로 처리 가능.

- 자바8에서 스트림을 제공하는 이유

> 스트림 라이브러리의 내부 반복은 데이터 표현과 하드웨어를 활용하는 병렬성 구현을 자동으로 선택.

* 외부 반복에서는 병렬성을 스스로 관리해야함(스스로 관리한다는 것은 병렬성을 포기 or synchronized 처리)


4. 스트림 연산

- java.util.stream.Stream 인터페이스는 많은 연산을 정의.

- 스트림 인터페이스의 연산은 크게 2가지로 구분 가능.

> 연결 가능 스트림 연산 - 중간연산

> 스트림을 닫는 연산 - 최종연산


4.1 중간 연산

- filter나 sorted 같은 중간 연산은 다른 스트림을 반환.

- 여러 중간 연산을 연결해서 질의를 만들수 있음.

- 단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도 수행하지 않음.

> 즉, 게으르다는 것(lazy)

- 중간 연산을 합친 다음에 합쳐진 중간 연산을 최종 연산으로 한번에 처리.

- 스트림의 게으른 특성 덕분에 몇 가지 최적화 효과 획득.

- 중간연산 예제 결과

filtering: pork

mapping: pork

filtering: beef

mapping: beef

filtering: chicken

mapping: chicken

중간연산 테스트: [pork, beef, chicken]


4.2 최종 연산

- 스트림 파이프라인에서 결과 도출.

- 최종 연산에 의해 List, Integer, void등 스트림 이외의 결과 반환.


4.3 스트림 이용하기

- 스트림 이용 과정은 다음과 같이 세 가지로 요약 가능.

> 질의를 수행할 (컬렉션 같은) 데이터 소스

> 스트림 파이프라인을 구성할 중간 연산 연결

> 스트림 파이프라인을 실행하고 결과를 만들 최종 연산.


- 스트림 파이프라인의 개념은 빌더 패턴(builder pattern)과 비슷.

> 빌더 패턴은 호출을 연결해서 설정을 만듦 <-> 스트림에서 중간 연산을 연결

> 준비된 설정에 build 메서드 호출 <-> 스트림에서 최종 연산.


표. 중간연산

연산

형식

반환형식

연산의 인수

함수 디스크립터

filter

중간 연산

Stream<T>

Predicate<T>

T -> boolean

map

중간 연산

Stream<T>

Function<T, R>

T -> R

limit

중간 연산

Stream<T>



sotred

중간 연산

Stream<T>

Comparator<T>

(T, T) -> int

distinct

중간 연산

Stream<T>



표. 최종연산

연산

형식

목적

forEach

최종 연산

스트림의 요소를 소비하면서 람다 적용. void 반환

count

최종 연산

스트림의 요소 개수를 반환. long 반환

collect

최종 연산

스트림을 리듀스해서 리스트, , 정수 형식의 컬렉션 생성.


5. 요약

- 스트림은 소스에서 추출된 연속 요소로, 데이터 처리 연산을 지원.

- 스트림은 내부 반복을 지원.

> 내부 반복은 filter, map, sorted 등의 연산으로 반복을 추상화.

- 중간 연산과 최종 연산이 있음.

> 중간연산: filter와 map처럼 스트림을 반환하면서 다른 연산과 연결될 수 있는 연산.

> 최종연산: forEach나 count처럼 스트림 파이프라인을 처리해서 스트림이 아닌 결과를 반환하는 연산.

- 스트림의 요소는 요청할 때만 계산.



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

스트림 활용 예제 소스  (0) 2017.06.26
스트림 활용  (0) 2017.06.26
람다 표현식  (0) 2017.06.15
동작 파라미터화 예제 소스  (0) 2017.06.13
자바8 동작 파라미터화 코드 전달하기  (0) 2017.06.13

Chapter3 - 람다 표현식


익명 클래스로 다양한 동작을 구현할 수 있지만 만족할 만큼 코드가 깔끔하지 않음.

> 깔끔하지 않은 코드는 동작 파라미터화를 적용하는 것을 막는 요소.

람다 표현식은 익명 클래스처럼 이름이 없는 함수면서 메서드를 인수로 전달할 수 있음.

> 람다에 대해 더 알아보기 전에는 익명 클래스와 비슷한 것이라 인지하면 좋음.

더 간결하고 유연한 코드를 구현하는 방법을 단계적으로 설명.


1. 람다란 무엇인가?

- 메서드로 전달할 수 있는 익명 함수를 단순화 한 것.

- 이름은 없지만, 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외리스트는 가질 수 있음.

> 익명

* 보통의 메서드와 달리 이름이 없으므로 익명이라 표현.

> 함수

* 메서드처럼 특정 클래스에 종속되지 않으므로 함수라 표현.

* 하지만 메서드 처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함.

> 전달

* 메서드 인수로 전달하거나 변수로 저장 가능.

> 간결성

* 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없음.

- 람다(lambda)라는 용어는 람다 미적분학 학계에서 개발한 시스템에서 유래

- 람다 표현식이 중요한 이유는?

> 간결한 방식으로 코드 전달이 가능하기 때문.

- 람다는 세 부분으로 구성됨.

> 파라미터 리스트

* Comparator의 compare 메서드의 파라미터(두 개의 사과) - 예제임.

> 화살표

* 람다의 파라미터 리스트와 바디를 구분.

> 람다의 바디

* 람다의 반환값에 해당하는 표현식.


2. 어디에? 어떻게 람다를 사용할까?

- 함수형 인터페이스라는 문맥에서 람다 표현식 사용 가능.


2.1 함수형 인터페이스

- 정확히 하나의 추상 메서드를 지정하는 인터페이스

> 자바 API의 함수형 인터페이스로 Comparator, Runnalbe등이 있다.

- 인터페이스는 디폴트 메서드를 포함할 수 있음. 많은 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스.

* 디폴트 메서드: 인터페이스의 메서드를 구현하지 않는 클래스를 고려해서 기본 구현을 제공하는 바디를 포함하는 메서드.


2.2 함수 디스크립터

- 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가르킴.

- 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라 부름.

> ex) Runnable 인터페이스의 추상 메서드 run은 인수와 반환값이 없음.(void반환)

* Runnable 인터페이스는 인수와 반환값이 없는 시그니처로 생각할 수 있음.

- () -> void 라는 표기는 파라미터 리스트가 없으며 void를 반환하는 함수를 의미.

- 람다 표현식은 변수에 할당하거나 함수형 인터페이스를 인수로 받는 메서드로 전달 가능.

- 함수형 인터페이스의 추상 메서드와 같은 시그니처를 갖음.


@FunctionalInterface란?

- 함수형 인터페이스를 가리키는 어노테이션

- 실제로 함수형 인터페이스가 아닐 경우 컴파일러에서 에러 발생.

- 한 개 이상의 추상 메서드가 존재할 경우 "Multiple nonoverriding abstract method found in interface xxx" 발생할 수 있음.


2.3 함수형 인터페이스 사용

- 다양한 람다 표현식을 사용하려면 공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합이 필요.

- 아래 자바 8 라이브러리인 Predicate, Consumer, Function 인터페이스를 설명 진행.

2.3.1 Predicate

- java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의.

- test는 제네릭 형식 T의 객체를 인수로 받아 boolean 반환.

- T 형식의 객체를 사용하는 boolean 표현식이 필요한 상황에서 Predicate를 사용할 수 있음.


2.3.2 Consumer

- java.util.function.Consumer<T> 인터페이스는 accept라는 추상 메서드를 정의.

- accept는 제네릭 형식 T의 객체를 인수로 받아 어떤 동작을 수행하고 싶을때 사용.

- Integer 리스트를 인수로 받아서 각 항목에 어떤 동작을 수행하는 forEach 메서드를 정의할 때 Consumer를 활용할 수 있음.


2.3.3 Function

- java.util.function.Function<T, R> 인터페이스는 apply라는 추상 메서드를 정의.

- apply는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환.

- 입력을 출력으로 매핑하는 람다를 정의할때 Function 인터페이스를 활용할 수 있음.


- 기본형 특화

> 자바의 모든 형식은 참조형(Byte, Integer, Object, List..) 아니면 기본형 (int, double, byte, char...)에 해당.

> 제네릭 파라미터에는 참조형만 가능.

> 자바에는 기본형 <-> 참조형 변환 기능을 제공.

* 기본형 -> 참조형: 박싱, 기본형 <- 참조형: 언박싱, 자동으로 이루어지는 방식: 오토박싱

* 형 변환시 비용이 소모.

> 자바8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공.

> 일반적으로 특정 형식을 입력으로 받는 함수형 인터페이스의 이름 앞에는 Double... Int..., LongBinary...처럼 형식명이 붙음.

> ToIntFunction<T>, IntToDoubleFunction등 다양한 출력 형식 인터페이스 제공.

- 자바 8의 대표적 함수형 인터페이스 목록

함수형 인터페이스

함수 디스크립터

기본형 특화

Predicate<T>

T -> boolean

IntPredicate, LongPredicate, DoublePredicate

Consumer<T>

T -> void

IntConsumer, LongConsumer, DoubleConsumer

Function<T, R>

T -> R

IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>,

ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T>

Supplier<T>

() -> T

BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier

UnaryOperator<T>

T -> T

IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator

BinaryOperartor<T>

(T, T) -> T

IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator

BiPredicate<L, R>

(L, R) -> boolean


BiConsumer<T, U>

(T, U) -> void

ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T>

BeFunction<T, U, R>

(T, U) -> R

ToIntBiFunction<T, U>, ToLongBiFunction<T, U>, ToDoubleBiFunction<T, U>


- 람다와 함수형 인터페이스 예제

사용 사례

람다 예제

대응하는 함수형 인터페이스

불린 표현

(List<String> list) -> list.isEmpty()

Predicate<List<String>>

객체 생성

() -> new Apple(10)

Supplier<Apple>

객체에서 소비

(Apple a) -> System.out.println(a,getWeight())

Consumer<Apple>

객체에서 선택/추출

(String s) -> s.length()

Function<String, Integer> 또는 ToIntFunction<String>

조합

(Int a, int b) -> a * b

IntBinaryOperator

객체 비교

(Apple a1, Apple a2) -> a.getWeight().compareTo(a2.getWeight())

Comparator<Apple> 또는 BiFunction<Apple, Apple, Integer> 또는 ToIntBiFunction<Apple, Apple>


2.4 형식 검사, 형식 추론, 제약

- 람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지 정보가 포함되어 있지 않음.

- 람다 표현식을 제대로 이해하려면 람다의 실제 형식 파악이 중요.


2.4.1 형식 검사

- 람다가 사용되는 컨텍스트를 이용해서 람다의 형식 추론 가능.

> 어떤 컨텍스트에서 기대되는 람다 표현식이 형식을 대상 형식이라 부름.

@ 형식 검사 과정

1. filter 메서드의 선언 확인

2. filter 메서드는 두번째 파라미터로 Predicate<Apple> 형식(대상 형식)을 기대.

3. Predicate<Apple>은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스.

4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사.

5. filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야함.


2.4.2 같은 람다, 다른 함수형 인터페이스

- 대상 형식이라는 특정 때문에 같은 람다 표현식이라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용 가능.

2.4.3 형식 추론

- 자바 컴파일러는 람다 표현식이 사용된 컨텍스트를 이용해 람다 표현식와 관련된 함수형 인터페이스를 추론.

> 즉, 대상 형식을 이용해 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론 가능.

> 결과적으로 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략 가능.

> 상황에 따라 명시적으로 형식을 포함하는 것이 좋을 때도 있고 형식을 배제하는 것이 가독성을 향상 시킬 때도 있음.


2.4.4 지역 변수 사용

- 익명 함수가 하는 것처럼 자유 변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용 할 수 있음.

> 이를 람다 캡처링이라고 부름.

- 자유 변수에도 약간의 제약은 존재.

> 인스턴스 변수와 정적 변수를 자유롭게 캡처(자신의 바디에서 참조할 수 있도록)할 수 있음.

> 지역변수는 명시적으로 final선언 또는 실질적으로 final로 선언된 변수와 똑같이 사용되어야 함.

* 즉, 람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡쳐할 수 있음.

- 지역 변수의 제약

> 내부적으로 인스턴스 변수와 지역 변수는 태생이 다름.

> 인스턴스 변수는 힙에 저장, 지역 변수는 스택에 위치.

> 람다에서 지역 변수에 바로 접근할 수 있다는 가정 하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서

 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있음.

> 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공.

> 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 항당해야 하는 제약이 생긴 것.

> 지역 변수의 제약 때문에 외부 변수를 변화시키는 일반적인 명령형 프로그래밍 패턴에 제동을 걸 수 있음.


2.5 메서드 레퍼런스

- 기존의 메서드 정의를 재활용하여 람다처럼 전달 가능.

- 때로는 람다 표현식보다 메서드 레퍼런스를 사용하는 것이 더 가독성이 좋으며 자연스러울 수 있음.


2.5.1 요약

- 메서드 레퍼런스는 특정 메서드만 호출하는 람다의 축약형이라고 생각할 수 있음.

- 메서드명 앞에 구분자(::)를 붙이는 방식으로 메서드 레퍼런스 사용 가능.

- 실제로 메서드를 호출하는 것이 아니므로 ()는 필요 없음.

  - 람다와 메서드 레퍼런스 단축표현 예제

람다

메서드 레퍼런스 단축 표현

(Apple a) -> a.getWeight()

Apple::getWeight

() -> Thread.currentThread().dumpStack()

Thread.currentThread()::dumpStack

(str, i) -> str.subString(i)

String::subString

(String s) -> System.out.println(s)

System.out::println


- 메서드 레퍼런스를 새로운 기능이 아니라 하나의 메서드를 참조하는 람다를 편리하게 표현할 수 있는 문법으로 간주 가능.

- 메서드 레퍼런스 만드는 방법 (굵고 색상 적용하기)

> 메서드 레퍼런스는 세가지 유형으로 구분 가능

* 정적 메서드 레퍼런스

@ Integer의 parseInt 메서드는 Integer::parseInt로 표현 가능.

* 다양한 형식의 인터페이스 메서드 레퍼런스

@ String의 length 메서드는 String::length로 표현 가능.

* 기존 객체의 인스턴스 메서드 레퍼런스

@ Transaction 객체를 할당 받은 expensiveTransaction 지역 변수가 있고, Transaction 객체에는 getValue 메서드가 존재.

 expensiveTransaction::getValue로 표현 가능.


2.5.2 생성자 레퍼런스

- ClassName::new처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 레퍼런스를 만들수 있음.

- 정적 메서드의 레퍼런스를 만드는 방법과 비슷.


2.6 람다, 메서드 레퍼런스 활용하기

2.7 람다 표현식을 조합할 수 있는 유용한 메서드

- Comparator, Function, Predicate 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메서드를 제공.

- 해당 의미는, 간단한 여러 개의 람다 표현식을 조합해서 복잡한 람다 표현식을 만들 수 있다는 것.


2.7.1 Comparator 조합

- 정적 메서드 Comparator.comparing을 이용하여 비교에 사용할 키를 추출하는 Function 기반의 Comparator를 반환 가능.

- 역정렬

> 역정렬을 하기 위해서 다른 Comparator 인스턴스를 생성할 필요가 없음.

> 인터페이스 자체에서 주어진 비교자의 순서를 바꾸는 reverse라는 디폴트 메서드를 제공함.

- Comparator 연결

> 정렬 시 동일한 조건이 결과가 존재한다면 두 번째 Comparator를 생성할 수 있음.

> thenComparing메서드로 두 번째 비교자를 만들수 있음.

> thenComparing은 함수를 인수로 받아 첫 번째 비교자를 이용해서 두 객채가 같다고 판단되면 두 번째 비교자에 객체를 전달.


2.7.2 Predicate 조합

- Predicate 인터페이스는 복잡한 Predicate를 만들수 있도로 negate, and, or 세 가지 메서드를 제공.

2.7.3 Function 조합

- andThen, compose 두 가지 디폴트 메서드를 제공.

2.8 요약

- 람다 표현식은 익명 함수의 일종.

> 이름은 없지만, 파라미터 리스트, 바디, 반환 형식을 가지고 있으며 예외를 던질 수 있음.

- 람다 표현식으로 간결한 코드 구현 가능.

- 함수형 인터페이스는 하나의 추상 메서드만 정의하는 인터페이스.

- 함수형 인터페이스를 기대하는 곳에서만 람다 표현식을 사용할 수 있음.

- 람다 표현식을 이용해서 함수형 인터페이스의 추상 메서드를 즉석으로 제공 가능하여 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급.

- java.util.function 패키지는 Predicate<T>, Function<T, R>, Supplier<T>, Consumer<T>, BianryOperator<T> 등을 포함해서

 자주 사용하는 다양한 함수형 인터페이스를 제공.

- Predicate<T>와 Function<T, R> 같은 제네릭 함수형 인터페이스와 관련한 박싱 동작을 피할 수 있도록 IntPredicate, IntToLongFunction

 등과 같은 기본형 특화 인터페이스도 제공.

- 람다 표현식의 기대 형식을 대상 형식이라 함.

- 메서드 레퍼런스를 이용하면 기존의 메서드 구현을 재사용하고 직접 전달할 수 있음.

- Comparator, Predicate, Function 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있는 다양한 디폴트 메서드를 제공.


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

스트림 활용  (0) 2017.06.26
스트림 소개  (0) 2017.06.16
동작 파라미터화 예제 소스  (0) 2017.06.13
자바8 동작 파라미터화 코드 전달하기  (0) 2017.06.13
자바8을 눈여겨봐야 하는 이유  (0) 2017.06.13

+ Recent posts