2014년 4월 26일 토요일

자바 8 살펴보기



자바8을 배워둬야 하는 이유

지난달 18일 자바Java 8이 정식으로 발표 되었다.

자바의 버전 업은 크게 2가지로 구분하는데, 기능적인 부분의 개선이 주를 이루는 버전업을 진화계라evolution 하고 언어 자체의 형태에 변화가 오는 버전업을 혁명계revolution 라 칭한다. 자바 6,7이 전자에 속하며 자바 5와 이번에 발표된 자바 8이 후자에 속한다.

함수형 언어의 여러 개념이 도입됨으로 인해 자바 8는 자바 5보다 훨씬더 많은 변화를 자바 언어에 가져올 것으로 보이며  코드의 형태 자체가 이전의 자바와는 확연히 구별되는 모습으로 바뀔것이 확실하다.

이 말은 기존 자바 개발자들이 자바8을 제대로 사용하기 위해서는 새로운 언어 한가지를 더 배우는 정도의 학습 비용이 요구된다는 뜻으로, 2,3년후 새로운 문법에 최적화된 방식으로 자바를 배운 개발자와 기존 개발자간의 간격은 작성된 코드의 형태를 통해 뚜렷하게 드러날 것이다.

함수형 언어

우선 자바8을 들여다 보기 전에 함수형 언어의 개념에 대해서 이해하지 않으면 안된다.
함수형 언의 개념에 대해 잠시 시간을 내어 관련 기사들을 읽어 두도록 하자.


자바8의 모든것

캘리포니아에 있는 소프트웨어 개발사인 TechEmpower에서는 자사의 블로그 에 "Everything about Java 8"란 타이틀로 자바8의 새로운 기능들을 요약해서 소개해 놓았다. 본 포스팅은 이 기사를 요약/정리한 InfoQ의 기사를 기반으로 작성되었다.

자바8의 전체 기능에 대한 상세는 Java.net가 제공하는 기능일람을 참고하기 바란다.

인터페이스의 개선

인터페이스에 static 메소드를 정의하는것이 가능해졌다. java.util.Comparator에 추가된 static naturalOrder메소드를 살펴보자.
    public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
        return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
    }
default 지시자를 이용해 기본 메소드의 정의가 가능하게되어 인터페이스를 구현하는 기존 코드의 변경없이 새 메소드의 추가가 가능해졌다. 예를 들어 java.lang.Iterable에는 forEach메소드가 default로 정의되어 있다.
    public default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
처리해야할 데이터인 Customer와 처리 내용인 action이 메소드의 인자값으로 전달 가능하게 되어 Iterable Collection에 대한 반복처리가 매우 간단히 구현 가능하게 된다.

여기서 한가지 주의해야 할 점은 예외적으로 Object클래스의 메소드에 대해서 default구현은 정의 할 수 없다는 점이다.

함수형 인터페이스

함수 인터페이스functional interface는 단 하나의 추상 메소드가 정의 가능한 인터페이스이다. 인터페이스가 함수형 인터페이스임을 나타내는 수단으로서 FunctionalInterface 어노테이션이 도입되었다. 예를 들어, java.lang.Runnable은 다음과 같은 함수 인터페이스를 지닌다.
    @FunctionalInterface
    public interface Runnable {
        public abstract void run();
    }
하지만, 어노테이션을 통해 명시적으로 지정하지 않더라도 함수 인터페이스의 정의를 만족하는 인터페이스라면 자바 컴파일러가 주석의 유무에 상관없이 함수 인터페이스로서 취급한다.

람다식

함수형 인터페이스의 중요한 특성으로 람다식lambda expression을 사용한 인스턴스 생성이 있다. 람다식에 대한 자세한 설명은 아래 기사를 참고하자.

자바에서 람다 식이 필요한 이유 – 1
자바에서 람다 식이 필요한 이유 – 2

람다식을 이용하면 동작과 데이터를 모두 동적으로 설정하는것이 가능해진다. 아래의 예제들은 모두 왼쪽이 입력값이 되고, 오른쪽이 동작에 대한 정의이다. 입력값의 데이터 타입이  유추 가능하므로 생략되고 있다는 점에 주목하자.

    (int x, int y) -> { return x + y; }
    (x, y) -> x + y
    x -> x * x
    () -> x
    x -> { System.out.println(x); }
예를들어 Runnable 함수 인터페이스를 인스턴스화 하는것은 다음과 같다.
    Runnable r = () -> { System.out.println("Running!"); }

여담으로 필자는 위의 예제를 처음 접했을때 잠시 맨붕이 왔었다.
오래전 Lisp의 벽에 부딧혀 emacs에서 손을 뗀 필자에게 모습을 바꿔 찾아온 함수형 프로그래밍은 이제 10년 넘는 세월을 투자해 간신히 얻은 자바프로그래머로서의 지위에 대해 유통기간을 부여하고 있었다.

