프로그래밍 언어/[JAVA]

[JAVA] 새로운 Java LTS버전, Java 21의 특징들에 대해 알아보자

연구소장 J 2024. 5. 15. 18:42

새로운 LTS 버전 Java 21의 등장

Jrebel의 2023 Java Developer Productivity Report에 따르면 31%는 Java 8을 사용하고, 28%가 Java 11을, 그리고 19%가 다른 버전의 Java를 사용한다고 한다.

 

이런 상황 속 2023년 9월 19일 새로운 JAVA LTS 버전인 Java 21이 출시되었다.

 

Java의 최신 버전을 적용하는 것은 아직 쉽지 않을지 모르지만 Java 21은 적용을 고려해 볼 만큼 좋은 발전들이 존재한다.

 

Java는 언어의 발전을 위해 JEP(Java development Enhancement Proposal)를 사용해 새로운 기능들을 추가한다.

JEP는 말 그대로 JDK를 향상시키기 위한 제안으로 새로운 기능을 Java에 추가할 수 있는 방법이다.

이때 JEP는 다음과 같이 3가지 종류로 나뉜다.

 

  • Incubator JEP : 아직 최종적으로 확정되지 않았으며 사용할 때 risk가 있는 JEP. 새로운 기능을 테스트하기 위함.
  • Preview JEP : 설계 및 구현이 완료되었지만 아직 영구적으로 적용되지는 않은 JEP. 향후 변경되거나 제거될 수 있다.
  • Permanent JEP : 공식적으로 Java 언어에 적용된 JEP. (※ 공식 용어는 아님. Incubator와 Preview를 제외한 JEP를 지칭하기 위해 Permanent JEP라고 부름)

 

Java 21에는 아래와 같은 15개의 JEP들이 존재한다.

 

  • Incubator JEP
    • JEP 448 : Vector API
  • Preview JEP
    • JEP 430 : String Templates
    • JEP 442 : Foreign Function & Memory API
    • JEP 443 : Unnamed Patterns and Variables
    • JEP 445 : Unnamed Classes and Instance Main Methods
    • JEP 446 : Scoped Values
    • JEP 453 : Structured Concurrency
  • Permanent JEP
    • JEP 431 : Sequenced Collections
    • JEP 439 : Generational ZGC
    • JEP 440 : Record Patterns
    • JEP 441 : Pattern Matching for switch
    • JEP 444 : Virtual Threads
    • JEP 449 : Deprecate the Windows 32-bit x86 Port for Removal
    • JEP 451 : Prepare to Disallow the Dynamic Loading of Agents
    • JEP 452 : Key Encapsulation Mechanism API

 

Incubator JEP와 Preview JEP는 언제든 변경 될 수 있고 사용에 risk가 있기 때문에 당장 프로젝트에 적용시키는 데에는 위험을 감수해야 한다. 

 

본문에서는 Incubator JEP와 Preview JEP를 제외한 Permanent JEP에 대해서만 설명하겠다.

다른 JEP도 궁금하다면 아래 문서를 참고하자.

 

https://openjdk.org/projects/jdk/21/

 

JDK 21

JDK 21 This release is the Reference Implementation of version 21 of the Java SE Platform, as specified by JSR 396 in the Java Community Process. JDK 21 reached General Availability on 19 September 2023. Production-ready binaries under the GPL are avai

openjdk.org

 

 

JEP 431 : Sequenced Collections

JEP 431는 첫 원소와 마지막 원소에 쉽게 접근할 수 있는 Sequenced Collection 들을 제공한다.

 

이전에는 Collection에서 첫 원소와 마지막 원소에 접근하려면 아래와 같이 해야만 했다.

  첫 원소 마지막 원소
List list.get(0) list.get(list.size()-1)
Deque deque.getFirst() deque.getLast()
SortedSet sortedSet.first() sortedSet.last()
LinkedHashSet linkedHashSet.iterator().next() next()로 끝까지 순회

 

Collection의 종류마다 첫 원소와 마지막 원소를 접근하는 방식이 다르고 심지어 LinkedHashSet의 경우는 마지막 원소에 접근하는 방법이 없어 iterator().next()로 처음부터 끝까지 모든 원소를 순회해야 한다.

 

 

 

 

 

SequencedCollection

