Java 21, 뭐가 달라졌을까? 가상 스레드부터 패턴 매칭까지 핵심 변경점 총정리

Java 21 뭐가 달라졌을까? Sense

2023년 9월 19일, 드디어 Java 21이 LTS(Long-Term Support) 버전으로 우리 곁을 찾아왔습니다. Java 17의 뒤를 잇는 중요한 릴리스인 만큼, 개발자들의 많은 기대를 받았는데요. 이번 포스팅에서는 Java 21에 정식으로 포함된 핵심 기능들을 중심으로, 어떤 변화들이 있는지 함께 살펴보겠습니다. (Preview 및 Incubator 단계의 기능은 제외하고 정식 기능 위주로 설명합니다.)

1. 더 똑똑해진 컬렉션: Sequenced Collections (JEP 431)

기존 자바 컬렉션 프레임워크에서는 List, Deque, SortedSet 등 순서가 있는 컬렉션에서 첫 번째나 마지막 요소, 또는 역순으로 요소에 접근하는 방식이 일관되지 않아 불편함이 있었습니다.

JEP 431 (Sequenced Collections) 은 이러한 문제를 해결하기 위해 SequencedCollection, SequencedSet, SequencedMap이라는 새로운 인터페이스를 도입했습니다. 이 인터페이스들은 다음과 같은 통일된 메소드를 제공합니다:

  • getFirst(): 첫 번째 요소 반환
  • getLast(): 마지막 요소 반환
  • addFirst(E): 맨 앞에 요소 추가
  • addLast(E): 맨 뒤에 요소 추가
  • removeFirst(): 첫 번째 요소 제거 후 반환
  • removeLast(): 마지막 요소 제거 후 반환
  • reversed(): 컬렉션의 역순 뷰(View)를 제공

이를 통해 개발자들은 다양한 순서 기반 컬렉션에서 일관된 API로 데이터를 더 쉽게 조작할 수 있게 되었습니다. 예를 들어, LinkedHashSet에서도 reversed().stream()과 같이 역순 스트림 처리가 간편해집니다.

2. 혁신적인 동시성 처리: Virtual Threads (JEP 444)

드디어! 많은 개발자들이 기다려온 가상 스레드(Virtual Threads)가 Java 21에 정식으로 포함되었습니다.

전통적인 플랫폼 스레드는 운영체제 스레드와 1:1로 매핑되어 생성 비용이 비싸고 개수에 제한이 있었습니다. 이로 인해 "하나의 요청마다 하나의 스레드(thread-per-request)" 스타일의 동시성 프로그래밍은 높은 처리량을 요구하는 애플리케이션에서 확장성 문제를 겪곤 했습니다.

가상 스레드는 JVM에 의해 관리되는 매우 가벼운 스레드로, 수백만 개까지도 쉽게 생성하고 관리할 수 있습니다. 이들은 소수의 플랫폼 스레드 위에서 동작하며, I/O 작업 등으로 블로킹될 때 해당 플랫폼 스레드를 점유하지 않고 다른 가상 스레드에게 양보합니다.

주요 이점:

  • 높은 처리량: 기존의 동기식 코드 스타일을 유지하면서도 비동기 방식과 유사한 높은 처리량 달성 가능.
  • 개발 용이성: 복잡한 비동기/리액티브 프로그래밍 패러다임을 익히지 않아도 동시성 애플리케이션 개발 가능.
  • 기존 코드 호환성: 기존 Java 코드 및 라이브러리와 대부분 호환.

가상 스레드의 도입은 Java 동시성 프로그래밍의 패러다임을 바꿀 만한 중요한 변화로, 서버 애플리케이션 개발에 큰 영향을 미칠 것으로 예상됩니다.

3. 더욱 강력해진 데이터 처리: Record Patterns (JEP 440)

Java 16에서 instanceof 연산자에 타입 패턴이 도입되어 타입 확인과 동시에 변수 바인딩이 가능해졌습니다. JEP 440 (Record Patterns) 은 이 기능을 레코드(Record) 클래스로 확장합니다.

기존에는 레코드의 필드에 접근하려면 다음과 같이 작성해야 했습니다:

// 이전 방식
if (obj instanceof Point p) {
    int x = p.x();
    int y = p.y();
    System.out.println(x + y);
}

Java 21부터는 레코드 패턴을 사용하여 다음과 같이 코드를 간결하게 작성할 수 있습니다:

// Java 21 Record Patterns
if (obj instanceof Point(int x, int y)) {
    System.out.println(x + y);
}