메소드 참조

메소드 참조method reference는 이미 이름이 있는 메서드를 대상으로 한 람다식의 간략형이며, 메소드 참조를 나타내는 예약어로서 (::)를 사용한다. 메소드 참조의 예와 그에 대응하는 람다식은 다음과 같다. 오른쪽이 메소드 참조, 왼쪽이 람다식이다.
    String::valueOf     x -> String.valueOf(x)
    Object::toString    x -> x.toString()
    x::toString         () -> x.toString()
    ArrayList::new      () -> new ArrayList<>()

캡처 vs 비캡처 람다식Capturing versus non-capturing lambdas

람다식의 외부에 정의된 static이 아닌 변수나 객체에 억세스하는것을 람다가 객체를 "캡쳐"한다고 부른다. 예를 들면 다음은 람다 변수 x에 억세스하는것이다.
    int x = 5;
    return y -> x + y;
람다식으로부터 억세스 가능한것은 로컬변수와 블록구의의 파라매터중에 final이거나 사실상 final판정effectively final을 받은 것에 한정된다.

java.util.function패키지에는 많은 새로운 함수형 인터페이스가 추가되었다. 몇가지를 예로 들자면 다음과 같다.

  • Function <T, R> - T를 입력으로 R을 출력으로 반환
  • Predicate <T> - T를 입력으로 boolean을 출력으로 반환
  • Consumer <T> - T를 입력으로 아무것도 반환하지 않는다
  • Supplier <T> - 입력을 취하지 않고 T를 반환
  • BinaryOperator <T> - 2 개의 T를 입력으로 하나의 T를 출력으로 반환

java.util.stream

자바 8의 중요한 패러다임의 하나로 새로운 java.util.stream패키지는 스트림에 대한 함수형 조작을 제공한다. 좀더 알기쉽게 설명하자면 배열이나 리스트, 맵으로 대표되는 컬랙션을 스트림으로 다룰 수 있게 되었다는 것 이다. 다음은 컬랙션에 대한 스트림화의 예이다.
   Stream <T> stream = collection.stream ();
이것이 함수형 프로그래밍과 결합하면 다음과 같은 형태가 된다.
    int sumOfWeights = blocks.stream () filter (b -> b.getColor () == RED)
                                      . mapToInt (b -> b.getWeight ())
                                      . sum ();
위의 샘플코드는 stream패키지의 Javadoc에 실린 예로서, stream의 소스로서 blocks라는 Collection을 사용하고 있다. 그 스트림에 대해 filter-map-reduce를 실행하여 붉은색(RED)블록에 대한 무게(weight)의 합(sum)을 구하는 일련의 과정이 한줄의 코드에 집약되어 표현되고 있다.

제네릭 타입 인터페이스의 개선

이 개선은 자바 컴파일러가 형에대한 추론능력을 갖추는 것으로 제네릭 형식 메소드 호출시 인수에 대한 형 정의를 생략 가능하게 해 준다.
예를 들어 자바7의 코드가 다음과 같은 것 이었다면
    foo(Utility.<Type>bar());
    Utility.<Type>foo().bar();
자바8에서는 인수와 호출에 대한 추론이 자동적으로 이루어져 다음과 같이 형태가 된다.
    foo(Utility.bar());
    Utility.foo().bar();

java.time

새로운 날짜/시간 관련 API가 java.time 패키지에 추가되고 있다. 클래스는 immutable이며 스레드에 대해 안전하다. 날짜 및 시간 형식으로  Instant, LocalDate, LocalDateTime, ZonedDateTime이 추가되었으며 날짜와 시간 이외의 것으로서 Duration과 Period가 추가되었다. 새로 추가된 값 형식은 Month, DayOfWeek, Year, Month YearMonth, MonthDay, OffsetTime, OffsetDateTime등이 있다. 이런한 새로운 날짜/시간 클래스는 대부분이 JDBC에서 지원됨으로서 RDB연동의 효율적인 구현이 가능하다.

Collections API의 확장

인터페이스가 default 메소드를 가질 수 있게 됨으로써 자바8의 Collection API에는 다수의 메소드가 새롭게 추가되었다. 인터페이스는 모두 default 메소드가 구현되었으며 새로이 추가된 메소드의 일람은 다음과 같다.
  • Iterable.forEach(Consumer)
  • Iterator.forEachRemaining(Consumer)
  • Collection.removeIf(Predicate)
  • Collection.spliterator()
  • Collection.stream()
  • Collection.parallelStream()
  • List.sort(Comparator)
  • List.replaceAll(UnaryOperator)
  • Map.forEach(BiConsumer)
  • Map.replaceAll(BiFunction)
  • Map.putIfAbsent(K, V)
  • Map.remove(Object, Object)
  • Map.replace(K, V, V)
  • Map.replace(K, V)
  • Map.computeIfAbsent(K, Function)
  • Map.computeIfPresent(K, BiFunction)
  • Map.compute(K, BiFunction)
  • Map.merge(K, V, BiFunction)
  • Map.getOrDefault(Object, V)

