Java 8 to 10 - (4) 새로운 API와 기능

Optional 클래스

  • Optional 클래스는 선택형 값을 캡슐화하는 클래스이다.
    • Optional<T> 객체는 T 타입의 값을 가지는 인스턴스와 빈 인스턴스로 나뉜다.
    • 값을 가지는 인스턴스는 Optional.of(obj)로 얻을 수 있다. 만일 입력 파라미터가 null 이면 바로 예외가 발생된다.
    • 빈 인스턴스는 Optional.empty()로 얻을 수 있는 singleton 객체이다.
    • Optional.ofNullable(obj)를 사용하면 객체가 있으면 값을 가지는 인스턴스를, null 이면 빈 인스턴스를 돌려준다.
  • null 참조가 발생할 수 있는 모든 곳을 Optional로 대체해서는 안된다.
    • 명확하게 값이 존재해야 하는 경우에는 기존과 마찬가지로 예외를 통해 명확하게 상황을 인지할 수 있도록 해야 한다.
    • 어디까지나 값의 존재 여부가 선택적인 경우에 동일한 체크로직을 직접 작성해야 되서 발생하는 실수를 방지하는 것이 중요하다.
  • Optional 또한 기본형 특화 버전을 제공하지만 성능상 이점이 없고 제공되는 메서드도 제한적이므로 되도록 사용을 지양한다.
  • Optional 객체는 null check를 대체할 수 있는 방법을 제공한다.
    • .map(function): 값이 존재하는 경우 함수를 호출하고 그 결과가 담긴 Optional 객체로 반환한다. 값이 없다면 빈 Optional 객체를 반환한다.
    • .flatMap(function): 값이 존재하는 경우 함수를 호출하고 그 결과를 그대로 돌려준다 (즉, Optional 객체로 wrapping 하지 않는다). 단, 함수의 결과값은 Optional 객체여야 한다. 값이 없다면 빈 Optional 객체를 반환한다. 보통 특정 필드의 값을 Optional로 선언한 경우 이중으로 Optional wrapping이 일어나지 않도록 하기 위해 사용한다.
    • .or(supplier): Optional 객체에 값이 있다면 해당 값을, 없다면 입력 파라미터로 주어진 함수를 실행하여 나온 결과를 돌려준다. orElseGet과의 차이점은 함수의 결과가 Optional 객체라는 점이다.
    • .orElse(value): Optional 객체에 값이 있다면 해당 값을, 없다면 입력 파라미터로 주어진 기본 값을 돌려준다.
    • .orElseGet(supplier): Optional 객체에 값이 있다면 해당 값을, 없다면 입력 파라미터로 주어진 함수를 호출하여 나온 결과를 돌려준다. 기본값의 생성을 지연시켜서 불필요한 연산을 줄일 수 있다.
    • .orElseThrow(supplier): Optional 객체에 값이 있다면 해당 값을, 없다면 입력 파라미터로 주어진 함수를 호출하여 나온 예외 객체를 throw 한다.
    • .ifPresent(consumer): Optional 객체에 값이 있다면 함수를 실행하고 아니면 아무것도 하지 않는다.
    • .ifPresentOrElse(consumer, runnable): Optional 객체에 값이 있다면 그 값으로 첫번째 함수를 실행하고 아니면 두번째 함수를 실행한다.
    • .filter(predicate): Optional 객체에 값이 있다면 조건을 만족하는지 확인한다. 조건을 만족하면 Optional 객체를 그대로 돌려주고 아니면 빈 Optional 객체를 돌려준다. 값이 없다면 조건 확인 없이 빈 Optional 객체를 돌려준다.
    • .stream(): Optional 객체에 값이 있으면 하나의 요소를 가지는 스트림을 생성한다. 값이 없다면 빈 스트림을 돌려준다.
    • .get(): Optional 객체의 값을 돌려준다. 값이 없는 경우 예외가 발생한다. Optional 객체에 값이 확실하게 존재하는 경우에만 unwrap을 위해서 사용해야 한다.
    • flatMap을 사용하면 Javascript의 optional chaining과 비슷한 구현이 가능하다.
    • // Person::getCar, Car::getInsurance는 Optional<Person>, Optional<Car>를 돌려준다. // Insurance::getName은 String을 바로 반환한다. public String getCarInsuranceName(Optional<Person> person) { return person.flatMap(Person::getCar) .flatMap(Car::getInsurance) .map(Insurance::getName) .orElse("Unknown"); }
  • Optional을 사용함으로서 인터페이스를 통해 좀 더 명확한 의도(값이 선택적임)를 전달할 수 있다.
  • Optional은 선택형 반환값을 지원하기 위해 만들어졌으므로 직렬화를 지원하지 않는다.
    • 아래와 같이 getter에서 선택형 반환값을 돌려주는 형태로 구현하는 것이 원래 의도에 더 적합하다.
    • public class Person { private Car car; public Optional<Car> getCar() { return Optional.ofNullable(car); } }
    • 다만 직렬화가 필요없는 경우라면 멤버 변수를 Optional 로 선언할 수는 있다.

