Java 8 개선 사항 관련 글 모음

완벽한 설계에 이르렀다 함은,
더할 것이 없을 때가 아닌,
뺄 것이 없을 때를 말한다.
– 앙투안 드 생텍쥐페리

모 든 기술은 세 단계를 거친다. 처음엔 조잡하게 단순하고 매우 불만족한 기계, 두번째는 매우 복잡한 조율을 거쳐 원형의 결점을 극복하고 그로인해 어느정도 만족스러운 성능을 내도록 설계된 터무니없이 복잡한 기계 뭉치, 세번째는 거기에서 나온 궁극의 타당한 설계.
– 로버트 A 하인라인

이 단순성과 적절성을 강조하는 두 명언은 1996년 5월 제임스 고슬링과 헨리 맥길턴이 작성한 백서, 자바 언어 환경(The Java Language Environment)에서 자바 언어의 특징을 강조하면서 인용되었습니다. 자바는 처음부터 뺄 것이 많아 불완전하고 복잡한 2단계 기계인 C++애서 친근함은 유지하면서 불필요한 복잡성은 제거하는 것을 목표로 개발되었습니다. 그리고 단순(완벽)을 추구했던 만큼 여러 버전을 거치면서 대부분 SDK가 바뀌고 JVM이 개선되었을 뿐 언어 자체에는 별다른 변화가 없었습니다. 이런 자바가 지금까지 언어 측면에서 두 번 큰 변화를 겪었는데 첫 변화가 자바 5였고 그다음이 이번에 출시된 자바 8입니다. (자바 7에서도 언어가 여러 가지로 개선되었지만 큰 주목을 받지는 못했습니다.)

사실 자바 8은 람다식에 대한 논의가 정리되지 않아서 자바 7 출시 일정이 계속 연기되자 자바 7 출시를 미루자는 안(Plan A)과 람다식과 몇 가지 쟁점을 제외한 상태로 자바 7을 출시한 후 곧이어 다음 버전을 출시하자는 안(Plan B)을 가지고 토론해 후자(Plan B)로 결정되었고 그 자바 7에서 빠졌던 Plan B의 두 번째 부분을 중심으로 출시한 것이라서 사람들은 관심은 람다식에 집중되어 있었습니다. 물론 자바 8의 최대 변경 사항은 람다식이 확실합니다. 하지만 그 외에도 (애초 계획과 달리) 워낙 광범위하고 대규모로 개선되었기에 자바 8의 변경 사항만 설명해도 책 한 권은 될 것 같습니다. (그런데 그것이 실제로 일어났습니다. 이 책은 정말 요점만 정리했을 뿐인데 240페이지나 됩니다.)

자바 8의 모든 개선 사항을 자세히 다루기는 힘들고 람다만 다루더라도 많은 분량이 될 것이니 “구글링 대신해주는 남자” 컨셉으로 구글의 여러 관련 글을 한번 모아 보았습니다. (참고로 이 글은 사내 공유용으로 작성했던 글을 조금 다듬었습니다.)

일단 자바 8의 특징 중 (제 생각에) 주목할만한 것들만 나열하면 다음과 같습니다.

  • 람다식(Lambda Expression)
    • 스트림(Stream) API
    • 기본 메서드(Default Method)
  • 새 자바스크립트 엔진, 나즈혼(Nashorn)
  • Joda Time 방식의 새 날짜 API 변경 (JSR 310)
  • 자바 FX 8
  • 메타 데이터 지원 보완
  • 동시성 API 개선
  • IO/NIO 확장
  • Heap에서 영속 세대(Permanent Generation) 제거

그럼 하나씩 건드려 보겠습니다.

람다식(Lambda Expression)

그렇습니다. 가장 먼저는 람다입니다.

