본문 바로가기
dev/spring

Hello, Java 22!

by igooo 2024. 6. 9.
728x90

 

개요

Java 22가 정식으로 출시되었고, Spring을 통해 관련 기능을 검토한다.

 

A Quick Programming Note

이 게시글 전반적으로 functional interface type인 LanguageDemonstrationRunner를 사용한다. Throwable를 던지도록 선언된 functional interface로 걱정할 필요가 없다.

package com.example.demo;

@FunctionalInterface
interface LanguageDemonstrationRunner {
    void run() throws Throwable;
}

 

ApplicationRunner에 LanguageDemonstrationRunner의 구현체를 주입후 run 메소드를 통해 호출한다.

  // ...	
    @Bean
	ApplicationRunner demo(Map<String, LanguageDemonstrationRunner> demos) {
		return _ -> demos.forEach((_, demo) -> {
			try {
				demo.run();
			} //
			catch (Throwable e) {
				throw new RuntimeException(e);
			}
		});
	}
    // ...

 

Bye, JNI!

Project Panama는 C, C++ 코드의 사용을 쉽게해준다. 기존에는 JNI는 Java와 함께 사용하려는 언어를 서로 연결하기 위해서는 더 많은 C/C++ 코드를 작성했어야했다. 하지만 Project Pnama는 C, C++ 로 작성된 네이티브 코드에 연결는 쉬운 방법을 소개한다. 지원에는 두 가지 수준이 있다. 다소 낮은 수준의 방식으로 메모리를 조작하고 데이터를 네이티브 코드로 앞뒤로 전달할 수 있다. Project Panama는 Java에서 네이티브 코드로 호출하는 "downcalls"과 네이티브 코드에서 Java로 호출하는 "upcalls"을 지원한다. 함수를 호출하고, 메모리를 할당 해제하고, struct s의 필드를 읽고 업데이트하는 등의 작업을 수행 할 수 있다.

아래 간단한 예제를 살펴보자. java.lang.foreign.* 새로운 API를 사용하여 printf를 호출하고, 메모리 버퍼를 할당하고, 해당 버퍼를 함수에 전달한다.

package com.example.demo;

import org.springframework.stereotype.Component;

import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.SymbolLookup;
import java.util.Objects;

import static java.lang.foreign.ValueLayout.ADDRESS;
import static java.lang.foreign.ValueLayout.JAVA_INT;

@Component
class ManualFfi implements LanguageDemonstrationRunner {

    // this is package private because we'll need it later
	static final FunctionDescriptor PRINTF_FUNCTION_DESCRIPTOR =
            FunctionDescriptor.of(JAVA_INT, ADDRESS);

	private final SymbolLookup symbolLookup;

    // SymbolLookup is a Panama API, but I have an implementation I'm injecting
	ManualFfi(SymbolLookup symbolLookup) {
		this.symbolLookup = symbolLookup;
	}

	@Override
	public void run() throws Throwable {
		var symbolName = "printf";
		var nativeLinker = Linker.nativeLinker();
		var methodHandle = this.symbolLookup.find(symbolName)
			.map(symbolSegment -> nativeLinker.downcallHandle(symbolSegment, PRINTF_FUNCTION_DESCRIPTOR))
			.orElse(null);
		try (var arena = Arena.ofConfined()) {
			var cString = arena.allocateFrom("hello, Panama!");
			Objects.requireNonNull(methodHandle).invoke(cString);
		}
	}

}

 

SymbolLookup의 대한 정의는 하나의 SymbolLookup을 시도하고 첫 번째가 실패할 경우 다른것을 시동하는 일종의 composite이다..

@Bean
SymbolLookup symbolLookup() {
    var loaderLookup = SymbolLookup.loaderLookup();
    var stdlibLookup = Linker.nativeLinker().defaultLookup();
    return name -> loaderLookup.find(name).or(() -> stdlibLookup.find(name));
}

 

실행하면 hello, Panama!가 출력되는 것을 볼 수 있다.

