Easy Understanding

Java 19에 등장한 Virtual Thread & Structured Concurrency 맛보기(Project Loom) 본문

Java

Java 19에 등장한 Virtual Thread & Structured Concurrency 맛보기(Project Loom)

appleg1226 2022. 10. 9. 01:34

자바 19가 2022/09/20 에 General Availability로 등장하게 되었다.

이전까지는 새 버전에 그렇게 주목하지는 않았었는데,

이번에는 자바의 비동기 프로그래밍의 새 희망인 Project Loom의 결과물이 등장한다고 하여 모니터링은 하고 있었다.

그래서 그에 해당하는 내용인 다음의 두 가지에 대해서 간단하게 어떻게 사용할 수 있는지 간단하게 예제 코드만 작성해보려고 한다.

 

JEP 425: Virtual Threads (Preview) - https://openjdk.org/jeps/425

JEP 428: Structured Concurrency (Incubator) - https://openjdk.org/jeps/428

 

 

위에 대한 자세한 내용 이해는 다음 링크 등을 참고했다. 

http://gunsdevlog.blogspot.com/2020/09/java-project-loom-reactive-streams.html

http://guruma.github.io/posts/2018-09-27-Project-Loom-Fiber-And-Continuation/

 

아직 기술 구조 자체에 대해선 이해도가 올라오지 않았으므로, 

개념보다는 어떻게 사용할지에 대한 아이디어 정도만 잡기 위하여 참고하면 좋을 듯 하다.  

 

1. JDK 19 셋팅

시대가 참 좋아지긴 했다.

JDK 19의 셋팅을 위해서는 새 프로젝트를 만들 때 JDK를 선택하는 탭에서 Download JDK 버튼을 누르면 이런 식으로 새로운 버전을 클릭하여 해당 JDK로 바로 개발을 시작할 수가 있다.

 

2. JDK 실행 전 셋팅

위에도 나와 있지만, 

Virtual Threads는 Preview이고, Structured Concurrency는 Incubator 상태라서 그냥 컴파일하면 실패를 해 버린다.

다음의 설정을 두 군데에 추가하여야 실행이 된다.

--enable-preview --add-modules jdk.incubator.concurrent

첫번째는 compiler 설정으로 Preference를 찾아보면 되고,

두번째는 jvm 실행 설정으로 우측위의 녹색 실행버튼 왼쪽 탭을 눌러서 Edit Configurations를 누르면 있다.

 

3. Virtual Threads 사용법

이 새로운 경량 스레드를 어떤 인터페이스를 이용할지에 대해서 여러 고민이 많았다고 하는데,

결국은 기존 자바의 Thread 인터페이스를 통하여 사용하도록 결정되었다고 한다.

private static void test1() throws InterruptedException {
    Thread thread = Thread.ofVirtual().start(() -> {
        System.out.println("Hello world! " + Thread.currentThread());
    });
    thread.join();
}

위의 코드처럼 ofVirtual() 로 만들어주면 기존 스레드와 비슷한 방식으로 사용할 수 있다.

이외에도 ExecutorService 등 기존 스레드를 다룰 때 사용하던 인터페이스들에서도 쉽게 사용할 수 있도록 구성이 되어있다.

아래처럼 생성하면 된다.

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

기존에 자바 어플리케이션에서 해당 인터페이스를 사용했다면 간단하게 오른쪽만 끼워서 사용해주면 될 것 같고,

스프링과의 통합도 Spring 6 버전에서 같이 진행된다고 하니 쉽게 마이그레이션이 가능할 것 같다. 

 

기존 스레드와 원리는 많이 다르지만 사용법은 거의 비슷하기에 꽤나 넘어가기가 쉬울 듯 하다.

기존 멀티스레드의 단점 때문에 자바에서 사용하던 리액티브 스트림 같은 기술들과 비교해보면 재밌을 것 같은데,

 

기존 스레드처럼 사용 가능하며 쉽고 성능도 꽤나 괜찮은 Virtual Thread

vs

성능은 괜찮지만 이해하기 어려운 Reactive Streams

 

그냥 이렇게 쓰고보니 Reactive Streams의 완패처럼 써 놓은 것 같은데, 

물론 실제로 사용하고 까봐야 아는거라 일단은 그럴 것 같다고 예상만 해보겠다.

 

4. Structured Concurrency 사용법 

대충 번역하면 구조화된 병렬처리라고 읽을 수 있을 것 같다.

