Java 8에서 15까지 기능 빠르게 살펴 보기

시작하기

이 페이지에서는 Java 7부터 추가된 새로운 기능에 대해서 소개하며, 2020년 가을에 출시된 Java 15까지 Java 버전마다 큰 개선점을 살펴 보겠다. Java는 이제 람다식, 함수 프로그래밍, var에 의한 형식 추론, 간단한 생성자에 불변 컬렉션, 다중 문자열을 완벽하게 지원하게 되었다. 또한 데이터 클래스(record)와 sealed 클래스 등 새롭고 흥미로운 실험적인 기능도 있다. 그리고 시간 효율적으로 좋은 Java REPL에 대해 설명하겠다.

Java 8

함수형 프로그래밍

Java 8에서는 함수형 프로그래밍과 람다식을 언어 기능으로 추가되었다. 함수형 프로그래밍의 두 가지 핵심 패러다임은 불변의 값과 함수의 중요성 향상이다. 데이터 변환 처리는 파이프 라인을 통과한다. 각 단계에서 입력을 수신하여 새로운 출력에 매핑한다. 함수형 프로그래밍은 Java의 Stream와 Optional(null-safe monads)에서 사용할 수 있다.

람다식

public static void main(String[] args) {

    Runnable runner = () -> { System.out.println("Hello Lambda!"); };
    runner.run(); //Hello Lambda!
}

Streams

일반적인 컴퓨터 프로그램에서는 값 목록을 사용하여 각 값에 대해 빈번히 변환을 하는 일이 자주 발생한다. Java 8 이전에는 변환에 for 반복문을 사용해야 했지만 이제는 다음과 같이 Stream 을 사용할 수 있게 되었다.

Stream.of("hello", "great")
    .map(s -> s + " world")
    .forEach(System.out::println);
> hello world
> great world

map 함수는 Stream의 모든 요소에 적용되는 람다를 입력으로 사용한다.

Stream은 변환을 통해서 List, Set, Map으로 동작한다. Stream에 의해 코드의 루프를 거의 없앨 수 있다!

Optional

Java에서 자주 있는 발생하는 문제 중 하나는 Null Pointer Exception 이었다. 이에 Java는 Optional를 도입하였다. 이는 Null인 경우와 Null이 아닌 경우 참조를 감싸는 모나드(monad) 이다. Optional로 업데이트를 적용하려면 함수의 형태으로 수행할 수 있다.

Optional.of(new Random().nextInt(10))
    .filter(i -> i % 2 == 0)
    .map(i -> "number is even: " + i)
    .ifPresent(System.out::println);
number is even: 6

위의 예제에서는 난수를 생성하고 이를 Optional 객체로 래핑하여 짝수인 경우에만 값을 출력한다.

Date and Time API

LocalDate, LocalTime, LocalDateTime, DateTimeFormatter, ZonedDateTime 등 날짜와 관련된 편리한 클래스가 많이 추가되었다.

Java 9

JShell

드디어 Java에 REPLJShell 기능이 Java 9부터 도입되었다. 이 JShell를 사용하게 되면 Java의 완전한 클래스를 작성하여 컴파일하지 않고도 미리보기로 테스트할 수 있게 된 것이다. 즉, 한번에 하나의 명령을 실행하고 즉시 결과를 볼 수 있게 된 거다. 간단한 예는 아래와 같다.

$ <JDK>/bin/jshell
jshell> System.out.println("hello world")
hello world

JavaScript와 Python과 같은 인터프리터 언어에 익숙한 사람은 오랫동안 REPL을 즐겨 왔지만, Java에는 지금까지 이 기능이 없었다. JShell에서는 변수 뿐만 아니라 여러 줄의 함수, 클래스, 반복문 실행 등, 보다 복잡한 엔티티 정의할 수도 있다. 또한 자동 완성을 지원하고 있으며, 특정 Java 클래스가 제공하는 정확한 메소드에 대해 알아보고 싶은 경우에 유용하다.

불변(Immutable) 컬렉션의 팩토리 메소드

List의 간단한 초기화는 Java에서는 오랫동안 누락되었는데, 이제 드디어 추가되었다.이전에는 다음과 같이 해야만 했다.

jshell> List<Integer> list = Arrays.asList(1, 2, 3, 4)
list ==> [1, 2, 3, 4]

이렇게 하던 것이 다음과 같이 단순화되었다.

jshell> List<Integer> list = List.of(1, 2, 3, 4)
b ==> [1, 2, 3, 4]

