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. 섹시한 자바 개발자로 거듭나기

모던 자바의 역습(3) 자바 코딩 스타일 변천사

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


모던 자바의 역습 

3. 자바 코딩 스타일 변천사


자바는 20년이나 되는 역사속에서 꾸준히 발전을 거듭해온 만큼 언어 사양 자체의 변화에 따른 코딩 스타일의 변화를 겪어왔습니다. 구식 코딩 스타일은 당장 프로그래을 작동시키는 데에는 큰 문제가 없어보이나 가독성을 떨어트리거나 어떤 경우에는 버그의 원인이 되기도 하는 등 품질 저하의 요인으로 작용하기 때문에 코딩 규약 등에 포함시켜 프로젝트나 프로덕트 전체의 코딩 스타일을 최신 스타일로 유지할 필요가 있습니다.

이번 장에서는 아직까지도 쉽게 접할 수 있는 구식 코딩 스타일의 잔재를 확인하고 최신 코딩 스타일을 적용시키는 방법에 대해서 살펴보도록 하겠습니다.

파일을 행 단위로 읽어 리스트에 저장하는 간단한 프로그램을 예로 들어보겠습니다.

C 스타일 자바private List m_list=null; // null 초기화private int read_file(String str_le_name){ // 스네이크 케이스로 메소드명을 작성    String str_line; // 지역 변수를 함수 맨 윗부분에 몰아서 정의하고 있음    List list_lines=new ArrayList();    int i_result // 함수의 처리 결과를 숫자로 반환        =read_le(str_le_name,list_lines); // 결과값을 참조에 의한 호출을 이용해 넘겨줌      if(i_result==0){ //        List list_record=new ArrayList();        for(int i=0;i<list_lines.size();i++){            str_line=(String)list_lines.get(i);            Record record=new Record();            i_result=parse_line(str_line,record); // 결과값을 참조에 의한 호출을 이용해 넘겨줌            if(i_result!=0){                return i_result; //하위함수의 결과값이 0(정상종료)이 아닐경우 이를 그대로 반환            }            list_recordord.add(record);        }        m_list=list_record;        return 0; //정상 종료 시 0 반환    }else{        return i_result;    }}

C 스타일의 자바 코드는 자바 초창기에 흔히 보였지만 오늘날에도 원래 C 언어에서 넘어온 개발자들이 흔히 고수하는 스타일이기도 합니다. 구조적 프로그래밍 패러다임이 녹아 있는 C 스타일의 자바 코드는 가독성을 크게 해칠 뿐만 아니라 객체지향적 디자인 품질도 저하 시키기 때문에 자바 코딩 스타일로서는 매우 좋지 않은 형태라 할 수 있습니다.

자바1.4 스타일
private List resultList;private List readFile(String leName) throws SystemException { //예외를 이용한 에러 처리    List lines = readFile(fileName);    List recordList = new ArrayList();    for (int i = 0; i < lines.size(); i++) {        String line = (String) lines.get(i);        Record record = parseLine(line);        recordList.add(record);    }    return recordList; //값을 반환}

자바 1.4 스타일은 지금까지도 흔히 쓰일 정도로 가장 오랫동안 자바 코딩의 표준 스타일로 자리 잡았던 코드 스타일입니다. 방금 전에 살펴보았던 C 스타일 코드와 비교해보았을 때 가장 큰 차이점은 에러 처리에 있어서 결과값이 아닌 예외를 사용한다는 점입니다. 결과값 또한 처리 결과 얻어진 데이터를 반환하고 있으며, 참조에 의한 호출은 사용하지 않게 된 점을 볼 수 있습니다. 이 밖에 메소드명이나 변수명에 스네이크 케이스대신 단어의 첫 글자에 대문자를 사용하여 가독성을 높인 낙타표기법을 이용하는 것도 중요한 포인트 입니다.

자바 7 스타일
private List<Record> readFile(String leName)throws SystemException {    List<String> lines = readFile(fileName); //제네릭을 이용한 형 지정.    List<Record> recordList = new ArrayList();    for (String line : lines) {        Record record = parseLine(line);        recordList.add(record);    }    return recordList;}


자바 7의 경우 제네릭과 foreach가 적용되어 코드가 좀 더 심플해진 것을 알 수 있습니다.

try-with-resources

자바 7에 도입된 문법 중 아직도 많은 자바 프로그래머에게 생소한 try-with-resources에 대해서 간단히 살펴보겠습니다.

방금 전 살펴 보았던 파일 읽기 예제에 예외 처리를 추가하여 보겠습니다.

private List<String> readFile(String leName) {    List<String> lines = new ArrayList<String>();    BufferedReader reader = null;    try {        reader = new BufferedReader(new FileReader(fileName));        String line;        while ((line = reader.readLine()) != null) {            lines.add(line);        }    } catch (FileNotFoundException ex) {        throw new SystemException(FILE_NOT_FOUND, ex);    } catch (IOException ex) {        throw new SystemException(FILE_READ_ERROR, ex);    }    nally {        try {            if (reader != null) {                reader.close();            }        } catch (IOException ex) {            // 이건 무시?        }    }    return lines;}

자바 개발자라면 io 예외에 대해 어디까지 처리해줘야 하는지 한 번쯤은 고민해봤을 것 입니다. 자바 7의 경우 try-with-resources 구문을 통해 이러한 딜레마를 간단히 해결할 수 있습니다.

private List<String> readFile(String leName) {    List<String> lines = new ArrayList<>();    try (FileReader in = new FileReader(fileName);            BufferedReader reader = new BufferedReader(in)) {        String line;        while ((line = reader.readLine()) != null) {            lines.add(line);        }    } catch (FileNotFoundException ex) {        throw new SystemException(FILE_NOT_FOUND, ex);    } catch (IOException ex) {        throw new SystemException(FILE_READ_ERROR, ex);    }    return lines;}

위의 예제처럼 파일을 읽어들여 리스트 형태로 반환하는 것뿐이라면 자바 7의 NIO2에 새로 추가된 API인  File.readAllLines를 이용해 다음과 같이 간단히 처리할 수도 있습니다.

private List<String> readFile(String leName) {    try {        return Files.readAllLines(Paths.get(fileName), Charset.defaultCharset());    } catch (IOException ex) {        throw new SystemException(FILE_READ_ERROR, ex);    }}


새로운 코딩 스타일에 적응하기

사용하는 언어의 새 버전이 등장할 때마다 기대감에 설레이기도 하지만 한편으로는 새 스타일에 적응하는 것에 대한 부담감이 있는 것 또한 사실입니다. 필자의 경우 새 코딩 스타일에 적응하는 방법으로 다음과 같은 원칙을 세워놓고 있습니다.
  • 프로젝트 도중에는 언어의 버전이 바뀐다 하더라도 특별한 경우가 아닌 이상 코딩 표준은 변경하지 않는다.
  • 새로운 코딩 스타일의 적용은 개개인이 실시하는 것이 아닌 코딩 표준(coding conventions)에 먼저 적용한 후에 팀이나 프로젝트 전체에 일괄적으로 적용시킨다.
  • 코딩 스타일의 준수 여부는 가급적 Checkstyle이나 SonarQube와 같은 정적 해석툴을 이용해 기계적으로 실시할 수 있도록 하며, 정적 해석툴이 지원하지 못하는 범위는 코드 리뷰를 통해 준수 여부를 확인한다.