간단한 코드를 보겠습니다. MyClass라는 객체의 리스트를 value 프로퍼티에 따라 역순으로 정렬하는 자바 코드입니다. 역순으로 정렬하려고 Comparator 인터페이스를 익명 클래스로 구현해 전달했습니다. 자바는 순수 OOP를 표방한 언어로서 추상화를 클래스 상속이나 객체 위임으로 처리하는 편입니다. 하지만 Collections의 sort(…) 메서드는 비교하는 알고리듬을 호출하는 쪽에서 인자로 전달받아 사용합니다. 굳이 따지자면 이 방법도 객체 위임이라고 볼 수 있으나 일반적인 객체 위임인 전략 패턴(Strategy Pattern)보다 간단합니다. 물론 클래스 상속보다도 번거롭지 않습니다. 익명 클래스를 인라인으로 사용했으므로 더욱 간단해졌습니다.

Collections.sort(theListOfMyClasses, new Comparator<MyClass>() {
    public int compare(MyClass a, MyClass b) {
        return b.getValue() - a.getValue();
    }
});

하지만 sort(…) 메서드가 정말 필요로 하는 코드는 return b.getValue() – a.getValue() 뿐인데 이 코드 한 줄을 인자로 넘기려고 주절주절 클래스를 만들어야 합니다. 한 줄 뿐인 로직을 객체에 담아 넘기는 이유는 자바가 기본(Primitive) 타입이나 객체만 메서드의 인자로 넘길 수 있기 때문입니다. 익명 클래스가 다른 클래스 보다는 덜 번거롭지만 이렇게 간단한 로직도 반드시 클래스 안에 두어야 한다는 점은 자바의 결함이나 세련되지 못한 면으로 지적되었습니다.

람다 덕에 자바 8에서는 이를 더 간단히 표현할 수 있게 되었습니다. 다음 코드를 보면, 조금 생소하기는 하지만, 꼭 필요한 부분만 남았습니다.

theListOfMyClasses.sort((MyClass a, MyClass b) -> {
    return a.getValue() - b.getValue();
});

이전과 달리 클래스를 정의하고 객체를 생성해서 인자로 넘기지 않고 마치 꼭 필요한 로직만 전달하는 것처럼 보입니다. 이렇게 클래스 없이 메서드 정의 수준으로 인자로 전달할 로직을 표현할 수 있는 표기법을 람다식 또는 람다표현이라고 말합니다. 조금 더 자바에 어울리는 익명 메서드란 용어도 사용 가능합니다. 다른 언어에서는 조금씩 개념을 달리하면서 클로저, 블럭 등으로 부르기도 합니다.

자바 8의 람다식 문법은 이렇습니다.

람다 매개변수  ->  람다 본문

위 예와 비교해서 보면 어떤 구조인지 쉽게 파악할 수 있습니다.

람다식은 여러 방편으로 간략하게 표현하는 장치들을 제공하는데 위 코드는 사실 이렇게 약식으로 표현할 수 있습니다.

theListOfMyClasses.sort((a, b) -> a.getValue() - b.getValue());

이렇게 약식으로 표현할 수 있는 이유는 람다식이 매개변수의 타입을 추론할 수 있고, 간략한 코드는 (자바의 if, for 문 등 처럼) 블럭을 생략할 수 있을 뿐 아니라 return 구문도 제거 가능하기 때문이다. 심지어 매개변수가 하나 뿐이라면 괄호도 생략 가능합니다. 다음은 규약서에 있는 다양한 람다식의 예입니다.

() -> {}                     // No parameters; result is void
() -> 42                     // No parameters, expression body
() -> null                   // No parameters, expression body
() -> { return 42; }         // No parameters, block body with return
() -> { System.gc(); }       // No parameters, void block body
() -> {
  if (true) return 12;
  else {
    int result = 15;
    for (int i = 1; i < 10; i++)
      result *= i;
    return result;
  }
}                          // Complex block body with returns
(int x) -> x+1             // Single declared-type parameter
(int x) -> { return x+1; } // Single declared-type parameter
(x) -> x+1                 // Single inferred-type parameter
x -> x+1                   // Parens optional for single inferred-type case
(String s) -> s.length()   // Single declared-type parameter
(Thread t) -> { t.start(); } // Single declared-type parameter
s -> s.length()              // Single inferred-type parameter
t -> { t.start(); }          // Single inferred-type parameter
(int x, int y) -> x+y      // Multiple declared-type parameters
(x,y) -> x+y               // Multiple inferred-type parameters
(final int x) -> x+1       // Modified declared-type parameter
(x, final y) -> x+y        // Illegal: can't modify inferred-type parameters
(x, int y) -> x+y          // Illegal: can't mix inferred and declared types

