자바

java21 - scoped value에 대해서 알아보자

알쓸개잡 2023. 11. 26.

최근에 릴리즈 된 java 21 기능에 범위 지정 값(이하 scoped value) API가 preview 기능으로 제공된다.

scoped value API는 메서드 매개변수를 사용하지 않고 메서드에 안전하고 효율적으로 데이터를 공유할 수 있도록 한다.

특히 java21에 릴리즈 된 가상 스레드를 사용할 때 ThreadLocal을 사용하면서 발생할 수 있는 문제에 대해서 scoped value 기능은 ThreadLocal에 대한 문제들을 해결할 수 있을 것이다.

이번 포스팅에서는 scoped value에 대한 도입 배경과 ThreadLocal 사용에 대한 문제와 함께 scoped value를 사용하는 방법에 대해서 정리해 보고자 한다.

 

scoped value History

  • scoped value에 대한 기능은 JDK 20 버전에서 처음 인큐베이팅 되었다. (JEP 429)
  • scoped value에 대한 기능은 JDK 21 버전에서 preview 기능으로 변경되었다. (JEP 446)

도입배경

  • 사용 편의성
    • 데이터 흐름에 대한 추론을 간소화할 수 있도록 스레드 및 하위 스레드와 데이터를 공유하는 프로그래밍 모델을 제공한다.
  • 이해 가능성
    • 코드의 구문 구조에서 공유 데이터의 수명을 확인할 수 있다.
  • 견고성
    • caller가 공유한 데이터는 호출된 callee에서만 사용할 수 있도록 한다.
  • 성능
    • 여러 스레드에 걸쳐서 공유되는 데이터를 변경할 수 없도록(Immutable)하여 런타임 최적화를 가능하게 한다.

스레드 로컬 변수 문제

JDK 1.2부터 도입된 스레드 로컬 변수에는 다음과 같은 설계적인 결함이 있다.

  • 제약 없는 변경 가능성
    • 모든 스레드 로컬 변수는 변경이 가능하다.
    • 스레드 로컬 변수의 get() 메서드를 호출할 수 있는 코드는 언제든지 해당 변수의 set() 메서드를 통해 변경 가능하다.
    • 이로 인해 복잡한 데이터 흐름이 발생할 수 있고 어떤 메서드가 공유 상태를 어떤 순서로 업데이트하는지 파악하기 어렵게 한다.
  • 무제한 수명
    • 스레드 로컬 변수의 스레드 복사본이 set() 메서드를 통해 설정되면 설정된 값은 스레드 수명 동안 또는 스레드 코드가 스레드 로컬 변수의 remove() 메서드를 호출할 때까지 유지된다.
    • 특히 스레드 풀을 사용하는 경우 스레드 실행이 완료되면 명시적으로 remove()를 호출해야 하는데 이를 누락할 실수 위험이 있다. remove() 호출이 누락되면 관련 없는 스레드의 태스크로 정보가 유출되어 잠재적으로 위험한 보안 취약점이 발생할 수 있다.
    • 스레드 로컬 변수가 자주 변경되는 경우, 스레드가 종료될 때까지 스레드별 데이터에 대해서 가비지 컬렉션이 수행되지 않기 때문에 remove()를 호출하더라도 안전한 시점이 명확하지 않을 수 있으며 이로 인해 장기적인 메모리 누수가 발생할 수 있다.
  • 비싼 상속 비용
    • 부모 스레드의 스레드 로컬 변수가 자식 스레드에 상속될 수 있기 때문에 스레드 로컬 변수의 오버헤드는 많은 수의 스레드를 사용할 때 더 심해질 수 있다.
    • 스레드 로컬 변수가 상속되면 자식 스레드는 부모 스레드의 로컬 변수에 대한 별도의 스토리지를 할당하여 사용하게 되는데 이로 인해 상당한 메모리 공간이 소비될 수 있다.
      • 상속 시 스레드 로컬 변수에 대한 스토리지를 별도로 할당하여 사용하는 이유는 자식 스레드에서 스레드 로컬 변수를 변경할 수 있기 때문에 (제약 없는 변경 가능성) 이는 부모 스레드의 스레드 로컬 변수에 영향을 주지 않기 위함이다.
      • 보통은 자식 스레드에서 스레드 로컬 변수를 변경하는 일은 흔하지 않음에도 제약 없는 변경 가능성으로 인하여 불필요한 메모리 낭비를 하는 것이다.

 

