Java 8에 새로 생긴 인터페이스로 라이브러리 메서드가 반환할 결과값이 없음을 명백하게 표현할 필요가 있는 곳에서 제한적으로 사용할 수 있는 메커니즘을 제공하기 위해 새로 생겨났다.
Java api doc의 API 노트를 보면 다음과 같이 설명하고 있다. Optional은 주로 결과 없음을 나타낼 필요성이 명확하고 null을 사용하면 오류가 발생할 수 있는 메소드 반환 유형으로 사용하도록 고안되었다.
한마디로 비어있을 수도 있고, 어떠한 값 하나만 담고 있을수도 있는 인스턴스의 타입
등장 배경
1. 참조형 멤버변수 와 NPE
런타임에 NPE(NullPointerException)라는 예외를 발생시킬 수 있다.
NPE 방어를 위해서 들어간 null 체크 로직 때문에 코드 가독성과 유지 보수성이 떨어진다.
/* OnlineClass.java */publicProgress progress;publicProgressgetProgress() {return progress;}publicvoidsetProgress(Progress progress) {this.progress= progress; }}/* OptionalTestApp.java */OnlineClass spring_boot = new OnlineClass(1, "spring boot", true); // 이슈상황 → 참조형 멤버 변수 사용 시 초기화 되지 않아 null 값을 참조 할 수 있다.
Duration studyDuration =spring_boot.getProgress().getStudyDuration(); // NullPointExecption 발생System.out.println(studyDuration);
모든 객체타입은 null을 갖을 수 있기 때문에 해당 값이 null인지 아닌지는 run time에 정확히 알지 못할 수가 있어 NPE를 발생시킬 여지가 있다.
사용하는 시점에 null을 체크하는 방법도 존재하는데 이러면 get 메서드마다 null을 체크하는 코드를 작성해줘야 하고, 비즈니스로직보다 null 체크 코드가 더 길어져 가독성이 떨어질 수 있다.
이러한 문제들을 스칼라나 하스켈과 같은 함수형 언어들은 존재할지 안 할지 모르는 값을 표현할 수 있는 타입을 가지고 있고 자바도 처음 만들어졌을떄 존재하지 않는 값을 표현하기 위해 null을 만들었다면 이번에는 위 언어들의 컨셉을 모티브로 Optional API를 만들고 null 처리를 Optional에게 위임하는 방법으로 해결하고자 추가되었다.
장점
NPE를 유발할 수 있는 null을 직접 다루지 않아도 된다.
수고롭게 null 체크를 직접 하지 않아도 된다.
명시적으로 해당 변수가 null일 수도 있다는 가능성을 표현할 수 있다. (따라서 불필요한 방어 로직을 줄일 수 있다!)
이때 orElseGet()은 Supplier를 인자로 받으며, 값이 없을때에 해당 supplier가 수행된다. 하지만 orElse()는 Optional로 감싸고 있는 객체타입을 인자로 받으며 값이 있더라도 내부가 수행되고 사용되지 않는 경우 해당 객체를 지우게 되어 필요없는 오버헤드가 발생한다.
예를 들어 위와 같은 코드를 작성했을때 optStr은 null이 아니라 new String("hi1")가 실행되지 않을 것 같지만 바이트코드를 보면 새로 문자열을 생성했다가 POP하는 것을 볼 수 있고, orElseGet()은 우리가 lambda에서 봤던것처럼 static 메서드로 생성하여 호출되는 타이밍에 이를 실행해 객체를 생성하는 것을 볼수있어 orElse는 필요없는 오버헤드에 주의해야한다. 하지만 반드시 이미 생성되어있는 객체를 반환하는 것이라면 orElse()를 사용하는 것이 좋을 수도 있다.
예외처리를 하는 경우에도 isPresent()를 통한 예외처리보다는 orElseThrow()를 이용하여 예외처리를 하는 것이 바람직하다. 인자로 Supplier를 통해 특정 Exception을 던질 수 있는데 아무것도 주지 않으면 기본적으로 NoSuchElementException을 던진다.
3. Optional이 있을때만 이를 소비하여 무언가를 할때는 isPresent()가 아닌 ifPresent()를 활용
Optional은 애초에 필드로 사용할 목적으로 만들어지지 않아 Serializable도 구현하지 않았기 때문에 사용을 지양해야 한다.
9. Optional을 메서드,생성자 인자로 사용하지말자.
//badpublicvoidrender(Optional<Renderer> renderer){renderer.orElseThrow(() ->newIllegalArgumentException("null 일 수 없습니다."));//...}//goodpublicvoidrender(Renderer renderer){if(renderer ==null){thrownewIllegalArgumentException("null 일 수 없습니다."); }//...}
이러한 방법은 불필요하게 코드를 복잡하게 할 뿐아니라 이를 호출하는 쪽에서도 Optional 생성을 강제하게 하는 것이다. 또한, Optional은 하나의 객체로 이를 호출하는 것이 결코 비용이 저렴하지 않다.
10. null이 확실하면 ofNullable()이 아닌 of()를 사용하자.
//badOptional.ofNullable("NULL일 수 없지!");//goodOptional.of("NULL일 수 없지!");
//ofNullablepublicstatic<T>Optional<T>ofNullable(T value) {return value ==null?empty():of(value);}
ofNullable은 내부적으로 보면 삼항연산자를 통해 비어있지 않는 경우 of를 호출하는 것을 볼 수있다. 그렇기 때문에 이러한 연산을 조금이라도 줄일 수 있기 때문에 of를 사용하자.
//badOptional<Integer> max =Optional.of(10);for(int i=0; i <max.get(); i++)//goodOptionalInt max =OptionalInt.of(10);for(int i=0; i <max.getAsInt(); i++)//byte codeL0 LINENUMBER 10L0 BIPUSH 10INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;INVOKESTATIC java/util/Optional.of (Ljava/lang/Object;)Ljava/util/Optional; ASTORE 0L1 LINENUMBER 12L1 BIPUSH 10INVOKESTATIC java/util/OptionalInt.of (I)Ljava/util/OptionalInt; ASTORE 1
내부적으로 Integer.valueOf()를 통해 한번 boxing이 일어나는 것을 볼 수 있고 또 이를 사용할때 unboxing이 일어나기 때문에 OptionalInt를 사용하는 것이 좋다.
12. Optional을 리턴하는 메서드에서 null을 리턴하지 말자.
publicstaticOptional<String>hi(){returnnull;}
당연한거지만 Optional도 객체이기 때문에 null을 리턴이 가능한데 이렇게 리턴하게 되면 Optional을 사용하는 것이 의미가 없기 때문에 null을 리턴하지 말자.
Optional API
1. Optional 생성
선언하기
제네릭을 제공하기 때문에, 변수를 선언할 때 명기한 타입 파라미터에 따라서 감쌀 수 있는 객체의 타입이 결정된다.
Optional<Order> maybeOrder; // Order 타입의 객체를 감쌀 수 있는 Optional 타입의 변수Optional<Member> optMember; // Member 타입의 객체를 감쌀 수 있는 Optional 타입의 변수Optional<Address> address; // Address 타입의 객체를 감쌀 수 있는 Optional 타입의 변수
변수명은 그냥 클래스 이름을 사용하기도 하지만 maybe나 opt와 같은 접두어를 붙여서 Optional 타입의 변수라는 것을 좀 더 명확히 나타내기도 한다.
객체 생성하기
Optional.empty() : null을 담고 있는, 한 마디로 비어있는 Optional 객체를 얻어온다.
Optional<Member> maybeMember =Optional.empty();
이 비어있는 객체는 Optional 내부적으로 미리 생성해놓은 싱글턴 인스턴스이다.
Optional.of() : null이 아닌 객체를 담고 있는 Optional 객체를 생성
위 코드의 문제점은 두가지가 존재하게 된다. 첫번째로 if 조건문 내에 null 체크와 비지니스 로직이 혼재되어 있어서 가독성이 떨어진다는 점이다. 두번째로는 null을 리턴할 수 있기 때문에 메소드 호출부에 NPE 위험을 전파하고 있다는 것이다.
이런 문제점 해결하고자 filter를 적용하면 아래와 같이 코드를 작성 할 수 있다.
publicOptional<Member>getMemberIfOrderWithin(Order order,int min) {returnOptional.ofNullable(order).filter(o ->o.getDate().getTime() >System.currentTimeMillis() - min *1000).map(Order::getMember);}
Optional과 filter를 이용하면 if 조건문 없이 메소드 연쇄 호출만으로도 좀 더 읽기 편한 코드를 작성할 수 있을 뿐만 아니라, 메소드의 리턴 타입을 Optional로 사용함으로써 호출자에게 해당 메소드가 null을 담고 있는 Optional을 반환할 수도 있다는 것을 명시적으로 알려준다.
Optional flatMap(Function) : Optional 안의 인스턴스가 Optional인 경우 사용하면 편리하며 Stream에서 사용하는 경우와 비슷하게 Optional을 한번 분리해서 쪼개주는 걸 뜻한다.
Date / Time
날짜와 시간을 표현하기 위해 Java에서 사용해왔다.
등장 배경
1. 명확하지 않은 클래스 이름
날짜 클래스중 Date 는 시간과 TimeStamp 모두 표현할 수 있다. (사실상 TimeStamp)
시간 값이 에폭타임 이라 하여 세계 표준시(UTC)로 1970년 1월 1일 00시 00분 00초를 기준으로 현재까지 흐른 모든 시간을 초(sec)단위로 표현 하여 사람이 알아보기 어렵다.
2. Thread safe하지 않은 mutable한 속성
publicstaticvoidmain(String[] args) throws InterruptedException {Date date =newDate();long time =date.getTime();System.out.println("date = "+ date);Thread.sleep(1000*3);Date after3Seconds =newDate();System.out.println("after3Seconds = "+ after3Seconds);after3Seconds.setTime(time);System.out.println("after3Seconds = "+ after3Seconds);}/* [실행 결과] date = Thu Oct 28 20:22:24 KST 2021 after3Seconds = Thu Oct 28 20:22:27 KST 2021 after3Seconds = Thu Oct 28 20:22:24 KST 2021*/
새로 생성한 after3Sceconds 라는 객체가 setter를 통해 다른 시간으로 변경이 된 것을 볼 수 있는데, 이는 Date 클래스가 mutable 하다는 것을 의미한다. mutable하기 때문에 thread unsafe 하다.
thread unsafe? Date 인스턴스의 값을 각각 다른 Thread에서 접근해서 변경이 가능하면 기존에 사용하던 Thread에서 변경 되어 잘못된 Date 정보를 가져와서 버그가 발생할 위험이 있다는 뜻.
Date/Time의 모든 객체는 mutable한 속성의 단점을 해결하고자 Immutable한 속성을 갖고 설계가 되었는데 이 때문에 메서드를 이용해 날짜,시간을 변경하면 위에 정의된 with()함수를 사용하게 되고 with()함수는 새로운 객체를 만들어 반환하고 있는 것을 볼 수 있다.
LocalDateTime now =LocalDateTime.now(); //서버의 시스템 zone 기준System.out.println(now);LocalDateTime of =LocalDateTime.of(1982,Month.JULU,15,0,0,0);ZonedDateTime nowInKorea =ZonedDateTime.now(ZoneId.of("Asia/Seoul"));System.out.println(nowInKorea);
보통 사람이 읽고 쓰기 편한 시간 표현방식으로 표현해주는 API
LocalDateTime.now() : 현재 시스템 Zone에 해당하는(로컬) 일시를 반환
ZonedDateTime.now(ZoneId.of("UTC")) : 특정 Zone의 현재 시간을 반환합니다.
ZonedDateTime.of(1988, Month.JUNE.getValue(),10,0,0,0,0, ZoneId.of("UTC")) : 특정 Zone의 특정 일시를 반환합니다.
3. Duration / Period
//PeriodLocalDate today =LocalDate.now();LocalDate thisYearBirthDay =LocalDate.of(2022,Month.FEBRUARY,7);Period period =Period.between(today, thisYearBirthDay);System.out.println("생일까지 남은 기간 : " + period.getYears() + " 년 " + period.getMonths() + "월 " + period.getDays() + "일" ); //생일까지 남은 기간 : 0 년 3월 5일
Period p =today.until(thisYearBirthDay);System.out.println("생일까지 남은 기간 : " + p.getYears() + " 년 " + p.getMonths() + "월 " + p.getDays() + "일" ); //생일까지 남은 기간 : 0 년 3월 5일
//DurationInstant now =Instant.now();Instant plus =now.plus(10,ChronoUnit.SECONDS);Duration between =Duration.between(now,plus);System.out.println(between.getSeconds()); //10
Period는 사람이 사용하는 날짜/시간의 기간을 측정, Duration은 초단위(나노,밀리)로 반환을 하기 때문에 주로 기계용 시간간의 기간을 측정하는데 사용할 수 있다.
시간 비교시 유용한 방식
위의 방식은 대부분 시간 메소드를 어떤식으로 사용해야하는 지에 대해서 이야기하는게 대부분인데, 실제로 제일 필요한 건 시간 비교가 제일 유용할 것 같아서 좀 더 정리해본다.
이때 truncatedTo() 메서드를 이용해서 시간을 잘라 내고 날짜만으로 비교가 가능한데 trucatedTo()메서드는 파라미터로 지정된 단위 이후의 값들을 버린 후, 복사한 LocalDateTime 객체를 리턴하는 메서드이다. 파라미터로 전달되는 단위는 ChronoUnit 클래스에 지정된 상수를 사용하며, DAYS보다 큰 단위인 YEARS, MONTHS 등의 값은 허용되지 않는다.
일수부터는 생략한다는 개념이 모호하다.예를 들어, day에 0이 오는것도 말이 안되며 1이 온다고 해도 일수를 잘라내 정확히 년,월을 뜻하는게 아니라 년,월,1일을 뜻하는 것이기 때문에 매개변수로 올 수 가 없는 것이다.
하지만 해당해의 1일을 만약에 표시하고 싶을경우에는 TemporalAdjusters(시간 조정기)를 이용할 수 있다.
date.with(TemporalAdjusters.firstDayOfMonth()).truncatedTo(ChronoUnit.DAYS); //2021-11-01T00:00date.with(TemporalAdjusters.firstDayOfYear()) : 해당 년도의 1월 1일 //2021-01-01T17:02:22.973160900date.with(TemporalAdjusters.firstDayOfMonth()) : 해당 월의 1일 //2021-11-01T17:03:05.777745100date.with(TemporalAdjusters.lastDayOfYear()) : 해당 년도의 마지막 날짜 //2021-12-31T17:03:34.531656400date.with(TemporalAdjusters.lastDayOfMonth()) : 해당 월의 마지막 날짜 //2021-11-30T17:04:03.837483400
ChronoUnit
4. DateTimeFormatter
//formattingLocalDateTime now =LocalDateTime.now();DateTimeFormatter formatter =DateTimeFormatter.ISO_DATE_TIME;System.out.println(now.format(formatter)); //2021-11-02T23:28:51.388655formatter =DateTimeFormatter.ofPattern("MM.dd.yyyy");System.out.println(now.format(formatter)); //11.02.2021
DateTimeFormatter은 ofPattern()을 이용해 특정 패턴을 지정할 수 있고 미리 정의된 포맷터들이 존재하는데 이 형식을 이용하고자하면 굳이 새로 정의해줄 필요가 없다. 정의된 포맷터들은 여기를 참고하자.
이때, Formatter에 사용되는 형식은 다음처럼 사용 가능하다.
Symbol Meaning Presentation Examples
------ ------- ------------ -------
G era text AD; Anno Domini; A
u year year 2004; 04
y year-of-era year 2004; 04
D day-of-year number 189
M/L month-of-year number/text 7; 07; Jul; July; J
d day-of-month number 10
Q/q quarter-of-year number/text 3; 03; Q3; 3rd quarter
Y week-based-year year 1996; 96
w week-of-week-based-year number 27
W week-of-month number 4
E day-of-week text Tue; Tuesday; T
e/c localized day-of-week number/text 2; 02; Tue; Tuesday; T
F week-of-month number 3
a am-pm-of-day text PM
h clock-hour-of-am-pm (1-12) number 12
K hour-of-am-pm (0-11) number 0
k clock-hour-of-am-pm (1-24) number 0
H hour-of-day (0-23) number 0
m minute-of-hour number 30
s second-of-minute number 55
S fraction-of-second fraction 978
A milli-of-day number 1234
n nano-of-second number 987654321
N nano-of-day number 1234000000
V time-zone ID zone-id America/Los_Angeles; Z; -08:30
z time-zone name zone-name Pacific Standard Time; PST
O localized zone-offset offset-O GMT+8; GMT+08:00; UTC-08:00;
X zone-offset 'Z' for zero offset-X Z; -08; -0830; -08:30; -083015; -08:30:15;
x zone-offset offset-x +0000; -08; -0830; -08:30; -083015; -08:30:15;
Z zone-offset offset-Z +0000; -0800; -08:00;
p pad next pad modifier 1
' escape for text delimiter
'' single quote literal '
[ optional section start
] optional section end
# reserved for future use
{ reserved for future use
} reserved for future use
LocalDateTime now =LocalDateTime.now();formatter =DateTimeFormatter.ofPattern("MM.dd.yyyy");System.out.println(now.format(formatter)); //11.02.2021//parsingLocalDate parse =LocalDate.parse("08.12.2021",formatter);System.out.println(parse);
또한, Date/Time타입의 parse()메서드를 통해서 특정 문자열을 ofPattern에서 선언한 패턴 방식으로 파싱하여 LocalDate 타입의 인스턴스를 생성해 반환할 수도 있다.
5. 레거시 API 지원
예전에 구현 및 사용하던 날짜와 시간(Date) API와 현재 추가된 LocalDate, LocalDateTime, Instant는 서로 호환 된다!