Java 23에 새로 포함된 2개의 Preview

· 5min · clemado1

Java 23이 9월 17일 출시되었다. 그 중 2개 프리뷰가 처음 포함되었는데, 많은 자바 개발자들이 기대했던 원시 타입을 패턴, instanceof, switch 문에서 활용하는 기능이 포함되었다.

1. 패턴, 인스턴스오브(instanceof), 스위치의 기본 타입

JEP 455: Primitive Types in Patterns, instanceof, and switch

모든 패턴 컨텍스트에서 원시 타입 패턴을 허용하도록 한다. 이로 인해 코드를 더 간결하고 직관적으로 작성할 수 있으며, 언박싱이 필요 없어 성능 또한 향상될 수 있다. 패턴 매칭, 인스턴스오브(instanceof), 스위치(switch) 사용 시 모든 패턴 컨텍스트에 대해 원시 타입 패턴을 허용하여 패턴 매칭이 개선되었다.

예제 코드:

Switch

switchint, short, byte, char 등 기본 원시 타입의 사용은 원래도 가능했으나, 패턴 매칭을 활용한 라벨은 사용하지 못했다.

// Before JEP 455
switch (x.getStatus()) {
    case 0 -> "okay";
    case 1 -> "warning";
    case 2 -> "error";
    default -> "unknown status: " + x.getStatus();
}

JEP 455에서는 이런 불편함을 개선하고 원시 타입을 그대로 패턴 매칭할 수 있도록 개선했다.

// JEP 455
switch (x.getStatus()) {
    case 0 -> "okay";
    case 1 -> "warning";
    case 2 -> "error";
    case int i -> "unknown status: " + i;
}

또한 기존에 매칭이 불가능하던 long, float, double, boolean 타입도 새로 추가되었다. 대신 이러한 타입을 사용하는 경우 라벨이 모두 동일한 타입이어야 한다. 이는 불일치로 인해 발생하는 문제를 예방하고 프로그래머의 의도를 보호하기 위함이다.

부동소수점 라벨은 부동소수점 표준에 따라 컴파일 타임 혹은 런타임에 해석한다.

float v = ...
switch (v) {
    case 1.0f -> ...
    case 0.999999999f -> ...    // Error: 중복 라벨, 컴파일 타임에 1.0f로 반올림 됨
    default -> ...
}

boolean 타입을 사용한다면 라벨은 true와 false만 존재할 수 있다.

boolean v = ...
switch (v) {
    case true -> ...
    case false -> ...
    // 또는: case true, false -> ...
}

Record

JEP 455에서는 레코드 원시 타입 패턴을 참조 타입 패턴과 동일하게 작동하도록 개선했다. 아래 JsonValue 라는 인터페이스로 예를 들어보자.

sealed interface JsonValue {
    record JsonString(String s) implements JsonValue { }
    record JsonNumber(double d) implements JsonValue { }
    record JsonObject(Map<String, JsonValue> map) implements JsonValue { }
}

var json = new JsonObject(Map.of("name", new JsonString("John"),
                                 "age",  new JsonNumber(30)));

여기에서 age를 사용하려면 참조 타입과는 다르게 캐스트가 필요했다.

// Before JEP 455
if (json instanceof JsonObject(var map)
    && map.get("name") instanceof JsonString(String n)
    && map.get("age")  instanceof JsonNumber(double a)) {
    int age = (int)a;  // 수동적이고 잠재적으로 위험한 캐스트

    // age 사용
}

JEP 455에서는 레코드 원시 타입 패턴을 참조 타입 패턴과 동일하게 작동하도록 한다.

// JEP 455
if (json instanceof JsonObject(var map)
    && map.get("name") instanceof JsonString(String n)
    && map.get("age")  instanceof JsonNumber(int age)) {
    // age를 직접 int로 사용 가능
}

instanceof

또한 instanceof에서도 원시 타입 패턴 지원 및 safeguard casting이 추가되어, 더욱 안전하고 간결하게 타입 범위를 체크할 수 있게 되었다.

int getPopulation() {...}
float pop = getPopulation();  // 잠재적 손실 가능성
// JEP 455
int getPopulation() {...}
if (getPopulation() instanceof float pop) {
    ...pop...
}

아래 예시와 같이 캐스팅을 위한 복잡한 숫자 범위 체크도 필요없어졌다.

int i = ...
if (i >= -128 && i <= 127) {
    byte b = (byte)i;
    // i can be b
}
// JEP 455
int i = ...
if (i instanceof byte b) {
    // i can be b
}

아래는 instanceof의 safeguard casting 결과를 보여준다.

byte b = 42;
b instanceof int;         // true (unconditionally exact)

int i = 42;
i instanceof byte;        // true (exact)

int i = 1000;
i instanceof byte;        // false (not exact)

int i = 16_777_217;       // 2^24 + 1
i instanceof float;       // false (not exact)
i instanceof double;      // true (unconditionally exact)
i instanceof Integer;     // true (unconditionally exact)
i instanceof Number;      // true (unconditionally exact)

float f = 1000.0f;
f instanceof byte;        // false
f instanceof int;         // true (exact)
f instanceof double;      // true (unconditionally exact)

double d = 1000.0d;
d instanceof byte;        // false
d instanceof int;         // true (exact)
d instanceof float;       // true (exact)

Integer ii = 1000;
ii instanceof int;        // true (exact)
ii instanceof float;      // true (exact)
ii instanceof double;     // true (exact)

Integer ii = 16_777_217;
ii instanceof float;      // false (not exact)
ii instanceof double;     // true (exact)

2. 모듈 가져오기 선언

JEP 476: Module Import Declarations

Java Platform Module System (JPMS)에서 모듈 관리 방식이 개선되었다. import module 선언을 통해 다른 모듈의 특정 패키지나 타입을 선택적으로 가져올 수 있게 되어, 모듈 간의 결합도를 낮추고 더 세밀한 제어가 가능해졌다.

예제 코드:

이전에는 모듈 선언 시 requires 키워드를 사용하여 전체 모듈을 가져와야 했다.

// Before JEP 476
module com.example.app {
    requires java.base;
    requires java.sql;
    requires com.example.utils;

    // 특정 패키지만 사용하고 싶어도 전체 모듈을 가져와야 함
    requires com.example.largemodule;
}

JEP 476에서는 import 지시어를 사용하고 런타임에 requires로 변환된다.

module com.example.app {
    import module java.base;
    import module java.sql {
        java.sql;
        javax.sql;
    }

    import module com.example.utils {
        com.example.utils.logging;
    }
}

이 예제에서는 java.base 모듈 전체를 가져오고, java.sql 모듈에서는 java.sql과 javax.sql 패키지만 가져온다. 또한 com.example.utils 모듈에서는 com.example.utils.logging 패키지만 선택적으로 가져옵니다.

또한 컴파일 타임에 모듈 import 유효성을 검사하게 되면서 모듈 의존성 관련 오류를 더 빨리 발견하고 수정할 수 있게 되었다.

느낀점

  • 자바에서 단점으로 지적되었던 원시 타입 지원이 점점 확대되고 있다. 오랜 역사를 가진 언어가 모던 언어의 장점을 수용하고 변화하는 모습이 인상적이다.
  • JPMS는 써본적이 없는데 매우 직관적으로 변한것같다. 한번 써봐야겠다.

참고