IntelliJ IDEA

Java 19와 IntelliJ IDEA

Read this post in other languages:

Java는 그 어느 때보다 활기를 띠고 있습니다. 릴리스 주기가 짧아져 6개월마다 새로운 언어 또는 플랫폼 기능을 시험해 볼 수 있습니다. IntelliJ IDEA에서는 이러한 새로운 기능을 부담 없이 알아보고 사용해볼 수 있습니다.

이 블로그 글에서는 Java 19의 언어 기능인 레코드 패턴switch용 패턴 일치(3차 테스트 버전)에만 한정하여 다루겠습니다. 테스트 버전 API인 가상 스레드와 같은 다른 Java 19 기능은 의도적으로 다루지 않았습니다. IntelliJ IDEA는 가상 스레드에 대한 기본 구문 강조 표시를 지원하며, IDEA 팀은 디버거 및 프로파일러에서 가상 스레드에 대한 지원을 추가하기 위해 작업 중입니다.

레코드 패턴은 레코드 구성 요소에 대한 액세스를 단순화합니다. 인스턴스가 레코드의 구조와 일치할 때 레코드의 구성 요소 값을 변수 집합으로 추출하는 기능인 레코드 구조 분해와 레코드 패턴을 비교해보세요. 처음에는 큰 차이가 보이지 않을 수도 있습니다. 그러나 레코드 패턴을 switch 및 sealed 클래스의 패턴 일치와 같은 다른 언어 기능과 결합하면 생각하지 못한 결과에 놀라게 될 것입니다.

switch 패턴 일치는 switch 문과 switch 식의 case 라벨에 패턴을 추가합니다. switch와 함께 사용할 수 있는 선택자 표현식의 타입은 모든 참조 값으로 확장됩니다. 또한 case 라벨은 더 이상 상수 값으로 제한되지 않습니다. if-else 문 체인은 switch로 대체되어 코드 가독성이 향상되었습니다. 이 블로그 게시물에서는 switch용 패턴 일치의 3차 테스트 버전에 소개된 변경 사항을 다룹니다.

Java 19 기능을 사용하도록 IntelliJ IDEA를 구성하는 방법부터 시작하겠습니다.

IntelliJ IDEA 구성

Java 19에 대한 지원은 IntelliJ IDEA 2022.3에서 사용할 수 있습니다. 향후 IntelliJ IDEA 릴리스에서 더 많은 지원이 제공될 예정입니다. Java 19에서 switch 패턴 일치를 사용하려면 Project Settings(프로젝트 설정) | Project(프로젝트)로 이동하여 Project SDK를 19로 설정하고 Project language level(프로젝트 언어 수준)을 ’19 (Preview) – Record patterns, pattern matching for switch (third preview)'(19(테스트 버전) – 레코드 패턴, switch 패턴 일치(3차 테스트 버전))로 설정합니다.

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

Modules(모듈) 탭에서 모듈에 대해 동일한 언어 수준 즉, 19 (Preview) – Record patterns, pattern matching for switch (third preview)가 선택되었는지 확인합니다.

이렇게 선택하면 IntelliJ IDEA가 다음 버전에서 Java 테스트 버전 언어 기능에 대한 지원을 중단할 수 있음을 알리는 팝업이 표시될 수 있습니다. 테스트 버전 기능은 아직 영구적인 것이 아니므로 향후 Java 릴리스에서 변경되거나 제거될 수 있습니다.
지금부터 레코드 패턴과 그 이점에 관해 알아보고 실습 예제를 사용해 시연해보겠습니다.

레코드 패턴이 필요한 이유

데이터는 대부분의 애플리케이션에서 핵심입니다. 데이터를 찾거나 의사 결정에 도움이 되는 방식으로 데이터를 처리하기 위해 애플리케이션을 사용하는 경우가 많습니다. 물론 애플리케이션이 데이터를 저장, 검색 또는 처리할 수 없다면 불가능한 얘기입니다.

최근 Java 릴리스(버전 16) 중 하나에서 레코드가 Java 언어에 추가되어 개발자의 데이터 작업이 더 쉬워졌습니다. 레코드는 불변의 데이터를 모델링하는 방식을 크게 단순화합니다. 말하자면 데이터에 대한 투명한 캐리어 또는 래퍼의 역할을 합니다. 한 줄의 코드만 사용하여 레코드와 해당 구성 요소를 정의할 수 있습니다.