GraalVM에서는 약간의 제약사항이 있어서 원본 문서를 참고바란다. (https://spring.io/blog/2024/03/19/hello-java-22)

 

Virtual Threads, Structured Concurrency, and Scoped Values

Virtual threads는 IO-bound 서비스를 실행하는 경우 클라우드 인프라 지출, 하드웨어 등을 더 효율적으로 사용할 수 있다. 이를 통해 java.io의 blocking  IO API에 대해 작성된 기존 코드의 변경없이 Virtual threads 로 전환하고 훨씬 더 나은 확장성을 처리할 수 있습니다. 그 결과 일반적으로 IO에서 thread blocking이 발생하지 않아 평균 응답 시간이 줄어들고 더 좋은 점은 시스템이 동시에 더 많은 요청을 처리하는 것을 볼 수 있다것이다. 그리고 Spring Boot 3.2를 사용하는 경우 spring.threads.virtual.enabled=true만 지정하면 이점을 얻을 수 있다.

Virtual threads는 Java를 간결하고 평균적인 규모의 시스템으로 만들기 위해 설계된, 앞으로 5년 이상 동안 계속될 새로운 기능의 일부입니다. Virtual threads는 함께 작동하도록 설계된 세 가지 기능 중 하나였고, 그중 릴리즈 형태로 제공된 유일한 기능입니다.

Structured Concurrency,  Scoped Values은 아직 출시되지 않았다. Structured Concurrency은 도시 코드 작성을 위한 보다 우아한 프로그래밍 모델을 제공하고, Scoped Values은 효율적이고 다양한 대안을 제공하며, 특히 현실적으로 수백만개의 스레드를 가질 수 있는 가상 스레드의 컨텍스트에서 유용하다.

 

Implicitly Declared Classes and Instance Main Methods

기본적인 아이디는 클래스 정의, public static void, String [] 인수 없이 최상위 main 메소드만 가질 수 있는 것이다. 이것이 응용프로그램에 대한 진입점으로 좋지 않습니까?

void main() {
    System.out.println("Hello, world!");
}

 

Statements Before Super

Java는 상속받은 하위 클래스에서 super 생성자를 호출하기 전에는 액세스를 허용하지 않았다. 이는 유요하지 않은 상태와 관련된 버그를 피하는 것이었다. 그러나 이는 다소 과중한 작업이으로 개발자는 super 메서드를 호출하기 전에 사소하지 않은 계산을 수생하려고 할때마다 전용 private static 보조 메서드에 의존해야했다.

JEP 페이지의 예제를 첨부한다.

class Sub extends Super {

    Sub(Certificate certificate) {
        super(prepareByteArray(certificate));
    }

    // Auxiliary method
    private static byte[] prepareByteArray(Certificate certificate) {
        var publicKey = certificate.getPublicKey();
        if (publicKey == null)
            throw new IllegalArgumentException("null certificate");
        return switch (publicKey) {
            case RSAKey rsaKey -> ///...
            case DSAPublicKey dsaKey -> ...
            //...
            default -> //...
        };
    }

}

미리보기 기능인 새로운 JEP를 사용하면 생성자 자체에서 해당 메서드를 인라인할 수 있어서 가독성을 높이고, 코드의 무분별한 확장을 방지할 수 있다.

 

Unnamed Variables and Patterns

Java에서는 Lambda를 사용하여 코딩을 많이 하는데, Lambda에서 사용하지 않은 매개변수라도 사용을 위해서는 모두 선언해줘야한다. 하지만 Java 22에서는 '_' 문자를사용하는 것으로 해결되었다.

(Rust에서도 동일한 문법으로 사용하지 않는 파라미터에 대해서 _ 문자로 처리한다.)

package com.example.demo;

import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;

@Component
class AnonymousLambdaParameters implements LanguageDemonstrationRunner {

	private final JdbcClient db;

	AnonymousLambdaParameters(DataSource db) {
		this.db = JdbcClient.create(db);
	}

	record Customer(Integer id, String name) {
	}

	@Override
	public void run() throws Throwable {
		var allCustomers = this.db.sql("select * from customer ")
                // here! 
			.query((rs, _) -> new Customer(rs.getInt("id"), rs.getString("name")))
			.list();
		System.out.println("all: " + allCustomers);
	}

}

JDBC 배치 처리등을 위해 ResultSet을 사용하는 경우 rowNum 파라미터는 대부분의 경우 사용하지않고, checkstyle 같은 분석툴에서는 code smell로 잡아낸다. 

 

Gatherers

Gatherer는 어느 시점에서든 Stream을 컬렉션으로 구체화할 필요 없이 Streams에 모든 종류의 새로운 작업을 연결할 수 있는 기능을 제공하는 약간 더 낮은 수준의 추상화를 제공한다.

package com.example.demo;

import org.springframework.stereotype.Component;

import java.util.Locale;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Gatherer;
import java.util.stream.Stream;

@Component
class Gatherers implements LanguageDemonstrationRunner {

    private static <T, R> Gatherer<T, ?, R> scan(
            Supplier<R> initial,
             BiFunction<? super R, ? super T, ? extends R> scanner) {

        class State {
            R current = initial.get();
        }
        return Gatherer.<T, State, R>ofSequential(State::new,
                Gatherer.Integrator.ofGreedy((state, element, downstream) -> {
                    state.current = scanner.apply(state.current, element);
                    return downstream.push(state.current);
                }));
    }

    @Override
    public void run() {
        var listOfNumberStrings = Stream
                .of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                .gather(scan(() -> "", (string, number) -> string + number)
                        .andThen(java.util.stream.Gatherers.mapConcurrent(10, s -> s.toUpperCase(Locale.ROOT)))
                )
                .toList();
        System.out.println(listOfNumberStrings);
    }

}

위 코드의 주요 목적은 여기에 Gatherer<T, ?, R>의 구현을 반환하는 scan 메서드가 있다는 것이다. 각 Gatherer<T, O, R>에는 초기화 프고르갦과 통합 프로램이 필요하다. 기본 결합기와 기본 마무리 장치가 함께 제공되지만 둘 다 재정의 할 수 있다. 위 구현은 모든 숫자 항목을 읽고 각 항목에 댛ㄴ 문자열을 구축한 다음 모듬 연속된 문자열 뒤에 연속적으로 누적된다. 결과적으로 1, 12, 123, 1234 등을 얻게된다.

 

Class Parsing API

.class 파일을 어떻게 일고 구성하는지에 대한 새로운 기능이 추가되었다.

다음은 .class 파일을 byte[] 배열로 로드한 다음 이를 검사하는 간단한 예제다.

package com.example.demo;

import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

import java.lang.classfile.ClassFile;
import java.lang.classfile.FieldModel;
import java.lang.classfile.MethodModel;

@Component
@ImportRuntimeHints(ClassParsing.Hints.class)
class ClassParsing implements LanguageDemonstrationRunner {

    static class Hints implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            hints.resources().registerResource(DEFAULT_CUSTOMER_SERVICE_CLASS);
        }

    }

    private final byte[] classFileBytes;

    private static final Resource DEFAULT_CUSTOMER_SERVICE_CLASS = new ClassPathResource(
            "/simpleclassfile/DefaultCustomerService.class");

    ClassParsing() throws Exception {
        this.classFileBytes = DEFAULT_CUSTOMER_SERVICE_CLASS.getContentAsByteArray();
    }

    @Override
    public void run() {
        // this is the important logic
        var classModel = ClassFile.of().parse(this.classFileBytes);
        for (var classElement : classModel) {
            switch (classElement) {
                case MethodModel mm -> System.out.printf("Method %s%n", mm.methodName().stringValue());
                case FieldModel fm -> System.out.printf("Field %s%n", fm.fieldName().stringValue());
                default -> {
                    // ... 
                }
            }
        }
    }

}

 

String Templates

String Template은 Java에 문자열 보간을 제공한다. 이 기능을 사용하면 언어가 컴파인된 문자열 값의 범위에서 사용 가능한 변수를 삽입할 수 있다.

package com.example.demo;

import org.springframework.stereotype.Component;

@Component
class StringTemplates implements LanguageDemonstrationRunner {

    @Override
    public void run() throws Throwable {
        var name = "josh";
        System.out.println(STR.""" 
            name: \{name.toUpperCase()}
            """);
    }

}

 

 

 

 

참고