Language/Java

Java 21의 주요 패치 내용 살펴보기!

JaeHoney 2023. 9. 10. 18:18

이번 포스팅에서는 Java 21이 2023년 9월(작성일 기준 이번달)에 출시한다.

개인적으로 Java 21의 패치 내용이 되게 인상깊은데 Java 21 버전의 변경사항에 대해 알아보자.

SequencedCollection

기존에 List에서 첫 번째 요소와 마지막 요소를 구할 때는 아래와 같이 코드를 작성했다.

String first = list.get(0); // 첫 번째 요소
String last = list.get(list.size() - 1) // 마지막 요소

이런 부분들 구현을 외부에 노출하므로 가독성이 많이 떨어진다.

Java 21부터는 SequencedCollection이라는 인터페이스를 제공한다.

public interface SequencedCollection<E> extends Collection<E> {
    void addFirst(E e);
    void addList(E e);
    E getFirst();
    E gitLast();
    E removeFirst();
    E removeLast();

    SequencedCollection<E> reversed();
}

해당 인터페이스를 구현하면 구현을 밖으로 드러내지 않고 명시적인 개념을 드러내서 가독성 좋게 코드를 작성할 수 있다.

Java 21의 ArrayList 역시 해당 인터페이스를 구현하고 있다. 그래서 아래와 같이 코드를 변경할 수 있다.

String first = list.getFirst(); // 첫 번째 요소
String last = list.getLast() // 마지막 요소

reversed()를 사용하면 리스트를 쉽게 뒤집을 수 있다.

List<String> reversed = list.reversed();

이와 유사한 아래 인터페이스도 추가된다.

  • SequencedSet
    • SequencedCollection을 상속
    • reversed()의 리턴 타입만 SequencedSet으로 변경
  • SequencedMap
    • LinkedHashMap과 TreeMap이 구현

Record 패턴

기존에 상위 타입으로 변환할 때 아래와 같이 코드를 작성했었다.

if(obj instanceof Student) {
    Student student = (Student) obj;
    student.study();
    String name = student.getName():
    System.out.println(name);
}

위 코드는 다소 지저분해보이고 의도를 알 수 없다.

자바 16에서 instanceof에 패턴 변수를 사용할 수 있게 되었다.

if (obj instanceof Student student) {
    student.study();
    String name = student.getName():
    System.out.println(name);
}

자바 21에서는 레코드의 구성 요소까지 패턴 변수로 사용할 수 있다.

아래 코드를 보자.

if (obj instanceof Student(String name)) {
    student.study();
    System.out.println(name);
}

다소 과한 것처럼 보이기도 한다. 하지만 제너릭과의 호환이 좋아졌다. 아래 코드를 보자.

record Hero<T weapon> {
}

if(obj instanceof Hero(Sword sword)) {
    // 짧은 사거리의 적을 공격한다.
} else if (obj instanceof Hero(Arrow arrow)) {
    // 긴 사거리의 적을 공격한다.
}

Switch

null 비교

Java 14부터 가독성 증가 및 break문 누락 방지 등을 개선한 Switch 표현식을 사용할 수 있게 되었다.

하지만 switch문에서 null을 사용하려면 여전히 복잡한 코드를 짜야 했다.

if (value == null) {
    return "default";
}

return switch(value) {
    case 1 -> A;
    case 2 -> B;
    default -> "default";
}

Java 21 부터는 switch문 내부에서 null 처리가 가능해졌다.

return switch(value) {
    case null, default -> "default";
    case 1 -> A;
    case 2 -> B;
}

Pattern

기존에는 타입을 분기하려면 반드시 if + instanceof 문을 태웠어야 헀다.

Java 21에서는 switch를 사용할 수도 있다.

switch(weapon) {
    case Sword sword -> System.out.println("가까운 거리의 적을 공격합니다.");
    case Arrow arrow -> System.out.println("먼 거리의 적을 공격합니다.");
    case Special(String name) -> System.out.println("특수 무기 " + name + "으로 적을 공격합니다.");
    case default -> System.out.println("적을 공격합니다.");
}