람다식은 마치 메서드를 메서드의 인자로 넘기는 것처럼 보이게 해주는데, 사실은 컴파일러가 람다식으로 표현된 로직을 자동으로 익명 클래스로 바꾸어 전달하므로 받는 측에서는 특정 인터페이스의 구현 객체를 넘겨받게 됩니다. 자바 8에서는 예제의 Comparator같이 메서드가 하나뿐인 인터페이스를 특별히 함수형 인터페이스(Functional Interface)라고 구분해 부르며, 람다식으로 표현된 로직을 이 함수형 인터페이스로 자동으로 변환합니다.

Comparator 나 Observer나 Callable같이 자바 8 이전부터 있었거나 직접 만든, 메서드가 하나인 인터페이스도 함수형 인터페이스로 유효하며, 이 외에도 자바 8에서는 범용으로 쓸 수 있는 다양한 함수형 인터페이스를 java.util.function 패키지 안에 준비 두었습니다.

람다와 관련된 글들은 다음과 같습니다.

스트림(Stream)

자 바 8은 단순히 람다를 도입할 뿐 아니라 람다식의 효과적인 사용 방법을 안내하고 촉진할 수 있도록 기존 API에 람다를 대폭 적용했습니다. 그 대표적인 인터페이스가 Stream입니다. Stream 인터페이스는 컬랙션(Collection)을 다루는 새로운 방법을 제공합니다. 스트림은 컬랙션을 파이프식으로 처리하도록 하면서 고차함수로 그 구조를 추상화합니다. 그래서 지연 연산이나 병렬 처리 등이 동일 인터페이스로 제공됩니다. 흔히 이런 방식의 API를 함수형이라고 부릅니다.

기본 메서드(Default Method)

자바는 인터페이스에 메서드를 추가하면 하위 호환성이 깨지기 때문에 해당 인터페이스를 구현한 모든 클래스에 새로 추가된 메서드를 구현하고 이 인터페이스를 사용하는 모든 코드가 새로 컴파일해야 합니다. 이런 문제 때문에 인터페이스 개선과 관련된 여러 가지 의견이 혼재했습니다. 자바 8에서는 기본 메서드란 개념이 추가되어 하위호환성 문제없이 인터페이스에 새로운 메서드를 추가할 수 있게 되었습니다. 기본 메서드란 인터페이스에 구현된 메서드로서 인터페이스를 구현하는 클래스가 오버라이드 하지 않을 경우 기본 구현물로 적용됩니다. 초기에는 가상 확장 메서드(Virtual Extension Methods) 또는 방어 메서드(defender methods)로 부르기도 해서 혼용되기도 합니다.

  • The Java Tutorials: Default Methods: 자바 공식 튜토리얼의 기본 메서드 설명으로 매우 충실한 편입니다.
  • Java 8 explained: Default Methods: 자바 8에 람다가 도입되면서 기본 메서드(또는 가상 확장 메서드)가 도입될 수밖에 없었던 이유를 설명하면서 기본 메서드의 특징과 작동 방식을 살펴봅니다. 실제로 기본 메서드는 람다 프로젝트의 일부로 진행된 주요 개선된 사항 중 하나입니다.
  • Handy New Map Default Methods in JDK 8: 기본 메서드를 활용 사례로서 자바 8에서 Map 인터페이스에 추가된 메서드들을 소개합니다. 자바 SDK의 API는 그동안 수정하지 않는 것이 원칙이었는데 자바 8에는 여러 인터페이스에 새로운 메서드가 추가되었습니다.
  • Java 8 Interface Changes – static methods, default methods, functional Interfaces: 자바 8에서 인터페이스의 변경 사항만 간단히 정리했습니다. 기본 메서드와 함께 자바 8에서는 정적 메서드도 정의할 수 있게 되었습니다. 이로써 정적 메서드를 보다 폭넓게 활용할 수 있게 되었습니다.