Concurrency API의 확장

Concurrency API의 기능이 추가되었다. 몇가지를 소개해 보자면, ForkJoinPool.commonPool()은 모든 병렬 스트림 작업을 처리하는 구조이다. ForkJoinTak는 명시적으로 특정 풀을 가지지 않고, 일반적인 풀을 사용하게 되었다. 말도 많고 탈도 많았던 ConcurrentHashMap은 완전히 재 작성되었다. 또한 새로운 Locking처리의 구현으로서 추가된 StampedLock은 ReentrantReadWriteLock의 대안으로 사용할 수 있다.
 Future인터페이스의 구현인 CompletableFuture에서는 비동기 작업의 실행과 체이닝을 위한 방법이 제공된다.

IO/NIO API의 확장

IO/NIO에 메소드가 추가되어 파일이나 입력 스트림에서 java.util.stream.Stream을 직접 생성할 수 있게 되었다.
  • BufferedReader.lines ()
  • Files.list (Path)
  • Files.walk (Path, int FileVisitOption ...)
  • Files.walk (Path, FileVisitOption ...)
  • Files.find (Path, int BiPredicate, FileVisitOption ...)
  • Files.lines (Path, Charset)
  • DirectoryStream.stream ()
새로운 클래스의 UncheckedIOException은 RuntimeException을 확장한 IOException이다.
클로징 가능한 CloseableStream이 추가된것 또한 눈여겨 볼만 하다.

리플렉션과 어노테이션의 변경

어노테이션이 더 많은곳에서 사용될 수 있게 되었다. 예를들면, List<@Nullable String>과 같이 제네릭 형식 매게변수에 작성할 수도 있다. 따라서 정적 분석 도구에서 감지 가능한 오류의 범위가 확대되어 Java의 built-in type 시스템 또한 강화되고 정교해졌다.

Nashorn JavaScript엔진

Nashorn은 새로 JDK에 통합된 경량 고성능 JavaScript구현 엔진이다. Rhino의 후속이며, 성능과 메모리 관리가 개선되었다. javax.script API를 지원하고 있지만, DOM/CSS와 브라우저 플러그인API는 포함되어 있지 않다.

java.lang, java.util, 그리고 그 밖의것들

지금까지 언급한 것들 이외의 패키지에도 많은 추가 기능들이 있다. 몇가지 주목할만한 것들을 들어보자면, ThreadLocal.withInital(Supplier)는 보다 컴팩트한 thread 로컬 변수의 정의를 허용한다. 오랜 숙원이었던 StringJoiner과 String.join(...)가 자바8에서 구현되었다. Comparator는 체인 또는 필드기반의 비교를 가능하게 하는 새로운 방식을 제안하고 있다. String 풀의 기본값이 25~50K까지 확장되었다.

그분이 오신다. 긴장하라

눈치 빠른 독자들은 중간쯤에서 눈치를 채셨겠지만 자바8에서 추가된 중요 패러다임들은 대부분 멀티코어 프로세서상에서의 병렬처리 구현과 관련이 있다.

사실 자바 8의 모습에 대해서는 2011년 하반기에 어느 정도 윤곽이 확정되었으며 2013년5월8일 기능 사양이 동결되고 이후 모습을 드러낸 OpenJDK 8을 통해 많은 사람들이 준비해 오고 있었다. 기업용 어플리케이션의 경우 안정성을 중시하는 특성상 JavaEE8이 발표된 이후로도 얼마간 시간이 더 흘러야 주류로 정착될 것으로 보이긴 하지만 함수형 프로그래밍functional programming개념은 병렬처리 아키텍처나 코드 작성상의 간결함에 대한 매리트가 분명한 만큼 오픈소스 프로젝트를 중심으로 발빠르게 확산이 이루어지고 있으며, 자바4에서 5로 넘어가는 기간보다는 빠르게 확산될 것으로 보인다. 

이미 Tomcat과 Jetty는 자바 8을 지원하고 있으며 Spring및 Play와 같은 프레임워크도 자바8 정식 발표와 거의 동시에 자바8용 업데이트를 발표하고 있는 실정이다.

기존의 자바 개발자들은 긴장해야 한다. 멀티코어 개발에 대한 열망이 그 어느때보다 높은 현 시점에서 자바8이 가져올 변화는 예상보다 빠르고 치명적인 모습으로 우리앞에 나타날지도 모른다.