when

Java 21에서는 switch에 when 키워드를 사용해서 범위와 같은 조건식을 사용할 수 있게 되었다.

String s = switch (score) {
    case 100 -> "Perfect!";
    case Integer i when i >= 90 -> "Very Good!";
    case Integer i when i >= 80 -> "Good!";
    default -> "Bad";
}

패턴과 when 등을 사용하면서 누락된 케이스가 생기지는 않을까? 그렇지 않다!

누락된 케이스가 넣으면 Java에서 컴파일 에러를 내기 때문에 안전하게 사용할 수 있다.

가상 쓰레드

트래픽이 많고, I/O가 자주 발생하는 요청 당 쓰레드 모델의 경우 많은 메모리를 사용하고 CPU를 낭비하게 된다.

  • 쓰레드당 메모리 사용
  • I/O 응답 대기, 컨텍스트 스위칭에 따른 CPU 낭비

일반적인 해결 방법은 아래와 같다.

  • 비동기 I/O + 적은 쓰레드
  • 경량 쓰레드 + I/O 연동

가상 쓰레드란 I/O 중심 작업에서 처리량(성능)을 늘리기 위한 기능이다.

  • 대표적인 예: DB나 파일 시스템을 사용하는 웹 서버

Java 19에 요청 당 쓰레드 모델 구조에서의 HW 최적화를 목적으로 preview로 추가된 가상 쓰레드가 Java 21부터 정식으로 포함된다.

Thread virtual = Thread.ofVirtual()
        .name("virtual")
        .start(() -> {
            callMethod();
        });

virtual.join
try (ExecutorService executor = Executors.newVirtualThreadperTaskExecutor()) {
    executor.submit(() -> someCode())
}

쓰레드 간 구조는 아래와 같다.

img_1.png

각 쓰레드의 동작은 아래와 같다.

  • PlatformThread는 Virtual Thread를 실행해서 작업 수행을 시킨다.
  • Virtual Thread에서 Blocking이 발생하면 PlatformThread는 다른 VirtualThread에게 다른 작업을 시킨다.
  • PlatformThread는 Blocking된 Virtual Thread의 블로킹이 해제되면 해당 쓰레드의 작업을 이어서 수행시킨다.

성능 상 이점은 아래와 같다.

  • 스케줄링(Context Switching) 부하: Virtual Thread < Platform Thread
  • 메모리 사용: Virtual Thread < Platform Thread
  • I/O 블로키엥 따른 대기시간 낭비: Virtual Thread < Platform Thread

문제는 Pinned가 발생할 수 있다는 점이 있다.

  • Platform Thread가 다른 Virtual Thread를 실행할 수 없게되는 현상
  • ex. synchronized 블록에서 I/O 블로킹이 발생할 경우, Native method나 foreign function을 사용할 경우

VirtualThread의 경우 기존 코드를 수정할 필요가 없으며 풀링할 필요 없이 필요할 때 생성하면 된다.

정리

Java 21에서는 SequencedCollection를 통해 Collection을 사용할 때 index를 사용해서 구현을 밖으로 노출하는 것이 아니라 가독성 좋은 코드와 자율적인 객체를 통해 더 객체지향적으로 프로그래밍을 할 수 있게 된 것 같다.

  • 개인적으로는 ArrayList를 for문으로 loop 돌리기를 위한 isLastIndex() 등과 같은 메서드도 지원해주면 좋겠다.
  • (if(index == list.size() - 1)가 읽기 불편해서 메서드를 추출해서 쓰고 있다.)

그래도 충분히 좋아진 것 같다는 생각이 든다. recordswitch 문의 패치도 유용하다고 생각되는 부분이 많다.

Virtual thread를 통해 코드 수정 없이 성능을 늘릴 수 있다. 사내 코드의 경우에도 파일 시스템이나 RDB에 많이 접근하기 때문에 꽤 유용할 수 있을 것 같다는 생각이 든다.

빨리 충분한 POC 후 적용하고 싶다.

참고