나즈혼(Nashorn): 자바스크립트 엔진

지 금까지는 모질라의 리노(Rhino)가 자바의 기본 자바스크립트 엔진으로 배포되었습니다. 리노는 훌륭한 자바스크립트 엔진이었지만 너무 오래된 프로젝트라서 최신의 자바 개선 사항을 십분 활용하지 못하는 문제가 있었습니다. 나즈혼은 최근 JVM을 다언어 지원 가상 기계로 개선하면서 도입된 기능을 충분히 활용하도록 개발되었습니다.

새 날짜 API

자 바를 처음 공부할 때, 정말 황당하게 생각되었던 클래스가 Date였습니다. 그리고 그 후에 대체된 Calendar도 그리 정이 가지는 않았습니다. API 스타일 외에도 기존 자바의 날짜 API는 많은 문제를 가지고 있었습니다. 그래서 Joda Time 이라는 별도 라이브러리가 대안으로 많이 사용되었습니다. 이번 자바 8에 Joda Time이 통합된다는 소문이 있었지만, 최종적으로는 Joda Time을 참고한 새 API를 만드는 형태로 진행되었습니다.

자바FX 8

자 바FX 1.0은 명백히 RIA 시장의 선두면서 데스크톱까지 진출하려고 하던 플래시의 대안으로 소개되었습니다. 하지만 이미 시장은 플래시에 점령되었고 자바라는 이름을 가지고 있지만 새로운 언어를 배워야 했기에 시장의 반응은 싸늘했습니다. 자바FX 2.0은 1.0의 교훈을 발판삼아 자바로 프로그래밍할 수 있도록 API를 제공했지만, 사람들은 온통 HTML5만 얘기할 뿐 자바FX가 목표로 했던 플래시조차 사람들의 관심 밖으로 밀려난 상태였습니다. 그런데 자바 8에서 오라클은 모험을 선택했습니다. 모두 자바FX를 버릴 것으로 생각했는데 오라클은 오히려 스윙을 버리고 자바FX를 공식 자바 GUI 개발 기술로 제안합니다. 데스크톱과 모바일 기기를 통합하는 GUI 기술이 대세이기는 하지만 이 시도가 얼마나 효과를 얻을지 모르겠습니다. 오라클은 자바 5에서 공들였던 스윙을 버릴 정도로 자신 있는 모양입니다. 사실 스윙은 최신 GUI 경향을 반영하기엔 구식이 분명합니다.

메타 데이터 지원 보완

각종 프레임워크가 발달하면서 메타 프로그래밍이 개발 편의성과 생산성에 미치는 영향이 커지고 있는데 자바 8에서는 이를 위한 메타 데이터 지원 기능이 보강되었습니다. Devoxx 2012에서 발표된 Annotation Features in JDK 8은 메타 데이터 중 어노테이션과 관련된 부분을 깊이 있게 다룹니다.

자바 타입 어노테이션(JSR 308)

자 바 8에서는 어노테이션을 달 수 있는 대상이 대폭 넓어졌는데 그중에 타입도 해당합니다. 여러 타입을 지정하는 곳에 어노테이션을 달아서 자바의 기본 타입 시스템이 제공하지 않는 정보를 추가할 수 있게 되었습니다. 그리고 이와 함께 타입 확인 프레임워크를 통해 어노테이션으로 덧붙인 정보를 컴파일러가 처리할 수 있도록 확장할 수 있게 되었습니다. 자바 타입 어노테이션은 JSR 308로 진행되었고 자바 5에서 지네릭스로 강화된 자바의 타입 시스템을 한층 보완하기 위해 2006년부터 진행되었습니다.