interface SequencedCollection<E> extends Collection<E> {
    // new method
    SequencedCollection<E> reversed();
    // methods promoted from Deque
    void addFirst(E);
    void addLast(E);
    E getFirst();
    E getLast();
    E removeFirst();
    E removeLast();
}

 

반면 SequencedCollection은 위와 같이 첫 번째 원소와 마지막 원소를 접근하고 추가, 제거하는 메서드를 규격화시켜놓았다. 또한 reversed() 메서드를 통해 역순으로 원소들을 호출할 수도 있다.

 

SequencedSet

interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
    SequencedSet<E> reversed();    // covariant override
}

 

SequencedSet은 SequencedCollection을 상속받으며 중복 원소를 갖지 않는 Collection이다.

 

이때 SortedSet은 원소들이 상대적인 비교로 정렬되기 때문에 addFirst()나 addLast()와 같은 메서드를 사용할 수 없다. 만약 이를 실행하면 UnsupportedOperationException 예외가 발생한다.

 

그 외 LinkedHashSet 등의 경우는 addFirst()등을 사용할 수 있다. openJDK 문서에 따르면 LinkedHashSet에 삽입하려는 원소가 이미 set에 있으면 해당 원소가 적절한 위치로 움직이기 때문에 원소의 재배치가 불가능했던 기존 LinkedHashSet의 문제점을 해결했다고 한다.

===> 이 부분은 사실 잘 이해가 가지 않는다. Set은 중복 원소를 허용하지 않으니 애초에 중복 원소를 삽입하면 재배치가 이루어지지 않는 것 아닌지...? 혹시 아시는 분 있으면 알려주세요 ㅠㅠ

 

SequencedMap

interface SequencedMap<K,V> extends Map<K,V> {
    // new methods
    SequencedMap<K,V> reversed();
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    V putFirst(K, V);
    V putLast(K, V);
    // methods promoted from NavigableMap
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
}

 

SequencedMap은 putFirst(), putLast()와 같은 메서드가 추가되었다.

SequencedSet과 비슷하게 SortedMap에서는 putFirst()  등을 사용하면 UnsupportedOperationException을 throw 하고,

LinkedHashMap에서는 사용이 가능하다.

 

 

 

JEP 439 : Generational ZGC

JAVA 15부터는 ZGC(Z Garbage Collector)가 Java의 기본 Garbage Collector로 채택되었다. 

 

Generational ZGC는 이러한 ZGC를 향상한 버전이다. 

 

간단하게 설명하자면 young 객체(금방 수집되는 객체)와 old객체(꽤 오래 살아남는 객체)를 세대로 구분 짓고 young 객체를 old 객체보다 더 자주 수집하도록 한다.

 

Generational ZGC는 기존 ZGC보다 성능이 더 뛰어나며, 곧 Java의 기본 Garbage Collector로 채택될 가능성이 매우 크다.

 

Generational ZGC에 대해 설명하려면 본문보다도 길어질 수 있으니 기존 ZGC보다도 성능이 더 향상된 ZGC라고 이해하면 될 것 같다.

 

더 자세한 내용은 아래 문서를 살펴보자.

 

https://openjdk.org/jeps/439

 

JEP 439: Generational ZGC

JEP 439: Generational ZGC OwnerStefan KarlssonTypeFeatureScopeImplementationStatusClosed / DeliveredRelease21Componenthotspot / gcDiscussionhotspot dash gc dash dev at openjdk dot orgEffortXLDurationXLRelates toJEP 377: ZGC: A Scalable Low-Latency

openjdk.org

 

JEP 440 : Record Patterns

//  Java 16 이전
if (obj instanceof String) {
    String s = (String)obj;
    ... use s ...
}

//  Java 16 이후
if (obj instanceof String s) {
    ... use s ...
}

 

Java 16 때 instanceof 연산자로 pattern matching을 할 수 있도록 변경되었다. 

위를 예시로 들자면 객체 obj가 String이라면 String으로 타입을 변환해 s에 할당해 주는 것이다.

이를 type pattern이라고 한다.

 

record pattern은 이와 비슷하게 Record(JEP 395 참고)를 component로 분해해 바로 사용할 수 있게 해 준다.

// Java 16
record Point(int x, int y) {}

