Java

Java 20과 IntelliJ IDEA

Read this post in other languages:

Java 10 이후 한동안 새로운 Java 릴리스에 대한 글을 쓰면서 개발자들이 6개월마다 새로운 Java 기능을 배우고 사용하는 모습에서 즐거움을 얻습니다.

이전의 몇몇 릴리스와 비교할 때 Java 20에는 비교적 적은 기능이 추가되었습니다. 범위 지정된 값인큐베이팅 API로 도입하여 스레드 내부 및 스레드 간에 변경 불가능한 데이터를 공유할 수 있도록 함으로써 가상 스레드를 지원합니다. 두 번째 테스트 버전에서 레코드 패턴은 제네릭 레코드 패턴에 대한 지원을 개선하고 향상된 for 문에서 레코드 패턴을 사용할 수 있게 해줍니다. 네 번째 테스트 버전에서 switch 패턴 일치는 포괄적인 switch, 단순화된 switch 라벨 및 제네릭 레코드 패턴에 대한 추론 타입 인수로 작업할 때 사용 편리성을 개선합니다.

Java 20의 두 번째 테스트 버전에서 Foreign Function and Memory API는 Java 코드가 JVM 외부에서 코드 및 데이터와 통신할 수 있도록 기능을 지속적으로 개선하고 있습니다. 구조화된 동시성을 기반으로 멀티스레드 애플리케이션 제작 방식을 혁신적으로 변화시킬 경량 스레드인 가상 스레드가 최신 Java 릴리스의 또 다른 테스트 버전으로 나왔습니다. 현재 다섯 번째 테스트 버전인 Vector API는 코드에서 벡터 계산 작업을 도와줍니다.

이 블로그 글에서는 switch 레코드 패턴 및 패턴 일치와 같은 언어 기능에 대한 IntelliJ IDEA 지원, 특히 제네릭 레코드 패턴에 대한 타입 추론을 통한 향상된 지원, 포괄적인 switch 문과 표현식, 향상된 for 헤더에서 레코드 패턴과 같은 가장 흥미로운 변경 사항을 다루겠습니다.

그럼 시작해보겠습니다.

IntelliJ IDEA 구성

Java 20에 대한 지원은 IntelliJ IDEA 2023.1에서 사용할 수 있습니다. 향후 IntelliJ IDEA 릴리스에서 더 많은 지원이 제공될 예정입니다.

Java 20의 switch 레코드 패턴 및 패턴 일치와 같은 새로운 언어 기능을 사용하려면 ProjectSettings(프로젝트 설정) | Project(프로젝트)로 이동하여 Project SDK를 20으로 설정하고 프로젝트 언어 수준을 20 (Preview) – Record patterns (second preview), Pattern Matching for switch (fourth preview)(20(테스트 버전) – 레코드 패턴(두 번째 테스트 버전), switch 패턴 일치(네 번째 테스트 버전))로 설정합니다.

이미 시스템에 다운로드한 임의 버전의 JDK를 사용하거나 Edit(편집)을 클릭한 다음 Add SDK >(SDK 추가)를 선택하고 Download JDK(JDK 다운로드)…를 선택하여 다른 버전을 다운로드할 수 있습니다. 다운로드할 JDK 버전은 공급업체 목록에서 선택할 수 있습니다.