Scoped Value

scoped value API 대한 몇 가지 특징과 사용방법에 대해서 알아보자.

  • scoped value는 암묵적인 메서드의 매개변수이다.
  • scoped value는 immutable 한 인스턴스를 공유함으로써 데이터에 대한 무결성을 제공한다.
  • 스레드 혹은 메서드 내에서 호출되는 모든 메서드 호출에 자동으로 바인딩된다.
  • 스레드 혹은 메서드가 종료되면 자동으로 바인딩은 해제된다.
  • 부모 스레드에서 새 스레드를 포크(자식 스레드)하면 모든 자식 스레드가 자동으로 scoped value에 액세스 할 수 있다.
  • scoped value는 암묵적인 메서드의 매개변수라는 측면에서 보면 scoped value 변수의 참조만 복사되고 새 인스턴스를 생성하지 않는다는 점에서 메모리 문제를 해결할 수 있다. 이것은 수많은 가상 스레드가 실행되는 환경에서 과도한 메모리의 사용을 방지해 줄 수 있다.

샘플 코드

JDK21 Preview 활성화

scoped value API는 JDK21에서 preview 기능이기 때문에 Intellij IDE에서는 다음과 같은 설정이 필요하다.

SDK는 JDK21을 지정해야 한다. 샘플 코드는 temurin JDK 21 버전을 설치하여 지정하였다.

Language level은 21(Preview)을 설정해야 하는데 Intellij 2023.2.5 버전 이상에서 지원한다.

intellij 프로젝트 JDK 21 preivew 설정

혹은 실행 시 --enable-preview 옵션을 지정하여 활성화할 수 있다.

일반적 사용

scoped value API 타입은 ScopedValue 타입이다. scoped value API의 일반적인 사용 용법은 다음과 같다.

//scoped value 인스턴스 생성
final static ScopedValue<타입> scopedValue = ScopedValue.newInstance();

//'SCOPED' 값이 scopedValue에 바인딩되어 task()를 호출한다.
//task() 메서드에서는 'SCOPED' 값을 access 할 수 있다.
ScopedValue.where(scopedValue, "SCOPED").run(() -> task());

//where.run 호출을 하나로 호출할 수 있다.
ScopedValue.runWhere(scopedValue, "SCOPED", () -> task());

//'SCOPED' 값이 scopedValue에 바인딩되어 callTask()를 호출한다.
//callTask()에서는 'SCOPED' 값을 access 할 수 있다.
//callTask() 호출 결과를 리턴 받을 수 있다.
<callTask 리턴타입> = ScopedValue.where(scopedValue, "SCOPED").call(() -> callTask());

//where.call 호출을 하나로 호출할 수 있다.
<callTask 리턴타입> = ScopedValue.callWhere(scopedValue, "SCOPED", () -> callTask());

//scopedValue 가 바인딩 되었는지 확인할 수 있다.
boolean isBound = scopedValue.isBound();

//scopedValue 에 바인딩 된 값을 가져온다.
String boundedValue = scopedValue.get();

//scopedValue에 바인딩된 값이 없다면 'not bounded' 값을 리턴한다.
String boundedValue = scopedValue.orElse("not bounded");

//scopedValue에 바인딩된 값이 없다면 예외를 발생시킨다.
String boundedValue = scopedValue.orElseThrow(() -> new Exception("not bounded"));

. run,. runWhere,. call,. callWhere 호출은 새로운 스레드를 생성하는 것은 아니다.

일반 메서드에서 사용

import java.util.concurrent.Executors;
import java.util.concurrent.StructuredTaskScope;

public class Main {
	final static ScopedValue<String> SCOPED_VALUE = ScopedValue.newInstance();

	public static void main(String[] args) {
		normalMethod();
	}

	private static void normalMethod() {
		ScopedValue.where(SCOPED_VALUE, "task-out1").run(Main::method);
		System.out.println("method() after scoped value isBound: " 
			+ SCOPED_VALUE.isBound());
	}