아래 코드처럼 특정 Scope 내에서 Multi-Thread 코드의 관리를 용이하게 하도록 나온 기술이다.

문서에 따르면 maintainability, reliability, and observability 를 향상시켜준다고 하는데...

 

대략 기술을 요약하자면

복잡한 멀티스레드들의 활동을 이 Scope 안에서 다 통제할 수 있도록 하겠다는 목적을 가진 기술이다.

 

멀티스레드들 중 어떤 스레드에서 에러가 발생한다면 바로 해당 스코프를 종료하거나 에러를 던지거나 하고 싶을 수가 있다.

그럴 때 아래 코드의 ShutdownOnFailure()를 사용하면 된다.

두번째 작업에서 에러가 발생해버리면, 첫번째 fork 내부의 작업은 시작도 못하고 끝나버린다.

그리고 scope의 throwIfFailed() 메서드를 이용하여 Exception을 전달하게 된다.

private static void test2() {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        scope.fork(() -> {    // 500m 후에 시작
            sleep(500);
            System.out.println("scope 1 " + Thread.currentThread());
            return null;
        });  
        
        scope.fork(() -> {    // 바로 시작
            System.out.println("scope 2 " + Thread.currentThread());
            throw new RuntimeException("test");
        });

        scope.join();
        scope.throwIfFailed();   // 에러가 발생하면 throw 하기
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

 

이 외에도 성공하면 끝내버리는 ShutdownOnSuccess() 가 있기는 한데, 이건 성공하면 끝내는거라 쓸 일이 있을지는 잘 모르겠다.

 

추가로 로그를 보면 알겠지만, 기본적으로 VirtualThread를 사용하도록 셋팅이 되어있다.

 

5. 정리하자면

쓰고보니 정말 가이드에 나오는 정도의 코드 예시만 담아버렸다.

근데 뭐 이 이상은 더 볼 것도 없는게, 그냥 기존 자바의 Thread, Executors, Future 등의 인터페이스 쓰는 법만 알면 된다.

 

위의 문법을 이용하여 간단한 아이템을 구매하는 로직을 짠다면 이런 식으로 할 수 있지 않을까 생각은 해보았는데,

Scope 내부는 기존 Future나 CompletableFuture를 사용하던 자바 코드와 별 차이가 없기는 하다.

public void purchaseItem(ItemPurchaseRequest itemPurchaseRequest) {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Future<Account> accountFuture = scope.fork(() -> accountService.getAccount(itemPurchaseRequest.getAccountId()));
        Future<Item> itemFuture = scope.fork(() -> itemService.getItem(itemPurchaseRequest.getItemId()));

        scope.join();
        scope.throwIfFailed();

        Account account = accountFuture.resultNow();
        Item item = itemFuture.resultNow();

        account.validate();
        item.validate();

        this.purchase(account, item);
    } catch (ExecutionException | InterruptedException e) {
        throw new RuntimeException(e);
    }

    Thread.startVirtualThread(() -> loggingService.saveLog("saved"));
}

 

 

Virtual Thread라는게 사실 문법적으로 크게 어려운 부분은 아직까지 없다.

코루틴처럼 suspend 같은 키워드 같은 게 생기면서 엄청난 문법적 변화를 가져오는건가? 라고 생각했지만 그런 건 없었다.

경량 스레드는 JVM 레벨에서 지원하는 것이지, 문법적으로 엄청나게 바뀐 건 없다.

지금까진 이전처럼 Future를 다루듯이 개발하면 되는 느낌이다.

 

결국 아직까지는 불편함 중 덜 불편한 걸 하나 선택해야 하는 상황이다. 

자바의 Future 계열을 이용하여 중간중간 비즈니스 로직과 상관 없는 fork(), join() 같은 비동기 전용 코드를 열심히 끼워주든지,

아니면 코틀린으로 갈아타서 suspend fun 같이 비동기 키워드들을 대부분의 함수들에 붙여주든지.

결국 성능은 비슷하니 스타일대로 사용하면 될 듯 하다.

다만 자바 스프링이라면 코틀린 코루틴으로 넘어갈 이유는 다소 줄어들지 않을까 싶기는 하다.

 

하지만 자바도 코드 작성이 간단해지도록 설탕 한 스푼만 넣어주면 어떨까 하는 바람이 있다.

아직은 preview에 incubator 레벨이니 앞으로 발전을 더 기대해봐도 좋을듯 하다.