java.time API

Temporal API

  • java.time.temporal package를 통해 시간을 다루는 방식을 표준화하고 모든 객체를 불변형으로 제공하여 상태 변경으로 인해 잠재적으로 발생할 수 있는 문제점을 해결하였다.
  • 지역화된 날짜 객체도 제공하지만 입출력을 지역화하는 경우를 제외하면 모든 경우에 LocalDate를 사용하는 것이 바람직하다.
  • 특정 시점을 표현하는 여러가지 방식을 제공한다
    • Temporal 인터페이스를 통해 특정 시점을 표현하는 객체를 다루는 방식을 표준화했다.
    • 로컬 날짜 및 시간을 표현하는 LocalDate, LocalTime, LocalDateTime 클래스를 제공한다.
    • 유닉스 에포크 시간을 표현하는 Instant 클래스를 제공한다.
    • 이외에도 다양한 시간 표현 방식을 제공한다.
  • 두 시점간의 차이를 표현하는 여러가지 방식을 제공한다.
    • TemporalAmount 인터페이스를 통해 두 시점간의 차이를 표현하는 객체를 다루는 방식을 표준화했다.
    • 두 날짜 간의 차이를 표현하는 Period 클래스를 제공한다.
    • 두 시간 간의 차이를 표현하는 Duration 클래스를 제공한다.
  • 날짜를 조작하는 유틸리티 객체를 제공한다.
    • java.time.temporal.TemporalAdjusters 의 정적 메서드들을 통해서 날짜를 조작하는 몇가지 유틸리티 객체를 제공한다.
    • 직접 구현하는 것도 가능하다.
    • @FunctionalInterface public interface TemporalAdjuster { Temporal adjustInto(Temporal temporal); } // 예: 다음 업무일 구하기 TemporalAdjuster nextWorkingDay = (t) -> { int dayOfWeek = t.get(ChronoField.DAY_OF_WEEK); if (dayOfWeek >= 5) { return t.with(next(DayOfWeek.MONDAY)); } else { return t.plus(1, ChronoUnit.DAYS); } };

Formatter API

  • Thread safe한 날짜, 시간 형식 변환을 제공하는 객체이다.
  • 기본적으로 BASIC_ISO_DATE (yyyymmdd), ISO_LOCAL_DATE (yyyy-mm-dd)를 제공한다.
  • .ofPattern(string[, locale]): 특정 패턴으로 formatter를 생성한다.
  • DateTimeFormatterBuilder를 통해서 builder 방식으로 formatter를 생성할 수 있다.

Timezone API

  • 시간대를 나타내는 ZoneId 클래스를 제공한다.
    • .of(id): 특정 시간대의 ZoneId 인스턴스를 얻는다. id지역/도시 형식으로 이루어지며 IANA Time Zone Database에서 제공하는 지역 집합 정보를 기반으로 한다.
    • .systemDefault(): 기본 타입존에 해당되는 ZoneId 인스턴스를 가져온다.
    • ZonedDateTime 객체는 LocalDateTime + ZoneId의 조합이다.
      • ZonedDateTime.of(dateTime, zoneId)으로 생성할 수 있다.
  • 또는 UTC 시간대를 기준으로 시간 차이를 표현하는 ZoneOffset도 제공한다.
    • .of(offsetString): 특정 시간 차이에 해당되는 ZoneOffset 인스턴스를 얻는다 (예: ZoneOffset.of(”-05:00”)).
    • 단, 시간 차이는 고정이므로 서머 타임 등 특수한 상황은 반영되어있지 않다.
    • ZoneOffsetZoneId의 하위 클래스이다.
    • OffsetDateTime 객체는 LocalDateTime + ZoneOffset의 조합이다.
      • OffsetDateTime.of(dateTime, zoneOffset)으로 생성할 수 있다.