예를 들어, 다음 한 줄의 코드는 구성 요소 name에 대한 String 값과 age에 대한 int 값을 저장할 수 있는 새 레코드 Person을 생성합니다.

record Person (String name, int age) { }

레코드를 사용하면 상용구 코드를 작성하지 않아도 됩니다. 레코드는 생성자에 대한 기본 구현, 구성 요소에 대한 접근자 메서드, toString, equalshashCode와 같은 유틸리티 메서드를 묵시적으로 생성합니다. 레코드를 데이터의 래퍼로 사용하는 경우, 해당 구성 요소에 액세스하려면 래핑을 해제해야 할 가능성이 높습니다. 예를 들어 레코드 Person의 인스턴스가 있는 경우, 나이 구성 요소를 검사하여 해당하는 사람에게 투표할 자격이 있는지 여부를 확인할 수 있습니다. 다음은 이 동작을 수행할 수 있는 isEligibleToVote라는 메서드입니다.

boolean isEligibleToVote(Object obj) {
   if (obj instanceof Person person) {
       return person.age() >= 18;
   }
   return false;
}

앞의 예에서는 패턴 변수 person을 선언하는 instanceof의 패턴 일치를 사용하므로 objPerson으로 변환하기 위해 지역 변수를 생성할 필요가 없습니다.

레코드 패턴은 한 단계 더 나아갑니다. 인스턴스를 레코드 타입 Person과 비교할 뿐만 아니라 레코드의 구성 요소에 대한 변수를 선언하므로 레코드의 구성 요소에 액세스하기 위해 사용자가 지역 변수를 정의하거나 패턴 변수를 사용할 필요가 없습니다. 이는 컴파일러가 레코드 구성 요소의 정확한 수와 타입을 알고 있기 때문에 가능합니다.

레코드 패턴을 사용하여 앞의 메서드를 다시 작성해 보겠습니다. instanceof 연산자 또는 switch case 라벨과 함께 레코드 타입을 사용하면 IntelliJ IDEA가 이를 탐지하여 레코드 패턴 사용을 제안할 수 있습니다.

다음은 참조용으로 수정된 코드입니다.

boolean isEligibleToVote(Object obj) {
   if (obj instanceof Person(String name, int age)) {
       return age >= 18;
   }
   return false;
}

앞의 코드에서 레코드 패턴 Person(String name, int age)person.age() 대신 변수 age를 사용했을 뿐인 것처럼 보입니다. 그러나 이 블로그 게시물을 읽으면서 레코드 패턴이 코드의 의도를 단순화하고 간결한 데이터 처리 코드를 만드는 데 도움이 된다는 사실을 알게 될 것입니다.

이전에 레코드로 작업한 적이 없거나 레코드가 무엇인지 또는 IntelliJ IDEA가 레코드를 어떻게 지원하는지 자세히 알고 싶다면 레코드에 대한 저의 이전 블로그를 참조하세요.

명명된 레코드 패턴

레코드 패턴 다음에 레코드 패턴 변수가 올 수 있습니다. 그런 경우에 레코드 패턴을 명명된 레코드 패턴이라고 합니다(확정되지는 않았지만 Java 20의 레코드 패턴 2차 테스트 버전에서 명명된 레코드 패턴에 대한 지원이 중단될 수 있음).

레코드 패턴은 해당 구성 요소에 대한 패턴 변수도 정의할 수 있습니다. 명명된 레코드 패턴을 사용하고 레코드 패턴 변수로 해당 구성 요소 중 하나에 액세스하려고 하면 IntelliJ IDEA가 사용자에게 해당 구성 요소에 패턴 변수를 사용하도록 합니다. 이러한 코드는 노란색 배경으로 강조 표시됩니다. Alt+Enter를 사용하여 이 제안을 보고 수락하여 코드를 수정할 수 있습니다.

레코드 패턴 및 null

이전 섹션의 메서드 isEligibleToVote 예시를 다시 살펴보겠습니다. null 값이 다음 메서드에 전달되면 어떻게 될까요?

boolean isEligibleToVote(Object obj) {
   if (obj instanceof Person(String name, int age)) {
       return age >= 18;
   }
   return false;
}