이 멋진 of(...)메소드는 List , Set , Map에 포함되어 있다. 단, 한 줄로 간단한 코드 불변 객체를 생성할 수 있게 해준다.

Java 10

var의한 타입 추론

Java 10에서 변수의 형식을 생략할 수 있는 새로운 var 키워드가 추가되었다.

jshell> var x = new HashSet<String>()
x ==> []

jshell> x.add("apple")
$1 ==> true

위의 예에서는 컴파일러에 의해 x 타입은 HashSet라고 추측 할 수 있다.

이 기능은 보일러플레이트(boilerplate) 코드를 줄이고, 가독성을 향상시키는데 유용하다. 그런데, 몇 가지 제한 사항이 있다. var는 메소드 본문 내에서만 사용할 수 있으며, 컴파일러는 컴파일시에 타입를 추측하므로 모두 여전히 정적으로 타입이 지정된다.

보일러플레이트란? 컴퓨터 프로그래밍에서 보일러플레이트 또는 보일러플레이트 코드라고 부르는 것은 최소한의 변경으로 여러곳에서 재사용되며, 반복적으로 비슷한 형태를 띄는 코드를 말한다.

Java 11

단일 소스 파일 실행

이전에는 파일이 하나의 간단한 Java 프로그램을 작성하는 경우에는 먼저 해당 파일을 javac으로 컴파일하고 java에서 실행해야만 했다. Java 11에서는 하나의 명령으로 두 단계르 실행할 수 있다.

먼저, 하나의 소스 파일 Main.java을 작성한다.

public class Main {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

이제 하나의 단계에서 컴파일 및 실행를 한번에 할 수 있게 되었다.

$ java ./Main.java
hello world

간단한 스타터 프로그램이나 하나의 Java 클래스만 테스트를 하고 싶은 경우에 이 기능으로 단일 소스 파일을 시작하는 작업이 더 쉬워졌다.

Java 12

Switch 식

Java 12에서 Switch 표현식이라는 것이 등장하였다. 여기에서는 이 식이 이전에 switch 문과 어떻게 다른지를 살펴보도록 하겠다.

이전 switch 문 은 프로그램의 흐름을 정의한다.

jshell> var i = 3
jshell> String s;
jshell> switch(i) {
    ...>    case 1: s = "one"; break;
    ...>    case 2: s = "two"; break;
    ...>    case 3: s = "three"; break;
    ...>    default: s = "unknown number";
    ...> }
jshell> s
s ==> "three"

대조적으로, 새로운 Switch 식 값을 반환한다.

jshell> var i = 3;
jshell> var x = switch(i) {
    ...>    case 1 -> "one";
    ...>    case 2 -> "two";
    ...>    case 3 -> "three";
    ...>    default -> "unknown number";
    ...> };
x ==> "three"

이 새로운 switch 식 일종의 매핑 함수임을 주목하길 바란다. 하나의 입력(여기에서는 i)와 하나의 출력(여기에서는 x)가 있다. 이 기능은 사실 패턴 매칭 기능이며, Java를 함수형 프로그래밍의 원칙과 호환이 있는 것으로 하는데 유용한다. 이와 같은 switch문은 예전부터 Scala에서도 사용할 수 있었다.

주의해야 할 점이 몇 가지 있습니다.

