Notice
Recent Posts
Recent Comments
Link
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

'그 뭐더라'에서 '그' 를 설명하는 블로그

[spring boot] open API 연동 - 가상스레드 사용하기 본문

Spring

[spring boot] open API 연동 - 가상스레드 사용하기

그그그그 뭐더라 2024. 6. 23. 19:55

개요

기획의 변경으로 open API 1개당 API 1개를 개발하는것에서

open API 두개를 호출하여 하나의 API로 합쳐 반환하는 기능을 맡게 되었다.

해당 기능을 구현하기 위해

각 API를 비동기로 불러오고 이 과정을 스레드로 처리하면 좋겠다고 생각하였고.

마침 자바 22를 사용하고 있어 가상스레드를 사용하게 되었다.

코드를 작성하기 전에 가상스레드가 무엇인지 간단하게 알아보자.

 

가상 스레드란?

1. 가상 스레드 VS 플랫폼 스레드(자바 기존 스레드)

플랫폼 스레드

운영체제는 유저모드와 커널모드로 나눠져있어 사용자가 커널의 기능을 사용하기 위해선 시스템 콜을 통해 통신해야합니다.

기존 java 스레드는 유저 스레드를 만들면, Java Native Interface(자바와 외부가 통신해아하니깐)를 통해 System Call로 운영체제상의 스레드를 생성하고 매핑(1:1)하는 작업을 수행합니다.

따라서, 스레드 생성과 스케줄링 및 관리를 직접 OS 커널이 담당합니다. -> 스레드 생성과 컨텍스트 스위칭에 오버헤드 발생

특히, 실제 하드웨어와 cpu자원은 한정적이기 때문에 요청이 많아 스레드수가 증가하게 되면 스레드는 생성하지 못하고

이미 생성된 스레드들이 빠르게 돌아가면서 실행하여 컨텍스트 스위칭이 자주 발생합니다.

가상 스레드

가상 스레드는 플랫폼 스레드와 가상 스레드로 나뉩니다.

플랫폼 스레드 위에 여러 가상 스레드가 번갈아 실행되는 형태로 동작합니다.

이는 운영체제의 커널로부터 독립적으로 스케줄링되며, 스레드 관리는 모두 사용자 영역(JVM)에서 처리합니다. -> 시스템 콜과 같은 커널영역 호출이 적고 메모리가 아주 적음

따라서 스레드 생성과 컨텍스트 스위칭이 빠릅니다.

코드

가상 스레드를 위한 설정

가상 스레드 실행기를 생성하는 빈을 정의한다.

@Configuration
class ThreadConfig {
    private val log: Logger = LoggerFactory.getLogger(ThreadConfig::class.java)

    @Bean
    @ConditionalOnThreading(Threading.VIRTUAL)
    fun virtualThreadExecutor(): ExecutorService {
        log.info("Create virtual executor")
        return Executors.newVirtualThreadPerTaskExecutor()
    }
}

 

RestClient 가상 스레드 설정

private fun <T> createClient(
        baseUrl: String,
        clientClass: Class<T>,
    ): T {
        val restClientBuilder =
            RestClient.builder()
                .uriBuilderFactory(defaultUriBuilderFactory(baseUrl))

        log.info("virtualThreadEnabled={}", virtualThreadEnabled)

        if (virtualThreadEnabled) {
            restClientBuilder.requestFactory(
                JdkClientHttpRequestFactory(
                    HttpClient.newBuilder()
                        .executor(Executors.newVirtualThreadPerTaskExecutor())
                        .build(),
                ),
            )
        }

        restClientBuilder
            .requestInterceptor(RetryClientHttpRequestInterceptor())
            .requestFactory(clientHttpRequestFactory())
            .defaultStatusHandler(HttpStatusCode::is4xxClientError) { _, response ->
                log.error("Client Error Code={}", response.statusCode)
                log.error("Client Error Message={}", String(response.body.readAllBytes()))
                throw RestClientException()
            }
            .defaultStatusHandler(HttpStatusCode::is5xxServerError) { _, response ->
                log.error("Server Error Code={}", response.statusCode)
                log.error("Server Error Message={}", String(response.body.readAllBytes()))
                throw RestClientException()
            }
            .build()

        val restClient = restClientBuilder.build()
        val adapter = RestClientAdapter.create(restClient)
        val factory = HttpServiceProxyFactory.builderFor(adapter).build()

        return factory.createClient(clientClass)
    }

실행방식을 가상스레드로 동작하도록 설정

open API 비동기 작업

 fun searchTourism(
        numOfRows: Int,
        keyword: String,
    ): SearchResponses {
        val encodedKeyword: String = UriEncoder.encode(keyword)
        val encodedServiceKey: String = UriEncoder.encode(tourismProperties.apiKey)

        val tourResponse =
            CompletableFuture.supplyAsync(
                {
                    tourismClientInterface.searchTourismKeyword(
                        encodedServiceKey,
                        tourismProperties.mobileApp,
                        tourismProperties.mobileOs,
                        numOfRows,
                        encodedKeyword,
                        TOUR_CONTENT_ID,
                    )
                },
                executor,
            )

        val festivalResponse =
            CompletableFuture.supplyAsync(
                {
                    tourismClientInterface.searchTourismKeyword(
                        encodedServiceKey,
                        tourismProperties.mobileApp,
                        tourismProperties.mobileOs,
                        numOfRows,
                        encodedKeyword,
                        FESTIVAL_CONTENT_ID,
                    )
                },
                executor,
            )

        CompletableFuture.allOf(tourResponse, festivalResponse).join()

        return mergeResponses(tourResponse.get(), festivalResponse.get(), numOfRows)
    }

CompletableFuture.supplyAsync를 사용하여 비동기 작업을 가상 스레드에서 실행한다.

 

결론

하나의 로직에 여러 외부 API를 호출하는경우 비동기와 병렬처리를 적용하면 전체적인 처리시간을 줄일 수 있습니다.

이를 위해 CompletableFuture를 활용하게 되었고, 이를 위해 스레드를 지정해줘야 하는데

마침 자바 22를 쓰게되어 평소에 관심있게 본 가상스레드로 지정해주었습니다.

간단하게 가상 스레드를 알아봤지만 내부 구조나 실행방식 등 더 알아볼것이 방대합니다.이를 위해 참고한 글 링크를 남겨두겠습니다.출처https://0soo.tistory.com/259?category=580548#%EA%B-%B-%EC%A-%B-%--%EC%-A%A-%EB%A-%--%EB%--%-C%EC%--%--%--%EA%B-%--%EC%--%--%--%EC%-A%A-%EB%A-%--%EB%--%-C%EC%-D%--%--%EC%B-%A-%EC%-D%B-%EC%A-%--%---%--%EA%B-%--%EC%--%--%--%EC%-A%A-%EB%A-%--%EB%--%-C%EC%--%--%--%EC%BA%--%EB%A-%AC%EC%--%B-%--%EC%-A%A-%EB%A-%--%EB%--%-Chttps://techblog.woowahan.com/15398/#toc-4