	private static void method() {
		long threadId = Thread.currentThread().threadId();
		System.out.println("method, thread id: " + threadId + 
			", scoped value: " + SCOPED_VALUE.get());
		subMethod(threadId);
		System.out.println("method end thread id: " + threadId + 
			", scoped value: " + SCOPED_VALUE.get());
	}

	private static void subMethod() {
		long threadId = Thread.currentThread().threadId();
		System.out.println("sub method, parent thread id: " + parentThreadId +
			", thread id: " + threadId + ", scoped value: " + SCOPED_VALUE.get());
	}
}
method, thread id: 1, scoped value: task-out1
sub method, parent thread id: 1, thread id: 1, scoped value: task-out1
method end thread id: 1, scoped value: task-out1
method() after scoped value isBound: false

SCOPED_VALUE scoped value 변수는 'task-out1' 값을 바인딩하여 method() -> subMethod()를 호출하였다.

method() after scoped value isBound: false 출력을 통해 method()가 종료되면 바인딩은 해제됨을 알 수 있다.

SCOPED_VALUE에 할당된 'task-out1' 값의 scope는 다음과 같다.

+-- scoped value(task-out1)
|
| +-- method()
| |
| |  +-- subMethod()
| |  |
| |  |__
| |
| |__
|
|__

task-out1 scoped value의 scope은 method(), subMethod()에서 access 할 수 있다.

 

Rebinding scoped value

scoped value는 immutable이지만 다른 값을 전달해야 하는 경우가 있을 수 있는데 이 경우에는 중첩 바인딩을 통해서 해결할 수 있다.

코드를 통해서 알아보자.

import java.util.concurrent.Executors;
import java.util.concurrent.StructuredTaskScope;

public class Main {
	final static ScopedValue<String> SCOPED_VALUE = ScopedValue.newInstance();

	public static void main(String[] args) {
		normalMethod();
	}

	private static void normalMethod() {
		ScopedValue.where(SCOPED_VALUE, "task-out1").run(Main::method);
        System.out.println("method() after scoped value isBound: " 
			+ SCOPED_VALUE.isBound());
	}

	private static void method() {
		long threadId = Thread.currentThread().threadId();
		System.out.println("method, thread id: " + threadId +
			", scoped value: " + SCOPED_VALUE.get());
		ScopedValue.where(SCOPED_VALUE, "submethod " + SCOPED_VALUE.get())
			.run(() -> subMethod(threadId));
		System.out.println("method end thread id: " + threadId +
			", scoped value: " + SCOPED_VALUE.get());
	}

	private static void subMethod() {
		long threadId = Thread.currentThread().threadId();
		System.out.println("sub method, parent thread id: " + parentThreadId +
			", thread id: " + threadId + ", scoped value: " + SCOPED_VALUE.get());
	}
}
method, thread id: 1, scoped value: task-out1
sub method, parent thread id: 1, thread id: 1, scoped value: submethod task-out1
method end thread id: 1, scoped value: task-out1
method() after scoped value isBound: false

method() 내에서 subMethod()를 호출할 때 SCOPED_VALUE 변수에 'submethod task-out1' 값을 바인딩하여 subMethod()를 호출하였다.

method()에서는 SCOPED_VALUE에 'task-out1' 값이 바인딩되었고 subMethod()를 호출하기 전에 SCOPED_VALUE에 'submethod task-out1' 값을 바인딩하여 호출하였다.

이때 각 메서드에 대한 SCOPED_VALUE에 대한 scope는 다음과 같다.

+-- scoped value(task-out1)
| +-- method()
| | +-- scoped value(submethod task-out1)
| | |  +-- subMethod()
| | |  |
| | |  |__
| | |__
| |__
|__

subMethod()는 'submethod task-out1'이 바인딩된 SCOPED_VALUE를 access 한다.

method()는 'task-out1'이 바인딩된 SCOPED_VALUE를 access 한다.

가상 스레드에서 (virtual thread) 사용

가상 스레드에 대한 내용은 아래 포스팅을 참고하면 도움이 될 수도 있겠다.

2023.11.24 - [자바] - java 21 처리량 향상을 위한 대안 - virtual thread 알아보자

스레드 로컬 변수와 마찬가지로 ScopedValue를 통해서 스레드 내에서만 유지될 수 있는 값을 전달할 수 있다.

샘플 코드를 통해서 사용 방법을 알아보자.

import java.util.concurrent.Executors;
import java.util.concurrent.StructuredTaskScope;