디폴트 메서드

  • 인터페이스에 새로운 메서드를 추가할 때 기본 구현을 추가할 수 있다.
    • 기본 구현을 추가함으로써 기존에 해당 인터페이스를 상속 받았던 모든 클래스를 고치지 않고도 새로운 메서드를 추가할 수 있다.
  • 인터페이스에 정적 메서드를 추가할 수 있다.
    • 도구 메서드들을 제공하기 위해서 별도의 유틸리티 클래스를 선언할 필요가 없다.
  • 인터페이스를 이용하여 다중 상속을 구현할 수 있다.
    • 다중 상속 시 순서는 아래와 같이 정해진다.
      • 클래스에 정의된 메서드가 가장 우선권을 갖는다.
      • 그 다음에는 서브 인터페이스에 정의된 메서드가 우선권을 갖는다.
      • 위 규칙으로도 결정되지 않은 경우에는 인터페이스를 상속받은 클래스가 명시적으로 해당 메서드를 오버라이드하고 원하는 메서드를 호출해야 한다.
    • 새로운 super 문법을 제공한다.
      • SuperClass.super.method() 형식으로 특정 상위 클래스의 메서드를 호출할 수 있다.
      • 하위 클래스에서 특정 상위 클래스의 메서드를 호출할 때 사용한다.
  • 다중 상속의 방식은 선택형 메서드, 동작 다중 상속의 두가지가 있다.
    • 선택형 메서드
      • UnsupportedOperationException을 던지는 기본 구현을 제공하는 방식이다.
      • 상속하는 클래스에서 빈 구현을 추가할 필요가 없으며 필요한 경우에만 override해서 구현할 수 있다.
    • 동작 다중 상속
      • 서로 겹치지 않는 (디폴트 메서드로 구현된) 기능을 다중 상속하여 손쉽게 재사용 할 수 있다.
        • 이는 마치 ruby의 mixin과 비슷한 형태이다.
      • 이를 위해서는 각 인터페이스가 서로 기능이 중복되지 않는 최소의 인터페이스로 구현되어야 한다.

모듈

  • Java 9에서는 패키지를 묶는 새로운 단위인 모듈을 정의하고 서로 다른 모듈 간의 의존성을 좀 더 세밀하게 지정할 수 있는 방식을 제공한다.
  • 각 모듈의 source root src/main/java 위치에 module-info.java를 작성하여 모듈을 정의할 수 있다.
    • 모듈의 이름은 일반적으로 패키지의 이름과 유사한 방식으로 정의한다.
      • 일반적으로는 포함되는 패키지의 prefix와 일치하게 명명한다.
    • 몇가지 특수한 키워드 구문을 사용하여 모듈을 정의할 수 있다.
      • module MODULE_NAME { ... }: 모듈을 정의한다.
      • requires [transitive] MODULE_NAME : 특정 모듈에 의존성이 있음을 명시한다. transitive를 명시할 경우 만약 다른 모듈이 현재 모듈을 사용한다면 자동으로 특정 모듈에 대한 의존성이 추가된다.
      • exports PACKAGE_NAME [to MODULE_NAMES...] : 특정 패키지를 모듈 외부로 노출한다. 단, 노출 대상이 되는 것은 public 제한자를 가진 것들만 해당된다. 노출 대상이 되는 모듈을 제한하고 싶다면 to MODULE_NAME으로 지정할 수 있다.
      • opens PACKAGE_NAME [to MODULE_NAMES...] : 특정 패키지를 모듈 외부로 노출한다. 제한자와 관계없이 모든 항목이 노출되므로 reflection을 통해 모든 항목에 접근 가능하다. 노출 대상이 되는 모듈을 제한하고 싶다면 to MODULE_NAME으로 지정할 수 있다.
      • provides INTERFACE_NAME with CLASS_NAMES...: 특정 인터페이스의 구현체를 지정한다.
      • uses INTERFACE_NAME: 특정 인터페이스의 구현체들을 사용함을 명시한다.
        • 이렇게 명시된 인터페이스의 구현체들을 ServiceLoader를 통해서 접근하여 사용할 수 있다.
        • 구현체는 0개 또는 하나 이상이 지정될 수 있으므로 그 중에서 필요한 구현체를 직접 선택하여 사용해야 한다.
        • package greeting.client; import java.util.ServiceLoader; import greeting.api.MessageService; public class TestClient { public static void main(String[] args) { ServiceLoader<MessageService> ms = ServiceLoader.load(MessageService.class); for (MessageService m : ms) { System.out.println(m.getMessage()); } } }
    • 다음은 모듈 정의의 예시이다.
    • module com.example.foo { requires com.example.foo.http; requires transitive com.example.foo.network; requires java.logging; exports com.example.foo.bar; exports com.example.foo.internal to com.example.foo.probe; opens com.example.foo.quux; opens com.example.foo.internal to com.example.foo.network, com.example.foo.probe; provides com.example.foo.spi.Intf with com.example.foo.Impl; uses com.example.foo.spi.Intf; }
  • 자동 모듈은 module-info.java를 통해서 모듈을 정의하지 않은 경우 자동으로 생성되는 모듈을 뜻하며 모든 패키지를 open 한 것과 동일하다.