null은 레코드 패턴 Person(String name, int age)의 인스턴스가 아니므로 instanceof 연산자는 false를 반환하고 패턴은 변수 nameage는 초기화되지 않습니다. 이렇게 되면 레코드 패턴이 null을 처리하고 사용자가 not null 검사를 정의할 필요가 없기 때문에 편리합니다.

하지만 구성 요소 name의 값이 null이면 패턴이 일치합니다.

중첩 레코드 패턴 – 간결한 코드와 명확한 의도

다른 레코드를 자신의 구성 요소로 정의하는 레코드는 흔합니다. 예를 들면 다음과 같습니다:

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) { }

null 구성 요소 값을 확인할 수 있는 레코드 패턴이 없으면 다음과 같이 Passenger 레코드의 구성 요소 값(예를 들어 fNamecountryCode)을 처리하기 위해 몇 가지 null 확인 작업이 필요합니다.

boolean checkFirstNameAndCountryCode (Object obj) {
   if (obj != null) {
       if (obj instanceof Passenger passenger) {
           Name name = null;
           Country destination = null;
 
           if (passenger.name() != null) {
               name = passenger.name();
 
               if (passenger.destination() != null) {
                   destination = passenger.destination();
 
                   String fName = name.fName();
                   String countryCode = destination.countryCode();
 
                   if (fName != null && countryCode != null) {
                       return fName.startsWith("Simo") &&
                              countryCode.equals("PRG");
                   }
               }
           }
       }
   }
   return false;
}

레코드 패턴을 중첩하여 동일한 동작을 수행할 수도 있는데, 이 경우에는 코드의 의도도 훨씬 명확해집니다. 레코드 구성 요소 namedestination이 null이면 instanceof 검사가 실패합니다.

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

앞의 예시 코드에서와 같이 기본 레코드의 구성 요소에 대한 레코드 패턴을 선택적으로 포함할 수 있습니다. 예를 들어, 앞의 예시에서는 from 구성 요소에 레코드 패턴을 사용하지 않고, 기본 레코드 Passenger의 레코드 구성 요소 대상에 레코드 패턴을 사용합니다. 요컨대, 레코드 패턴을 정의할 때 패턴 변수로 추출하고자 하는 내용을 제어할 수 있습니다. 이 기능은 데이터 처리를 많이 하는 애플리케이션에 매우 유용할 수 있습니다.

레코드 패턴과 함께 var 사용

이전 예시의 checkFirstNameAndCountryCodeAgain 메서드로 다시 돌아가 일부 패턴 변수의 타입을 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에서 다음과 같이 표시할 수 있습니다.

레코드 패턴 및 제네릭

레코드가 제네릭이면 해당 레코드 패턴은 제네릭 타입을 사용해야 합니다. 예를 들어, 클래스 WristWatch와 제네릭 레코드 Gift의 정의를 다음과 같이 가정해보겠습니다.

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

다음 메서드를 사용하여 레코드 Gift의 인스턴스를 래핑 해제할 수 있습니다. 패턴 변수 watch의 타입으로 var 또는 WristWatch를 사용할 수 있습니다.

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

그러나 다음 코드는 작동하지 않습니다.

static void cannotUnwap(Gift<object> obj) {
   if (obj instanceof Gift(var s)) {   // won’t compile              
   	//..
   }
}

다음 섹션에서는 레코드 패턴과 switch 식을 사용하여 강력한 재귀 메서드를 만들었습니다.

레코드 패턴, switch 표현식 및 sealed 클래스

레코드 패턴, switch 식 및 sealed 클래스를 함께 사용하여 데이터를 처리하는 강력하면서도 간결하고 표현력이 풍부한 코드를 생성할 수 있습니다. 다음은 레코드 Point, Line, TriangleSquare에 의해 구현되는 sealed 인터페이스 TwoDimensional의 예입니다.

sealed interface TwoDimensional {}
record Point (int x, int y) implements TwoDimensional { }
record Line    ( Point start, 
                 Point end) implements TwoDimensional { }
record Triangle( Point pointA, 
                 Point pointB, 
                 Point PointC) implements TwoDimensional { }
record Square  ( Point pointA, 
                 Point pointB, 
                 Point PointC, 
                 Point pointD) implements TwoDimensional { }

다음 메서드는 switch 문을 사용하여 Line, Triangle 또는 Square와 같은 2차원 형상에서 모든 포인트의 xy 좌표 합계를 반환하는 재귀 메서드 프로세스를 정의합니다.