public class Main {
	final static ScopedValue<String> SCOPED_VALUE = ScopedValue.newInstance();

	public static void main(String[] args) {
		virtualThread();
	}

	private static void virtualThread() {
		try (var executorService = Executors.newVirtualThreadPerTaskExecutor()) {
			for (int i = 0; i < 5; i++) {
				final int temp = i;
				executorService.submit(() ->
					ScopedValue.where(SCOPED_VALUE, "task" + temp)
						.run(Main::task));
			}
		}
	}

	private static void task() {
		long threadId = Thread.currentThread().threadId();
		System.out.println("task, thread id: " + threadId + 
        	", scoped value: " + SCOPED_VALUE.get());
		childTask(threadId);
	}

	private static Void childTask(long parentThreadId) {
		String scopedValue = SCOPED_VALUE.orElse("not setted");
		long threadId = Thread.currentThread().threadId();
		System.out.println("child task, parent thread id: " + parentThreadId +
			", thread id: " + threadId + ", scoped value: " + scopedValue);
		return null;
	}
}
task, thread id: 23, scoped value: task1
task, thread id: 24, scoped value: task2
task, thread id: 21, scoped value: task0
task, thread id: 25, scoped value: task3
task, thread id: 26, scoped value: task4
child task, parent thread id: 23, thread id: 23, scoped value: task1
child task, parent thread id: 21, thread id: 21, scoped value: task0
child task, parent thread id: 26, thread id: 26, scoped value: task4
child task, parent thread id: 25, thread id: 25, scoped value: task3
child task, parent thread id: 24, thread id: 24, scoped value: task2

task() 메서드는 가상 스레드의 태스크 메서드다. task() 메서드에서 childTask()를 호출하면서 childTask() 에는 자동으로 바인딩된 scoped value 값이 전달된다. 가상 스레드의 task() 역시 childTask()를 호출할 때 scoped value를 rebinding 가능하다.

샘플 코드의 가상 스레드 내에서 scoped value의 scope는 다음과 같다.

+-- virtual thread
|  +-- scoped value(task-out-number)
|  |  +-- task()
|  |  |  +-- childTask()
|  |  |  |
|  |  |  |__
|  |  |__
|  |__
|__

 

scoped value 상속

부모 스레드와 자식 스레드 간의 scoped value 상속은 구조화된 동시성 API(Structured Concurrency API)를 통해서 생성된 자식 스레드에게만 자동으로 상속된다.

구조화된 동시성 API는 JEP428에서 제안되었고 JEP453에서 JDK21 preivew 되었다. 클래스는 StructuredTaskScope이다. 관련 사용법은 참고링크에 기재하였다.

샘플 코드를 통해서 확인해 보자.