static void printSum(Object obj) {
    if (obj instanceof Point p) {
        int x = p.x();
        int y = p.y();
        System.out.println(x+y);
    }
}

 

예를 들어 Java 16에서 record를 사용하려면 위와 같이 obj가 record인지 체크한 후 사용해야 했다.

// Java 21
static void printSum(Object obj) {
    if (obj instanceof Point(int x, int y)) {
        System.out.println(x+y);
    }
}

 

하지만 Java 21에서는 record pattern을 사용해 위와 같이 바로 record를 분해해 사용할 수 있다.

 

이 Record Patterns는 JEP 441 Pattern Matching for switch와 강력한 시너지 효과가 있다.

 

 

JEP 441 : Pattern Matching for switch

JEP 440에서 Pattern Matching에 대해 알아봤다.

JEP 441은 swtich 문에 Pattern Matching을 적용시킨 것이다.

 

// Java 21 이전
static String formatter(Object obj) {
    String formatted = "unknown";
    if (obj instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (obj instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (obj instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (obj instanceof String s) {
        formatted = String.format("String %s", s);
    }
    return formatted;
}

 

Java 21 이전에서 위와 같이 instanceof 연산자로 타입을 체크해 분기처리를 하는 코드가 있었다면,

 

// Java 21 이후
static String formatterPatternSwitch(Object obj) {
    return switch (obj) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        case String s  -> String.format("String %s", s);
        default        -> obj.toString();
    };
}

 

Java 21에서는 위와 같이 switch문을 사용해 훨씬 간결하고 가독성 좋게 코드를 바꿀 수 있다.

 

Switch 문과 null 체크

// Java 21 이전
static void testFooBarOld(String s) {
    if (s == null) {
        System.out.println("Oops!");
        return;
    }
    switch (s) {
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

 

Java 21 이전에는 switch 문의 검사할 값이 null이라면 NPE가 발생한다.

따라서 위와 같이 먼저 null 체크를 해줘야 했다.

 

// Java 21 이후
static void testFooBarNew(String s) {
    switch (s) {
        case null         -> System.out.println("Oops");
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

 

하지만 Java 21에서는 case에 null을 추가할 수 있어 null 처리를 따로 해줄 필요가 없다.

 

 

case 재정의

// Java 21
static void testStringOld(String response) {
    switch (response) {
        case null -> { }
        case String s -> {
            if (s.equalsIgnoreCase("YES"))
                System.out.println("You got it");
            else if (s.equalsIgnoreCase("NO"))
                System.out.println("Shame");
            else
                System.out.println("Sorry?");
        }
    }
}

 

Java 21에서 위와 같이 case 아래 또 다른 분기 처리를 하려고 하면

// Java 21
static void testStringNew(String response) {
    switch (response) {
        case null -> { }
        case String s
        when s.equalsIgnoreCase("YES") -> {
            System.out.println("You got it");
        }
        case String s
        when s.equalsIgnoreCase("NO") -> {
            System.out.println("Shame");
        }
        case String s -> {
            System.out.println("Sorry?");
        }
    }
}

 

case ~ when 문을 사용해 case를 재정의 할 수 있다. 

openJDK 문서에는 이 방식이 switch 문의 가독성을 높일 수 있다고 설명했다. 

 

 

Enum 처리

//  Java 21 이전
public enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES }

static void testforHearts(Suit s) {
    switch (s) {
        case HEARTS -> System.out.println("It's a heart!");
        default -> System.out.println("Some other suit");
    }
}

 

Java 21 이전에 switch 문에서 Enum 상수를 사용하는 것에는 제한이 있었다. 

switch 문의 선택자 표현이 무조건 enum 타입이어야 했고, case에 달린 label은 enum 상수의 simple name(이름 그 자체)이여만 했다.

 

// Java 21
sealed interface CardClassification permits Suit, Tarot {}
public enum Suit implements CardClassification { CLUBS, DIAMONDS, HEARTS, SPADES }
final class Tarot implements CardClassification {}

static void exhaustiveSwitchWithoutEnumSupport(CardClassification c) {
    switch (c) {
        case Suit s when s == Suit.CLUBS -> {
            System.out.println("It's clubs");
        }
        case Suit s when s == Suit.DIAMONDS -> {
            System.out.println("It's diamonds");
        }
        case Suit s when s == Suit.HEARTS -> {
            System.out.println("It's hearts");
        }
        case Suit s -> {
            System.out.println("It's spades");
        }
        case Tarot t -> {
            System.out.println("It's a tarot");
        }
    }
}

 

이를 case ~ when을 이용해 확장하더라도 코드가 매우 장황하다.

 

// Java 21
sealed interface CardClassification permits Suit, Tarot {}
public enum Suit implements CardClassification { CLUBS, DIAMONDS, HEARTS, SPADES }
final class Tarot implements CardClassification {}

static void exhaustiveSwitchWithBetterEnumSupport(CardClassification c) {
    switch (c) {
        case Suit.CLUBS -> {
            System.out.println("It's clubs");
        }
        case Suit.DIAMONDS -> {
            System.out.println("It's diamonds");
        }
        case Suit.HEARTS -> {
            System.out.println("It's hearts");
        }
        case Suit.SPADES -> {
            System.out.println("It's spades");
        }
        case Tarot t -> {
            System.out.println("It's a tarot");
        }
    }
}

 

장황한 코드를 위와 같이 간단하고 가독성 좋게 바꿀 수 있다. 

이때 switch문의 선택자 변수가 꼭 Enum 타입이 아니어도 되고(CardClassification은 interface)

case 상수에 Enum 상수의  simple name만(ex: CLUBS)을 사용하는 게 아닌 Suit.CLUBS처럼 qualified name을 사용할 수 있어 가독성을 높일 수 있다.

 

 

JEP 444 : Virtual Threads

JEP 444는 경량화된 스레드인 Virtual Thread를 공식적으로 채택했다. 

Virtual Thread는 처리량이 높은 동시성 애플리케이션을 관리하는 데 탁월한 성능을 발휘한다.

 

많은 Java 개발자들이 concurrent programming과 쓰레드 관리를 위해 많은 노력을 한다. 

Virtual Thread는 동시성에서 훨씬 좋은 성능과 낮은 복잡성을 제공해 준다. 

 

심지어 기존의 concurrent programming을 위한 코드를 수정할 필요 없이 기존 코드와 호환하여 Virtual Thread를 제공해준다.

 

Virtual Thread의 원리에 대해 설명하자면 본문이 너무 길이지기 때문에 궁금하다면 다음 문서를 참고하자.

 

https://openjdk.org/jeps/444

 

JEP 444: Virtual Threads

JEP 444: Virtual Threads AuthorRon Pressler & Alan BatemanOwnerAlan BatemanTypeFeatureScopeSEStatusClosed / DeliveredRelease21Componentcore-libsDiscussionloom dash dev at openjdk dot orgRelates toJEP 436: Virtual Threads (Second Preview)Reviewed byAlex

openjdk.org

 

 

 

 

JEP 449 : Deprecate the Windows 32-bit x86 Port for Removal

 

Windows 10을 마지막으로 더 이상 32-bit의 운영체제는 지원되지 않는다.

 

이 때문에 추후 Java 릴리즈에서 Windows 32-bit x86 port를 제거할 계획이기 때문에 deprecate 시켰다.

 

따라서 Windows 32-bit x86 port 환경에서 Java 21을 빌드시키면 에러가 발생한다.

 

 

JEP 451 : Prepare to Disallow the Dynamic Loading of Agents

JEP 451은 JVM이 실행 상태일 때 agent가 로딩되면 경고를 발생시킨다.

아직은 경고이지만 추후 에러를 발생하도록 수정될 예정이다.

 

이는 보안 강화를 위한 정책이다. 만약 현재 동적으로 agent를 로딩하는 라이브러리가 있다면 시작(startup) 할 때

-javaagent/--agentlib 옵션을 사용하거나 --XX:+EnableDynamicAgentLoading 을 사용해야 한다.

 

 

JEP 452 : Key Encapsulation Mechanism API

JEP 452에서는 새로운 암호화 API가 포함된다.

이 API를 사용하면 KEM(Key Encapsulation Mechanism)을 사용하여 암호화 키를 생성하며, RSA-KEM, ECIES, NIST등에서 제안된 최신 보안 규정을 준수할 수 있다.

 

 


참고

 

[1] https://www.jrebel.com/blog/java-21

[2] https://openjdk.org/projects/jdk/21/

 

반응형