Modules(모듈) 탭에서 모듈에 대해 동일한 언어 수준 즉, 20 (Preview) – Record patterns (second preview), Pattern Matching for switch (fourth preview)(20(테스트 버전) – 레코드 패턴(두 번째 테스트 버전), switch 패턴 일치(네 번째 테스트 버전)가 선택되었는지 확인합니다.

이렇게 선택하면 IntelliJ IDEA가 다음 버전에서 Java 테스트 버전 언어 기능에 대한 지원을 중단할 수 있음을 알리는 팝업이 표시될 수 있습니다. 테스트 버전 기능은 (아직) 영구적이지 않으므로 향후 Java 릴리스에서 변경(또는 삭제)될 수 있습니다.
Java 19에 도입된 레코드 패턴에 대해 간단히 되짚어보는 것으로 시작하겠습니다.

레코드 패턴

레코드 인스턴스로 수행하려는 분명한 작업은 해당 구성 요소의 값을 추출하여 애플리케이션에서 그 값을 사용하는 것입니다. 레코드 패턴이 바로 그러한 역할을 합니다.

우선 레코드 패턴이 무엇이고 왜 필요한지 간단히 복습해 보겠습니다.

레코드 패턴 빠른 요약

레코드는 데이터를 투명하게 전달할 수 있는 간단하고 간결한 방법을 제공합니다. 이를 통해 여러 값(구성 요소라고도 함)을 집계할 수 있습니다. 한편, 레코드 패턴은 레코드 인스턴스를 구성 요소로 분해하여 구성 요소 값을 쉽게 사용할 수 있도록 해줍니다.

다음 레코드를 예로 들어보겠습니다.

record Name       (String fName, String lName) { }
record PhoneNumber(String areaCode, String number) { }
record Country    (String countryCode, String countryName) { }
record Passenger  (Name name,
                  PhoneNumber phoneNumber,
                  Country from,
                  Country destination) { }

다음 코드는 레코드 패턴을 instanceof 연산자와 함께 사용하고 간결하고 명확한 코드를 정의하여 레코드 인스턴스의 구성 요소를 패턴 변수 세트로 분해하는 방법을 보여줍니다.

boolean checkFirstNameAndCountryCodeAgain (Object obj) {
  if (obj instanceof Passenger(Name (String fName, var lName),
                               var phoneNumber,
                               Country from,
                               Country (var countryCode, String countryName) )) {
      if (fName != null && countryCode != null) {
          return fName.startsWith("Simo") && countryCode.equals("PRG");
      }
  }
  return false;
}

레코드 패턴을 처음 접한다면 레코드 패턴에 대해 자세히 다룬 Java 19와 IntelliJ IDEA라는 블로그 글을 확인해 보세요. 레코드 패턴이 무엇인지, 그리고 이를 사용해야 하는 이유와 방법이 소개되어 있습니다. 이 글에서는 Java 19에서 Java 20으로 넘어오면서 레코드 패턴의 달라진 점을 다룰 것입니다.

Java 20에서는 Java 19에 처음 추가되었던 명명된 레코드 패턴이 더 이상 사용되지 않습니다. Java 20에서는 또한 제네릭 레코드의 타입 인수 추론에 대한 지원이 개선되었고, 향상된 for 루프의 헤더에서 레코드 패턴을 사용할 수 있습니다.

레코드 구성 요소의 타입 추론

Java 19는 레코드 구성 요소의 추론을 지원했습니다. 즉, 레코드 구성 요소의 명시적 타입을 사용하는 대신 var를 사용할 수 있었습니다. 이전 섹션의 레코드 패턴에 대한 예를 다시 살펴보고 var의 사용법을 참고하세요.

boolean checkFirstNameAndCountryCodeAgain (Object obj) {
  if (obj instanceof Passenger(Name (String fName, var lName),
                               var phoneNumber,
                               Country from,
                               Country (var countryCode, String countryName) )) {
      if (fName != null && countryCode != null) {
          return fName.startsWith("Simo") && countryCode.equals("PRG");
      }
  }
  return false;
}

var를 사용하여 정의된 모든 지역 변수의 경우와 마찬가지로, IntelliJ IDEA는 예약 키워드 var를 사용하여 정의된 변수의 명시적 타입을 표시할 수 있습니다.

제네릭 레코드 패턴

Java 20은 제네릭 레코드 패턴의 타입 인수 추론을 지원합니다. 이해하기 쉽게 친구에게 보내고 싶은 손목시계나 책 같은 선물의 예를 들어 보겠습니다.

제네릭 클래스가 아닌 BookWristWatch 및 제네릭 레코드인 Gift에 대해 다음과 같이 정의한다고 가정합니다.

class Book {...}
class WristWatch {...}
record Gift<T>(T t) {}

친구가 선물을 받고 포장을 풀려고 하면 어떻게 될까요? 친구가 아래에 정의된 것처럼 unwrap 메서드를 호출한다고 가정해 보겠습니다. 다음 코드에서 unwrap 메서드는 레코드 패턴 Gift<wristwatch>(var watch)를 사용합니다. 이 패턴은 레코드 이름이 Gift인 제네릭 타입 WristWatch를 이미 지정했기 때문에 패턴 변수 watchWristWatch 타입으로 추론될 수 있습니다.

void unwrap(Gift<wristwatch> obj) {
   if (obj instanceof Gift<wristwatch> (var watch)) {
       watch.setAlarm(LocalTime.of(10, 25));
   }
}

다음 예시는 Java 19에서 작동하지 않았지만 Java 20에서는 문제 없습니다.

void unwrapAndRevealSurprise(Gift<WristWatch> obj) {
    if (obj instanceof Gift<WristWatch> (var watch)) {
        System.out.println(watch);
    }
}

void unwrapAndUseGift(Gift<WristWatch> obj) {
    if (obj instanceof Gift(var gift)) {
        gift.setAlarmTime(LocalTime.now());
    }
}

void birthdayGift(Gift<DiamondStudded<WristWatch>> gift) {
    if (gift instanceof Gift<DiamondStudded<WristWatch>>(DiamondStudded(var personalizedGift))) {
        System.out.println(personalizedGift);
    }
}

void performanceBonus(Gift<DiamondStudded<WristWatch>> personalizedGift) {
    if (personalizedGift instanceof Gift(DiamondStudded(var actualGift))) {
        System.out.println("Wrist watch" + actualGift);
    }
}

다음 섹션에서는 이러한 변화로 인해 전체 switch 문의 작동이 어떻게 달라지는지 살펴보겠습니다.

제네릭 레코드가 포함된 전체 switch 문

시작하기 전에 기본적인 내용부터 짚고 넘어가겠습니다. 다음 이미지는 switch 문 또는 switch 식(switch 문에 전달하는 변수 또는 식)에서 선택자 표현식이 참조하는 내용을 보여줍니다.

switch 문 및 switch 식의 구문은 선택자 식의 값을 해당 case 라벨의 타입 패턴 또는 레코드 패턴과 일치시키려고 할 때 포괄적이어야 합니다. 즉, 선택자 식은 case 라벨에 정의된 값 중 최소 하나와 일치해야 합니다.

Object 타입과 같이 하위 타입의 명확한 개수(확장하는 다른 클래스의 고정 개수)가 없는 타입의 경우, default case 라벨 또는 Object 타입 자체를 case 라벨 중 하나로 정의하여 완전한 switch 문으로 만들 수 있습니다. 다음은 완전한 switch 식 및 switch 문을 잘 보여주는 예입니다.

String exhaustiveSwitchExpression(Object obj) {
   return switch (obj) {
       case String s -> "String";
       case Apple apple -> "Apple";
       default -> "everything else";
   };
}

void exhaustiveSwitchStatement(Object obj) {
   switch (obj) {
       case String s -> System.out.println("String");
       case Apple apple -> System.out.println("Apple");
       case Object object -> System.out.println("everything else");
   };
}

sealed 클래스와 같은 일부 타입에는 확정적인 하위 타입이 있으므로 완전한 switch 문이나 switch 식을 정의하기 위해 default 라벨이 필요하지 않을 수도 있습니다. 예를 들면 다음과 같습니다:

sealed interface HighProtein permits Egg, Cheese {}
final class Egg implements HighProtein {}
final class Cheese implements HighProtein {}
 
int processHighProtein(HighProtein protein) {
   return switch (protein) {
       case Egg egg -> 2;
       case Cheese cheese -> 10;
   };
}

그러나 HighProtein 인터페이스를 일반 인터페이스, 즉 sealed가 아닌 인터페이스로 정의하면 이전 코드를 컴파일링하는 데 문제가 있습니다.

이제 선택자 식이 레코드 패턴과 일치하는 제네릭 레코드인 경우, 완전한 case 라벨을 정의하는 방법에 대해 이야기하겠습니다. 다음은 또 다른 클래스인 HimalayanApple에 의해 확장되는 제네릭 클래스 Apple, EggCheese 클래스에 의해 구현된 sealed 인터페이스인 HighProtein, 그리고 타입 매개변수를 취하고 이 타입의 두 구성 요소를 정의하는 제네릭 레코드인 Dish의 예입니다.

public class Apple {}
public class HimalayanApple extends Apple{}

sealed public interface HighProtein permits Egg, Cheese {}
public final class Egg implements HighProtein {}
public final class Cheese implements HighProtein {}

public record Dish<T> (T x, T y){}

이 제네릭 레코드 클래스 Dish의 인스턴스를 전환할 때 작동하거나 작동하지 않는 여러 조합에 대해 이야기해 보겠습니다.

Dish<Apple> appleDish 타입의 메서드 매개변수를 취하고 이를 전환하여 레코드 패턴과 일치시키는 orderAppleDish 메서드인 orderAppleDish부터 시작하겠습니다. 다음 코드가 문제 없다고 생각하시나요?

    int orderAppleDish(Dish<Apple> appleDish) {
        return switch (appleDish) {
            case Dish<Apple>(Apple apple, HimalayanApple himalayanApple) -> 1;
            case Dish<Apple>(HimalayanApple himalayanApple, Apple apple) -> 2;
        };
    }

HimalayanApple 클래스는 Apple 클래스를 확장하므로 앞의 switch 식을 완전한 switch로 만들려면 다음 두 가지 조합이 필요합니다.

Apple, Apple
HimalayanApple, HimalayanApple

다음 gif는 IntelliJ IDEA가 이전 코드에 문제가 있음을 감지하고 코드를 되짚으면서 오류를 수정하도록 도움을 주는 과정을 보여줍니다.

다음은 참고용으로 제시된 이전 gif의 최종 코드입니다.

class Apple {}
class HimalayanApple extends Apple{}
record Dish<T> (T ingredient1, T ingredient2) {}

public class FoodOrder {

    int orderAppleDish(Dish<Apple> appleDish) {
        return switch (appleDish) {
            case Dish<Apple>(HimalayanApple apple1, HimalayanApple apple2) -> 4;
            case Dish<Apple>(Apple apple, HimalayanApple himalayanApple) -> 1;
            case Dish<Apple>(HimalayanApple himalayanApple, Apple apple) -> 2;
            case Dish<Apple>(Apple apple1, Apple apple2) -> 3;
        };
    }

}

제네릭 레코드인 Dish에서 다음의 sealed 인터페이스와 구현을 사용하는 또 다른 예를 살펴보겠습니다.

sealed public interface HighProtein permits Egg, Cheese {}
public final class Egg implements HighProtein {}
public final class Cheese implements HighProtein {}

public record Dish<T> (T x, T y){}

어떤 사람이 음식을 주문하고 HighProtein 인터페이스를 타입 매개변수로 사용하여 Dish 인스턴스를 전달한다고 가정해 보겠습니다. 이 경우 다음 switch 식은 완전합니다.

아래 코드를 참고하세요.

public class FoodOrder {

    int orderHighProteinDish(Dish<HighProtein> proteinDish) {
        return switch (proteinDish) {
            case Dish<HighProtein>(HighProtein protein, Egg egg) -> 1;
            case Dish<HighProtein>(HighProtein protein, Cheese cheese) -> 2;
        };
    }

}

HighProtein은 sealed 인터페이스이므로 다음 switch 문의 첫 번째 case 라벨은 Egg 또는 Cheese를 레코드 Dish에 두 번째 값으로 전달하는 경우를 포괄합니다. 따라서 세 개의 case 라벨만 정의하고 있지만 완전한 switch 식입니다.

public class FoodOrder {

    int orderHighProteinDish(Dish<HighProtein> proteinDish) {
        return switch (proteinDish) {
            case Dish<HighProtein>(Egg protein, HighProtein highProtein) -> 1;
            case Dish<HighProtein>(Cheese cheese, Egg egg) -> 2;
            case Dish<HighProtein>(Cheese cheese1, Cheese cheese2) -> 4;
        };
    }

}

완전한 switch 및 제네릭 레코드에 대한 이 마지막 예에서 switch 표현식의 다음 초기 case 라벨 세트는 완전하지 않습니다. 첫 번째 재료가 HighProtein의 인스턴스이고 두 번째 값이 Egg의 인스턴스인 고단백 요리를 처리할 가능성을 포함하지 않기 때문입니다.

    int orderHighProteinDish(Dish<HighProtein> proteinDish) {
        return switch (proteinDish) {
            case Dish<HighProtein>(Egg protein, Cheese cheese) -> 1;
            case Dish<HighProtein>(Cheese cheese, Egg egg) -> 2;
            case Dish<HighProtein>(HighProtein highProtein, Cheese cheese) -> 3;
        };
    }

이전 코드에 또 다른 case 라벨을 추가하여 완전한 switch 식으로 만들어 보겠습니다.

            case Dish<HighProtein>(HighProtein highProtein, Egg egg) -> 10;

향상된 for 구문과 함께 레코드 패턴 사용

Java 20에서는 향상된 for 루프의 헤더에서 레코드 패턴을 사용할 수 있습니다. 그러나 여러 case 라벨을 할당할 수 있는 switch 문 또는 식, 향상된 for 루프 헤더에서 사용하는 단일 레코드 패턴은 for 루프에서 반복하는 모든 값과 일치해야 합니다. 그렇지 않으면 코드에서 런타임 예외가 발생합니다.

다음은 레코드 PointTriangle과 헤더에서 레코드 패턴을 사용하여 Triangle 인스턴스 목록을 반복 처리하는 향상된 for 루프의 예입니다.

record Point (int x, int y) { }
record Triangle(Point pointA, Point pointB, Point PointC) { }

long addLowerRightCoordinates(List<Triangle> triangles) {
    long sum = 0;
    for (Triangle(Point a, Point b, Point (int x, int y)) : triangles) {
        sum += x + y;
    }
    return sum;
}

다음은 일부 잘못된 예입니다.

public class Test {

    sealed interface UpperWithPermit permits PermittedRecord {}
    record PermittedRecord(int x) implements UpperWithPermit {}
    interface Upper {}
    record Record(List<String> x) implements Upper {}

    void test1(List<Upper> lists) {
        for (Record(List<String> x) : lists) {} 
    }

    void test2(List<? super UpperWithPermit> lists) {
        for (PermittedRecord(var x) : lists) {} 
    }

    void test3(List<? super PermittedRecord> lists) {
        for (PermittedRecord(var x) : lists) {} 
    }

    void test4(List lists) {
        for (PermittedRecord(var x) : lists) {} 
    }

    void test5(List<?> lists) {
        for (PermittedRecord(var x) : lists) {} 
    }
}

switch 패턴 일치

Java 20에서 switch의 패턴 일치는 네 번째 테스트 버전입니다. 패턴 일치가 처음이라면 이 링크에서 instanceof를 통해 패턴 일치에 대한 기초적인 내용을 알아보시기 바랍니다. switch를 이용한 패턴 일치가 생소한 분들은 이 링크를 확인하세요.

Java 20에서 이 기능에 몇 가지 변경 사항이 추가되었습니다. 열거형 클래스와 함께 사용할 때 switch 패턴 일치는 이제 완전한 switch 문 또는 switch 식에서 런타임에 일치하는 라벨을 찾을 수 없는 경우, ImcompatibleClassChangeError를 발생시키는 대신 MatchException을 발생시킵니다. Java 20에서 이 기능의 또 다른 변경 사항은 case 라벨의 제네릭 레코드 패턴에 대한 타입 매개변수 추론과 관련이 있습니다. 이 기능은 이 블로그 글의 제네릭 레코드가 포함된 전체 switch 문 섹션에서 이미 다루었습니다.

요약

IntelliJ IDEA는 개발자가 최신 Java 기능을 사용하는 과정에서 인지 부하를 지속적으로 줄여줍니다. IntelliJ IDEA 2023.1은 Java 20에서 ‘switch 패턴 일치’ 및 ‘레코드 패턴’과 같은 언어 기능에 추가된 변경 사항을 지원합니다. 이러한 기능에서 가장 흥미로운 변경 사항은 강화된 for 헤더에서 레코드 패턴을 사용할 수 있다는 점과 제네릭 레코드 패턴에 대한 타입 인수의 타입 추론이 개선되었다는 점입니다.

즐겁게 코딩하세요.

게시물 원문 작성자

Jessie Cho

Mala Gupta

image description

Discover more