static int process(TwoDimensional twoDim) {
   return switch (twoDim) {
       case Point(int x, int y) -> x + y;
       case Line(Point a, Point b) -> process(a) + process(b);
       case Triangle(Point a, Point b, Point c) -> 
                                 process(a) + process(b) + process(c);
       case Square(Point a, Point b, Point c, Point d) -> 
                                 process(a) + process(b) + process(c) + process(d);
   };
}

IntelliJ IDEA는 또한 이 메서드의 여백에 재귀 호출 아이콘을 표시합니다.

Java 20의 레코드 패턴

Java 20에서는 현재 ‘목표로 제안됨’ 상태에 있는 레코드 패턴 2차 테스트 버전이 개선될 예정입니다. 제네릭 레코드 패턴의 인수에 대한 타입 추론을 지원하는 것 외에도 향상된 for 문의 레코드 패턴 지원도 언급됩니다. 패턴 일치는 확실히 일상적인 코딩을 변화시키고 있습니다.

switch 패턴 일치 – 3차 테스트 버전

Java 17에서 테스트 버전 언어 기능으로 도입된 switch 패턴 일치는 Java 19에서 3차 테스트 버전이 되었습니다. 이 주제를 처음 접하거나 어떤 내용이 다뤄지는지 또는 IntelliJ IDEA가 이를 어떻게 지원하는지 자세히 알고 싶다면 저의 자세한 설명이 담긴 이 블로그 게시물을 참조하세요. 패턴 일치, instanceof 패턴 일치, 그리고 switch 패턴 일치가 무엇인지부터 자세히 소개되어 있습니다.

여기에서는 Java 18의 2차 테스트 버전 대비 달라진 switch 패턴 일치의 변경 사항을 다루겠습니다. switch 블록에서 when 절을 사용하는 첫 번째 변경 사항부터 시작하겠습니다.

보호된 패턴을 switch 블록의 when으로 대체

아래와 같이 정의된 Pollution, AirPollution, Deforestation 등 일련의 클래스를 생각해 보세요.

class Pollution { }
class AirPollution extends Pollution {
   public int getAQI() {
       return 100;
   }
}
class Deforestation {
   public int getTreeDamage() {
       return 300;
   }
}

switch 패턴 일치의 이전 테스트 버전을 사용할 때는 보호된 패턴, 즉 &&를 사용하여 조건을 정의하는 식으로 case 라벨에 조건을 추가할 수 있었습니다. 다음 코드는 switch 패턴 일치와 보호된 패턴 && airPol.getAQI() > 200을 사용하여 이 switch가 값 500을 반환하는 AirPollution의 인스턴스를 더욱 구체화합니다.

public class MyEarth {
   int getDamage(Object obj) {
       return switch (obj) {
           case AirPollution airPol && airPol.getAQI() > 200 -> 500;
           case Deforestation def -> def.getTreeDamage();
           case null, default -> -1;
       };
   }
}

switch 패턴 일치의 이 3차 테스트 버전에서는 보호된 패턴이 when으로 대체되었습니다. SQL 쿼리로 작업한 경험이 있다면 쉽게 이해되실 겁니다. 앞의 예제를 다시 작성해 보겠습니다.

public class MyEarth {
   int getDamage(Object obj) {
       return switch (obj) {
           case AirPollution airPol when airPol.getAQI() > 200 -> 500;
           case Deforestation def -> def.getTreeDamage();
           case null, default -> -1;
       };
   }
}

새로운 검사 – ‘switch’ 식을 아래로 이동

IntelliJ IDEA에 새로운 검사 Push down for ‘switch’ expressions(‘switch’ 식을 아래로 이동)를 추가하여 계산과 그 부수 효과를 추가로 분리하는 데 사용할 수 있는 방식으로 switch 식을 쉽게 수정할 수 있게 했습니다. 예를 들어, 다음 코드를 생각해 보겠습니다.

void printObject(Object obj) {
   if (obj instanceof String s) {
       System.out.println("String: "" + s + """);
   } else if (obj instanceof Collection<?> c) {
       System.out.println("Collection (size = " + c.size() + ")");
   } else {
       System.out.println("Other object: " + obj);
   }
}

첫 번째 단계로 IntelliJ IDEA의 검사 Replace ‘if’ with ‘switch’(‘if’를 ‘switch’로 바꾸기)를 적용하여 다음 코드를 생성합니다.

