2015년 12월 10일 목요일

모던 자바의 역습(4) 모던 자바의 등장 - Java8

이번 포스팅은 김대우 님(http://lekdw.blogspot.kr/)과 함께 진행한 동명의 웨비너의 발표 내용에 바탕을 두고 작성되었습니다. 세상에 나온지 어느덧 20년. 오랜동안 프로그래밍 언어의 절대 강자로서 세상을 호령하던 자바를 둘러싼 진실 혹은 거짓말 그리고 과거와 미래에 대하여 알아봅니다.



스크롤의 압박을 피하기 위해 이번 포스팅은 다섯 파트로 나누어 연재합니다.


모던 자바의 역습 

4. 모던 자바의 등장 - Java 8


제네릭과 메타데이터 프로그래밍 패러다임이 도입된 자바 5 이후 거의 10년 만에 함수형 프로그래밍 패러다임을 들고 등장한 자바 8! C와 C++의 관계처럼 완전히 다른 언어는 아니지만 이전 버전과는 분명하게 선이 그어지는 프로그래밍 패러다임을 지닌 자바 8에 대해서 살펴보겠습니다.

함수형 프로그래밍은 왜 필요한가?

오랜 세월 대학원 연구실에서나 볼 수 있었던 함수형 프로그래밍 패러다임이 이처럼 주목 받게 된 것은 CPU의 멀티코어화가 일반화됨에 따라 더 간편하게 병렬처리를 구현할 수 있는 프로그래밍 방식이 필요해졌기 때문입니다.

함수형 프로그래밍 패러다임은 간단히 말해 처리 자체를 데이터와 완전히 분리하는 것으로, 함수를 다른 함수의 인자값으로 넘겨줌으로써 병렬 처리 구현이 매우 손쉽게 이루어 집니다. 이때, 병렬처리를 위해서 값은 스트리밍으로 처리될 필요성이 있으며, 이 스트리밍 처리와 관련된 API야 말로 자바 8의 핵심이라 할 수 있습니다.

 
아래 예제는 Collection.parallelStram()을 이용해 숫자 배열 중 짝수의 합을 구하는 프로그램 입니다.

List<Integer> integerList = Arrays.asList(new Integer[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
int sum = integerList
 .parallelStream()
 .filter(i -> i % 2 == 0)
 .mapToInt(i -> i)
 .sum();

만약 자바 8 이전 버전의 자바로 병렬 처리를 구현하려면 ExecutorService나 Fork/Join 프레임워크를 사용해야만 하는데, 이 경우 적어야 하는 코드양이 최소 네다섯 배는 많아집니다.

패러럴 스트림을 이용할 경우 이점은 코딩의 편리함 이외에, 속도에 있어서도 이득을 보는 경우가 있습니다. 아래 벤치마크 결과는 8코어 머신으로 세 가지 병렬 처리 모델을 실행 시켰을 때 속도를 측정한 것으로 16스레드로 움직였을 때 전체 테스트 중 가장 좋은 결과를 내고 있습니다.
ExecutorService vs. Fork/Join Framework vs. Parallel Streams
출처: http://blog.takipi.com/forkjoin-framework-vs-parallel-streams-vs-executorservice-the-ultimate-benchmark/

자바 8의 주요 신 기능

아래 내용은 캘리포니아에 있는 소프트웨어 개발사인 TechEmpower의 블로그에 올라온 ‘Everything about Java 8’의 일부를 발췌하여 정리한 것입니다.


인터페이스 개선

인터페이스에 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)을 사용한 인스턴스 생성이 있습니다.

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

  (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!"); }

메소드 참조

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

캡처 vs 비캡처 람다식

람다식의 외부에 정의된 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>과 같이 제네릭 형식 매개변수에 작성할 수도 있습니다. 따라서 정적 분석 도구에서 감지 가능한 오류의 범위가 확대되어 자바의 내장 자료형(built-in type) 시스템 또한 강화되고 정교해졌습니다.

Nashorn 자바스크립트 엔진

독일어로 코뿔소라는 뜻을 지닌 나스혼(Nashorn)은 새로 JDK에 통합된 경량 고성능 자바스크립트 구현 엔진입니다. 자바 7에 포함되었던 리노(Rhino)의 후속이며, 성능과 메모리 관리가 비약적으로 개선되었습니다. javax.script API를 지원하고 있지만, DOM/CSS와 브라우저 플러그인 API는 포함되어 있지 않습니다.

아래 그래프는 자바 6부터 탑재된 자바스크립트 엔진과 구글의 V8 엔진의 성능에 대한 벤치마크 결과입니다.





모던자바의 역습

  1. 프로그래밍 언어 자바
  2. 자바를 둘러싼 진실 혹은 거짓말
  3. 자바 코딩 스타일 변천사
  4. 모던 자바의 등장 - Java8
  5. 섹시한 자바 개발자로 거듭나기