instanceof 검사와 동시에 레코드의 컴포넌트를 바로 변수 x, y로 분해(deconstruct)하여 사용할 수 있게 되어, 코드의 가독성과 편의성이 크게 향상됩니다.

4. 유연하고 안전해진 제어 흐름: Pattern Matching for switch (JEP 441)

JEP 441 (Pattern Matching for switch)switch 문과 표현식에서 패턴 매칭 기능을 대폭 강화합니다.

주요 개선 사항:

  • 타입 패턴 지원: case 레이블에서 특정 타입의 객체를 바로 매칭하고 변수로 바인딩할 수 있습니다.

    // Java 21 Pattern Matching for switch
    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();
        };
    }
    
  • null 처리 개선: switch 문 내부에서 case null을 통해 null 값을 명시적으로 처리할 수 있게 되어, 외부에서 null 체크를 할 필요가 줄어듭니다.

    // Java 21 null case
    switch (s) {
        case null         -> System.out.println("Oops");
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
    
  • Guarded Patterns (조건부 case): when 키워드를 사용하여 case 레이블에 추가적인 조건을 부여할 수 있습니다.

    // Java 21 when clause
    switch (response) {
        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?");
        default                                      -> { /* ... */ }
    }
    
  • enum 상수 개선: enum 상수를 switch에서 더 자연스럽고 완전하게 사용할 수 있도록 개선되었습니다.

이러한 개선으로 switch 문은 더욱 강력하고 표현력이 풍부해졌으며, 복잡한 if-else if 체인을 대체하여 코드의 가독성을 높일 수 있게 되었습니다.

5. HotSpot 및 GC 개선

Generational ZGC (JEP 439)

ZGC(Z Garbage Collector)는 매우 낮은 지연 시간(pause time)을 목표로 하는 GC입니다. Java 21에서는 ZGC에 세대별 GC(Generational GC) 기능이 추가되었습니다. 대부분의 객체는 생성된 후 짧은 시간 내에 사용되지 않는다는 "약한 세대 가설(weak generational hypothesis)"에 기반하여, 객체를 젊은 세대(young generation)와 늙은 세대(old generation)로 나누어 관리합니다. 이를 통해 ZGC는 처리량(throughput)을 향상시키고, GC로 인한 오버헤드를 줄여 더 넓은 범위의 애플리케이션에서 매력적인 선택지가 될 수 있습니다.

6. 기타 주요 변경 사항

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

향후 릴리스에서 Windows 32-bit x86 포트를 제거하기 위한 준비 단계로, 해당 환경에서 JDK를 빌드하려고 하면 오류 메시지가 표시됩니다. (실행은 여전히 가능하나, 지원 중단을 예고)

Prepare to Disallow the Dynamic Loading of Agents (JEP 451)

JVM이 실행 중인 상태에서 에이전트(agent)를 동적으로 로드하려고 할 때 경고를 발생시킵니다. 대부분의 에이전트는 JVM 시작 시 로드되므로, 런타임 중 동적 로드는 잠재적인 보안 위험이나 안정성 문제를 야기할 수 있다는 판단에서 비롯된 조치입니다. 향후 릴리스에서는 기본적으로 동적 로딩이 금지될 수 있습니다.

Key Encapsulation Mechanism API (JEP 452)

공개 키 암호화를 사용하여 대칭 키를 안전하게 캡슐화하고 전송하기 위한 KEM(Key Encapsulation Mechanism) API를 도입합니다. 이는 기존의 키 교환 방식보다 간결하고, 특히 양자 컴퓨터의 공격에 대응하기 위한 양자내성암호(PQC)로의 전환에 중요한 역할을 할 것으로 기대됩니다.

마무리하며

Java 21은 가상 스레드라는 혁신적인 동시성 모델을 필두로, 컬렉션 API 개선, 패턴 매칭 강화 등 개발 생산성과 애플리케이션 성능을 한층 끌어올릴 수 있는 강력한 기능들을 대거 선보였습니다. LTS 버전인 만큼 안정성과 장기적인 지원을 바탕으로 많은 프로젝트에서 적극적으로 도입될 것으로 기대됩니다.

이번에 소개된 기능들 외에도 다양한 개선 사항들이 포함되어 있으니, Java 개발자라면 Java 21의 새로운 변화들을 직접 경험해보시는 것을 추천합니다!

(참고: 당초 JDK 21에 포함될 예정이었던 JEP 404: Generational Shenandoah (Experimental) 기능은 최종 검토 시간 부족 등의 이유로 JDK 22로 연기되었습니다.)