void printObject(Object obj) {
   switch (obj) {
       case String s -> System.out.println("String: "" + s + """);
       case Collection<?> c -> 
            System.out.println("Collection (size = " + c.size() + ")");
       case null, default -> System.out.println("Other object: " + obj);
   }
}

각 switch 라벨에는 System.out.println()에 대한 호출이 포함되어 있습니다. 이제 IntelliJ IDEA의 검사 ‘Push down for ‘switch’ expression’을 적용하여 다음 코드를 생성할 수 있습니다.

void printObject(Object obj) {
   System.out.println(switch (obj) {
       case String s -> "String: "" + s + """;
       case Collection<?> c -> "Collection (size = " + c.size() + ")";
       case null, default -> "Other object: " + obj;
   });
}

마지막으로, 변수를 추출하여 계산과 부수 효과를 분리하고 다음 코드를 생성할 수 있습니다.

void printObject(Object obj) {
   final var representation = switch (obj) {
       case String s -> "String: "" + s + """;
       case Collection<?> c -> "Collection (size = " + c.size() + ")";
       case null, default -> "Other object: " + obj;
   };
   System.out.println(representation);
}

다음 gif는 위의 단계를 모두 보여줍니다.

Java 20의 switch 패턴 일치

Java 20에서는 현재 ‘목표로 제안됨’ 상태에 있는 switch 패턴 일치 4차 테스트 버전을 통해 더 많은 변경 사항이 추가될 예정입니다. switch 라벨의 문법을 단순화하는 것 외에도 여러 가지 다른 변경 사항이 포함됩니다.

테스트 버전 기능

Java의 새로운 릴리스 주기가 6개월로 빨라지면서 새로운 언어 기능이 테스트 버전 기능으로 릴리스됩니다. 이러한 기능은 이후 Java 버전에서 변경되거나 변경 없이 2차 또는 3차 테스트 버전으로 다시 도입될 수 있습니다. 충분히 안정화된 기능은 표준 언어 기능으로 Java에 추가될 수 있습니다.

테스트 버전 언어 기능은 완전하지만 확정된 것은 아닙니다. 즉, 기본적으로 이러한 기능은 개발자가 사용할 수 있지만 개발자의 피드백에 따라 향후 Java 릴리스에서 세부적인 부분이 변경될 여지가 있습니다. API와 달리 안정화된 언어 기능은 향후 지원 중단되지 않습니다. 따라서 테스트 버전 언어 기능에 대한 피드백이 있으면 JDK 메일링 리스트(무료 등록 필요)에서 자유롭게 공유해 주세요.

이러한 기능이 작동하는 방식 때문에 IntelliJ IDEA는 현재 JDK의 테스트 버전 기능만 지원하기 위해 노력합니다. 테스트 버전 언어 기능은 폐기되거나 표준 언어 기능으로 추가될 때까지 Java 버전을 거치면서 변경될 수 있습니다. 이전 릴리스의 Java SE 플랫폼에서 제공하는 테스트 버전 언어 기능을 사용하는 코드는 최신 릴리스에서 컴파일되거나 실행되지 않을 수도 있습니다. 예를 들어 Java 12의 Switch 식은 브랜치에서 값을 반환하기 위해 break를 사용하도록 릴리스되었으나 이는 나중에 yield로 변경되었습니다. Switch 식에서 값을 반환하기 위해 break를 사용하는 방식에 대한 지원은 IntelliJ IDEA에서 이미 중단되었습니다.

요약

IntelliJ IDEA는 새로운 Java 기능을 지원할 뿐만 아니라 기존 인텐션과 검사가 이러한 기능과 원활하게 작동하도록 보장합니다.

IntelliJ IDEA 2022.3은 레코드 패턴을 지원하고 switch 패턴 일치 지원을 강화합니다. 더 많은 지원을 위한 작업이 진행 중입니다. IntelliJ IDEA는 sealed 클래스 및 인터페이스, 레코드, instanceof 패턴 일치 및 텍스트 블록과 같은 Java의 최근 추가 기능을 완벽하게 지원합니다.

여러분의 의견을 꼭 들려주세요. IntelliJ IDEA에서 이러한 기능 지원에 대한 피드백도 꼭 전해 주세요.

즐겁게 개발하세요!

게시물 원문 작성자

image description

Discover more