JSR 308은 지넥릭스와 함께 자바를 너무 복잡하게 만든다고 비난받던 개선 프로젝트였습니다. 앞으로 얼마나 널리 사용될지 모르겠지만, 관심 있는 분은 OTN의 JSR 308 Explained: Java Type Annotations를 읽어 보도록 하십시오. 타업 어노테이션과 타입 확인 프로세서 사용 방법을 예제와 함께 설명합니다.

JSR 308과 관련되어, 자바에 범자연수(0을 포함한 자연수), 즉 unsigned integer 지원 기능이 추가된다는 소식이 사람들의 이목을 끌었습니다. 정말로 해당 API가 자바 8에 추가되기는 했지만 아쉽게 JSR 308을 통한 범자연수 지원은 공식으로 자바 8에 포함되지는 못했습니다. Subtle Changes in Java 8: Repeatable Annotations에서는 아주 간단한 소개와 공식적인 참고 문헌을 접할 수 있습니다.

어노테이션 중복 지정

단일 언어 요소에 동일 어노테이션을 하나 이상 지정할 수 없는 제약은 자바 어노테이션의 문제로 지목되던 것 중 하나입니다. 자바 8에서는 이 제약이 풀렸습니다. 공식 튜토리얼에 사용 방법과 중복 지정 가능한 커스텀 어노테이션 제작법이 친절하게 설명되어 있습니다. Java 8 Features: Discover Repeating Annotations는 핵심만 요약해서 소개합니다.

매개변수 메타데이터 리플랙션 지원

자 바는 컴파일 후에도 많은 메타 데이터 정보를 바이트 코드에 남기지만 메서드의 매개변수 이름을 지우기 때문에 자바 리플랙션으로 메서드의 타입만 확인할 뿐 이름은 알 수 없었습니다. 따라서 메서드의 매개변수와 외부 값을 사상하는 경우 어노테이션에 일일이 대응하는 값의 이름을 지정했어야 합니다. 예를 들어 스프링 MVC의 @RequestParam의 경우 다음과 같이 해야 합니다. 외부에서 전달되는 값의 이름과 매개변수의 이름이 같더라도 일일이 어노테이션에 이름을 지정해야 합니다. (자바 컴파일러에 -debug 옵션을 주어 바이트 코드에 디버깅용 소스를 남기면 이 소스 코드를 활용해서 자동으로 매개변수 이름을 인식하는 기능이 스프링에 있었기는 하지만 리플랙션으로는 매개변수 이름을 알 수 없었습니다.)

public String foo(@RequestParam("name") String name, @RequestParam("file") MultipartFile file) { ... }

자바 8에서는 다음과 같이 생략이 가능합니다.

public String foo(@RequestParam String name, @RequestParam MultipartFile file) { ... }

자세한 정보는 자바 튜토리얼의 Obtaining Names of Method Parameters 부분을 참고하십시오.

기타

지금부터는 상대적으로 자잘하지만 놓치기 아까운 부분을 소개하겠습니다.

동시성 API 개선

무어의 법칙이 깨지고 멀티코어 CPU 시대로 접어들자 자바도 자바 5 이후로 동시성과 관련해서 꾸준히 기능을 개선하고 있습니다. 이번에도 여러 유용한 기능을 추가했습니다.

먼 저는 자바 5 이후 전통적으로 가장 인기 있던 HashMap의 자리를 차지한 ConcurrentHaspMap에 새로운 메서드를 추가해 사용성을 개선했습니다. 람다와 스트림 방식이 광범위하게 적용되었고, 그 외에도 몇 가지 편의 메서드로 캐시로 쓰기 좋게 개선되었는데 Caching with ConcurrentHashMap and computeIfAbsent에서 그 한 예를 볼 수 있습니다.