private static void task() {
    long threadId = Thread.currentThread().threadId();
    System.out.println("task, thread id: " + threadId +
        ", scoped value: " + SCOPED_VALUE.get());
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        scope.fork(() -> childTask(threadId));
        scope.join();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

private static Void childTask(long parentThreadId) {
    String scopedValue = SCOPED_VALUE.orElse("not setted");
    long threadId = Thread.currentThread().threadId();
    System.out.println("child task " + threadId + " is virtual: " + 
        Thread.currentThread().isVirtual());
    System.out.println("child task, parent thread id: " + parentThreadId +
        ", thread id: " + threadId + ", scoped value: " + scopedValue);
    return null;
}

task() 메서드에서 childTask()를 단순 호출 하는 것이 아닌 childTask()를 fork() 하여 자식 스레드의 태스크로 동작하도록 생성하였다.

결과는 다음과 같다.

task, thread id: 26, scoped value: task4
task, thread id: 23, scoped value: task1
task, thread id: 21, scoped value: task0
task, thread id: 25, scoped value: task3
task, thread id: 24, scoped value: task2
child task 36 is virtual: true
child task 33 is virtual: true
child task 32 is virtual: true
child task 34 is virtual: true
child task 35 is virtual: true
child task, parent thread id: 24, thread id: 36, scoped value: task2
child task, parent thread id: 25, thread id: 32, scoped value: task3
child task, parent thread id: 26, thread id: 34, scoped value: task4
child task, parent thread id: 21, thread id: 33, scoped value: task0
child task, parent thread id: 23, thread id: 35, scoped value: task1

task()의 자식 스레드인 childTask()에도 동일한 scoped value가 상속되는 것을 확인할 수 있다.

StructuredTaskScope 가 아닌 일반적인 스레드 생성 방법 (Thread, ExecutorService..)와 같은 방법으로 생성된 자식 스레드에는 scoped value가 상속되지 않는다.

 

task() 내에서 Thread 인스턴스를 통해서 자식 스레드를 생성하는 경우 scoped value가 자식 스레드에 전달되는지 여부를 확인해 보자.

private static void task() {
    long threadId = Thread.currentThread().threadId();
    System.out.println("task, thread id: " + threadId +
        ", scoped value: " + SCOPED_VALUE.get());

    Thread thread = Thread.ofVirtual().start(() -> childTask(threadId));
    try {
        thread.join();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

private static Void childTask(long parentThreadId) {
    String scopedValue = SCOPED_VALUE.orElse("not setted");
    long threadId = Thread.currentThread().threadId();
    System.out.println("child task " + threadId + " is virtual: " +
        Thread.currentThread().isVirtual());
    System.out.println("child task, parent thread id: " + parentThreadId +
        ", thread id: " + threadId + ", scoped value: " + scopedValue);
    return null;
}

위 코드의 결과는 다음과 같다.

task, thread id: 24, scoped value: task2
task, thread id: 25, scoped value: task3
task, thread id: 21, scoped value: task0
task, thread id: 23, scoped value: task1
task, thread id: 26, scoped value: task4
child task 36 is virtual: true
child task 37 is virtual: true
child task 33 is virtual: true
child task 34 is virtual: true
child task 35 is virtual: true
child task, parent thread id: 25, thread id: 37, scoped value: not setted
child task, parent thread id: 23, thread id: 34, scoped value: not setted
child task, parent thread id: 26, thread id: 35, scoped value: not setted
child task, parent thread id: 21, thread id: 36, scoped value: not setted
child task, parent thread id: 24, thread id: 33, scoped value: not setted

scoped value는 'not setted'로 출력되어 scoped value가 부모 스레드로부터 상속되지 않음을 알 수 있다.

부모 스레드에서 자식 스레드로 scoped value가 상속되려면 자식 스레드는 구조화된 동시성 API(StructuredTaskScope)를 통해서 생성된 스레드여야 한다.

 

자식 스레드 생성 시 scoped value rebinding

자식 스레드에게 scoped value를 상속이 아닌 다른 값으로 바인딩하려면 다음과 같이 사용한다.

private static void task() {
    long threadId = Thread.currentThread().threadId();
    System.out.println("task, thread id: " + threadId +
        ", scoped value: " + SCOPED_VALUE.get());

    //구조화된 동시성 API(StructuredTaskScope)를 통해서 생성된 자식스레드에게 scoped value는 상속된다.
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        scope.fork(
            () -> ScopedValue.callWhere(
                SCOPED_VALUE,
                "child task " + SCOPED_VALUE.get(),
                () -> childTask(threadId)
            ));
        scope.join();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    System.out.println("task end, thread id: " + threadId +
        ", scoped value: " + SCOPED_VALUE.get());
}
task, thread id: 26, scoped value: task4
task, thread id: 25, scoped value: task3
task, thread id: 24, scoped value: task2
task, thread id: 23, scoped value: task1
task, thread id: 21, scoped value: task0
child task 35 is virtual: true
child task 34 is virtual: true
child task 36 is virtual: true
child task 33 is virtual: true
child task 32 is virtual: true
child task, parent thread id: 21, thread id: 36, scoped value: child task task0
child task, parent thread id: 24, thread id: 35, scoped value: child task task2
child task, parent thread id: 25, thread id: 34, scoped value: child task task3
child task, parent thread id: 26, thread id: 32, scoped value: child task task4
child task, parent thread id: 23, thread id: 33, scoped value: child task task1
task end, thread id: 25, scoped value: task3
task end, thread id: 24, scoped value: task2
task end, thread id: 23, scoped value: task1
task end, thread id: 26, scoped value: task4
task end, thread id: 21, scoped value: task0

task() 메서드 내에서 scoped value는 'task <num>' 으로 바인딩 되었지만 childTask() 메서드 내에서는 'child task task<num>' 값으로 rebinding 되어서 사용된다.

 

전통적인 스레드(platform thread)에서 사용

가상 스레드에서 사용된 용법과 크게 다르지 않다. 동작 스레드가 platform 이냐 virtual 이냐의 차이다.

샘플 코드를 통해서 확인해 보자.

import java.util.concurrent.Executors;
import java.util.concurrent.StructuredTaskScope;

public class Main {
	final static ScopedValue<String> SCOPED_VALUE = ScopedValue.newInstance();

	public static void main(String[] args) {
		platformThread();
	}

	private static void platformThread() {
		try (var executorService = Executors.newFixedThreadPool(5)) {
			for (int i = 0; i < 5; i++) {
				final int temp = i;
				executorService.submit(() ->
					ScopedValue.where(SCOPED_VALUE, "platform" + temp)
						.run(Main::platformTask));
			}
		}
	}

	private static void platformTask() {
		long threadId = Thread.currentThread().threadId();
		System.out.println("platform task, thread id: " + threadId + 
			", scoped value: " + SCOPED_VALUE.get());
		//일반적인 방법으로 자식 스레드를 생성하면 scoped value는 상속되지 않는다.
		//new Thread(() -> platformChildTask(threadId)).start();

		//구조화된 동시성 API(StructuredTaskScope를 통해서 생성된 자식 스레드에게 scoped value가 상속된다.)
		try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
			scope.fork(() -> platformChildTask(threadId));
			scope.join();
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}

		//platformTask() 내에서 호출되는 메서드에는 scoped value가 자동 바인딩 된다.
		//platformChildTask(threadId);
		System.out.println("platform task end, thread id: " + threadId + 
			", scoped value: " + SCOPED_VALUE.get());
	}

	private static Void platformChildTask(long parentThreadId) {
		long threadId = Thread.currentThread().threadId();
		System.out.println("child platform task " + threadId +
			" is virtual: " + Thread.currentThread().isVirtual());
		String scopedValue = SCOPED_VALUE.orElse("not setted");
		System.out.println("child platform task, parent thread id: " + parentThreadId +
			", thread id: " + threadId + ", scoped value: " + scopedValue);
		return null;
	}
}

결과는 다음과 같다.

platform task, thread id: 21, scoped value: platform0
platform task, thread id: 24, scoped value: platform3
platform task, thread id: 22, scoped value: platform1
platform task, thread id: 23, scoped value: platform2
platform task, thread id: 25, scoped value: platform4
child platform task 27 is virtual: true
child platform task 28 is virtual: true
child platform task 29 is virtual: true
child platform task 26 is virtual: true
child platform task 30 is virtual: true
child platform task, parent thread id: 23, thread id: 26, scoped value: platform2
child platform task, parent thread id: 22, thread id: 30, scoped value: platform1
child platform task, parent thread id: 24, thread id: 28, scoped value: platform3
child platform task, parent thread id: 25, thread id: 27, scoped value: platform4
child platform task, parent thread id: 21, thread id: 29, scoped value: platform0
platform task end, thread id: 23, scoped value: platform2
platform task end, thread id: 21, scoped value: platform0
platform task end, thread id: 25, scoped value: platform4
platform task end, thread id: 24, scoped value: platform3
platform task end, thread id: 22, scoped value: platform1

 

platform 스레드 내에서도 scoped value를 자식 스레드에게 상속하기 위해서는 구조화된 동시성 API(StructuredTaskScope)를 사용하여 생성해야 한다. 구조화된 동시성 API를 통해서 생성된 스레드는 가상 스레드임을 알 수 있다.

 

지금까지 JDK 21에서 preview 기능으로 제공된 scoped value API에 대한 사용법을 정리해 보았다. scoped value는 기존의 스레드 로컬 변수에 대한 설계적인 문제점들을 개선할 수 있을 것으로 보인다. 또한 가상 스레드의 릴리즈로 인해 scoped value 기능이 재빠르게 정식 릴리즈가 되지 않을까 생각된다.

 


참고링크

https://openjdk.org/jeps/446

https://www.baeldung.com/java-20-scoped-values

https://www.baeldung.com/java-structured-concurrency

https://howtodoinjava.com/java/multi-threading/java-scoped-values/

댓글

💲 추천 글