  • 세미 콜론(;) 대신에 화살표 ->를 사용한다.
  • break이 필요하지 않는다.
  • 생각할 수 있는 모든 case를 고려되면, default는 생략할 수 있다.
  • Java 12에서는 이 기능을 사용하려면 --enable-preview -source 12을 사용한다.

Java 13

다중 문자열

JSON 또는 XML과 같은 긴 다중 문자열을 정의하는 경험이 있는가? 지금까지는 모두 하나의 행에 개행 문자 \n를 넣어야 했기에 문자열을 읽기 어렵게 되었다. 이를 불편함을 해결하기 위해 Java 13는 다중 문자열 기능이 도입되었다.

public class Main {
    public static void main(String [] args) {
        var s = """
            {
                "recipe": "watermelon smoothie",
                "duration": "10 mins",
                "items": ["watermelon", "lemon", "parsley"]
            }""";
        System.out.println(s);
    }
}

다만, 단일 파일 기동으로 Main 메소드를 실행해 보자.

java --enable-preview --source 13 Main.java
{
    "recipe": "watermelon smoothie",
    "duration": "10 mins",
    "items": ["watermelon", "lemon", "parsley"]
}

결과 문자열은 여러 줄에 되어 있고, 따옴표 ""는 그대로 유지되며, 탭 \t도 유지된다!

Java 14

데이터 클래스 record

이 페이지에서 새로운 기능 가운데, 가장 흥분하고있는 것은 아마 이것일 것이다. 마침내 Java에 데이터 클래스가 등장하였다. 이 클래스는 record 키워드로 선언된 자동 Getter, 생성자, equals 메소드 등이 있다. 즉, 방대한 보일러플레이트 코드의 덩어리를 제거 할 수 있게 되었다.

jshell> record Employee (String name, int age, String department) {}
| created record Employee

jshell> var x = new Employee("Anne", 25, "Legal");
x ==> Employee[name=Anne, age=25, department=Legal]

jshell> x.name()
$2 ==> "Anne"

Scala에 유사한 케이스 클래스가 있고, Kotlin에도 데이터 클래스가 있다. Java에서는 지금까지 많은 개발자들은 Lombok을 사용했었는데, Java 14의 record 기능에 영감을 주어 기능을 거의 제공하고 있다. 자세한 내용은 Baeldung을 참고하길 바란다.

캐스팅 없이 instanceof

instanceof 키워드는 Java의 이전 버전에서 이미 있었다.

Object obj = new String("hello");
if (obj instanceof String) {
    System.out.println("String length: " + ((String)obj).length());
}

여기서 아쉬운 부분은 먼저 obj 변수가 String 타입임을 확인하고, 다시 캐스팅하여 길이를 반환하는 부분이다.

Java 14에서는 컴파일러가 instanceof 체크 후에 자동으로 타입 추론하고 바로 사용할 수 있게 되었다.

Object obj = new String("hello");
if (obj instanceof String mystr) {
    System.out.println("String length: " + mystr.length());
}

Java 15

Sealed 클래스

sealed 키워드를 사용하면 특정 클래스 또는 인터페이스를 확장할 수 있는 클래스를 제한할 수 있다.

public sealed interface Fruit permits Apple, Pear {
    String getName();
}

public final class Apple implements Fruit {
    public String getName() { return "Apple"; }
}

public final class Pear implements Fruit {
    public String getName() { return "Pear"; }
}

이 소스에서 우리에게 어떤 도움이 주게 되는 걸까? Fruit이 몇 개 있는지 알게 되었다. 이는 완벽하게 지원된 패턴 매칭에 중요한 단계이다. 열거형처럼 클래스를 처리할 수 있게 된다. 이 sealed 기능은 앞에서 설명한 새로운 switch식과 잘 어울린다.

번외: Java 8 이상의 조건 업데이트

이 페이지의 마지막 주제는 라이선스이다. Oracle이 Java 8(무료 상용 버전) 업데이트를 중단 한 것을 들어 본 적이 있을 것이다. 옵션은 다음과 같다.

  • 새로운 Oracle JDK 버전을 사용한다(Oracle은 각 출시 후에 6개월 동안만 무료 보안 업데이트를 제공한다).
  • 자기 책임하에 이전 JDK 버전을 사용한다.
  • 이전 OpenJDK Java 버전을 사용한다. 이는 계속 오픈 소스 커뮤니티와 협력 업체를 통해 보안 업데이트를 받는다.
  • 프리미엄 지원 을위한 Oracle 지불 (예: Java 8은 2030 년까지 지원).

다음은 JDK 당 임시 Oracle 지원 기간이다.

JDK 출시일 다음까지 프리미어 지원 연장 지원
7 2011.07 2019.07 2020.07
8 2014.03 2019.07 2020.07
9 2017.09 2018.03 -
10 2018.07 2018.09 -
11(LTS) 2018.09 2023.09 2026.09
12 2019.03 2019.09 -
13 2019.09 2020.03 -
14 2020.03 2020.09 -
15 2020.09 2021.03 -
JDK별 Oracle 지원 예정

Oracle의 새로운 라이선스 모델은 새로운 릴리스 주기에 영향을 받는다. Oracle은 6개월마다 새로운 Java 버전을 발표한다. 이를 위해 Oracle은 빠른 속도로 Java를 개선하고 실험적인 기능을 통해 보다 신속한 피드백을 받고, Scala , Kotlin , Python 등과 같은 최신 언어를 따라 잡는데 도움이 된다.

라이선스에 대한 자세한 내용은 이 문서를 참고한다.

마무리

Java는 지난 6년 동안 먼 길을 걸어 왔고, 그 이후로 실제로 8개의 새로운 Java 릴리스가 있었다! 이 모든 멋진 새 기능은 Java를 다른 JVM 기반 경쟁자(Scala 및 Kotlin)에 비해 경쟁력 있는 옵션으로 만드는 데 도움이 된다.

참조