또 한 개선점은 계수나 누산 같은 작업을 원자적으로 할 수 있도록 돕는 클래스가 java.util.concurrent.atomic에 추가되었습니다. Java 8 Performance Improvements: LongAdder vs AtomicLong에서 새로 추가된 LongAdder가 기존의 AtomicLong에 비해 얼마나 빠른지 확인할 수 있습니다. Java 8 Concurrency: LongAdder는 LongAdder의 구현 방식을 살펴보며 이토록 빠른 이유를 설명합니다. 5 Features In Java 8 That WILL Change How You Code에서는 새로 추가된 LongAccumulator와 DoubleAccumulator가 코드를 바꿀 다섯 가지 자바 8의 개선 사항 중 하나로 꼽혔습니다.

다음은 ForkJoinPool입니다. 멀티 코어 대응 ExecutorService 구현인 ForkJoinPool은 자바 7의 스타 중 하나입니다. 이번엔 commonPool()이란 정적 메서드가 추가되어 특별히 ForJoinPool을 따로 생성하지 않고 공통으로 사용하는 ForkJoinPool을 얻을 수 있게 되었습니다. 성능도 개선되었는데 Is Java 8 the fastest JVM ever? Performance benchmarking of Fork-Join은 7과 8의 ForkJoinPool 성능을 비교합니다.

자바 8에서는 내부적으로 사용하려고 새로 Lock 객체를 만들기로 했습니다. 기존 ReadWriteLock가 기대와 달리 너무 성능이 느리자 이를 개선한 StampledLock을 새로 만들었습니다. StampedLock은 그 자체로 속도가 빠를 뿐 아니라 낙관적 락(Optimistic Lock)을 제공해 더욱 빠르게 동작합니다. 자바원의 Phaser and StampedLock Concurrency Synchronizers (발표자료) 세션에서 기존 락의 문제와 StampedLock에 대해서 자세히 설명합니다.

자바 8 전반의 동시성 측면 개선점은 JVM concurrency: Java 8 concurrency basics을 참고하십시오.

IO/NIO 확장

Files에 스트림을 반환하는 메서드가 몇가지 추가되어 디렉터리나 파일을 편하게 탐색하거나 읽을 수 있습니다. Java 8 Friday Goodies: The New New I/O APIs에서  간단한 예제와 설명을 읽을 수 있습니다.

그리고 드디어! Base64 인코딩과 디코딩을 자바 표준 API로 할 수 있게 되었습니다. Base64 Encoding in Java 8에 API 사용 방법을 설명합니다.

영속 세대(Permanent Generation) 제거

그 동안 java.lang.OutOfMemoryError: PermGen 오류의 원흉이던 영속 세대가 자바 힙에서 사라졌습니다. 지금까지 힙의 이 영역은 기본적으로 GC가 되지 않으며 클래스 메타 데이터를 저장하는 용도로 사용되었습니다. 점점 자바 프로젝트에 사용되는 프레임워크와 라이브러리의 수와 규모가 커지기도 하고 최근엔 동적 클래스 생성 기법도 광범위하게 사용되어 영속 세대가 부족해 오류가 발생하는 일이 빈번했습니다. Java 8: From PermGen to Metaspace는 영속 세대에서 메타스페이스로 옮긴 내용과 둘 간의 비교를 정리했습니다.

추가 참고 자료

기사/블로그 글

  • Java 8 Lambdas (사파리 온라인): 자바 8의 람다를 적절한 예제와 함께 잘 설명했습니다. 람다 자체뿐 아니라 람다로 인해 바뀐 Collection, 동시성 처리와 테스트, 디버깅, 설계 기법까지 다룹니다.
  • Java 8 in Action (MEAP): 자바 8을 설명한 책인데 아직 작성 중이고 50% 정도 완성된 것 같습니다. 목차를 보면 급하게 작성하는 것 같아 별 기대는 안 됩니다.
  • Java SE 8 for the Really Impatient : 자바 8의 변경사항 전체를 간단히 요약했습니다. 이미 번역본까지 나왔으나 정말 담백하게 요약만 했기 때문에 설명이 불친절하고 이해하기 어렵습니다.