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

+ Recent posts