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 연동하기 (한국관광공사) - REST 엔드포인트 호출 restClient 본문

Spring

[spring boot] open API 연동하기 (한국관광공사) - REST 엔드포인트 호출 restClient

그그그그 뭐더라 2024. 6. 18. 12:01

개요

스프링은 REST 엔드포인트 호출을 위해 다음과 같은 선택 사항을 제공한다.
장점과 단점을 주로 기록하려고 한다. 실제로 단점을 느끼고 싶으면 코드를 찾아보면 좋다.

  1. RestTemplate
    스프링 3.0에 추가된 HTTP 통신 템플릿이다.동기 방식으로 작동한다.
    장점
  • 오래된 스프링 버전에서 사용 가능
  • 자유롭게 구성을 할 수 있음
    단점
  • 여러 메서드가 오버로드되어있어 사용하기 힘듦
  • 고전적인 템플릿 패턴
  • Non-blocking 환경에서 적합하지 않음
  1. WebClient
    spring 5에 추가된 템플릿으로 비동기, 논블로킹을 지원하며 사용하고 싶다면 webflux 의존성이 필요하다.
    장점
  • 비동기 방식
  • 직관적이고 유연한 API 제공
    단점
  • webflux 의존성 추가 필요 -> werClient만을 위해 해당 종속을 추가해야한다면....굳이 싶은...
  1. RestClient
  • 블로킹 스타일의 유연한 API -> fluent API는 그대로 해서 RestTemplate 인프라와 함께 사용 가능 사용 방식은 webClient와 유사
  • 종속성 없음

버전이 높아지고 기술이 나올수록 추상화 수준이 높아져서 사용하기 아주 편리하고 코드 가족성도 높다.
우선 가장 최신기술인 RestClient 를 채택하여 개발해보겠다.

개발

RestClientConfig

- 재시도 로직

RetryClientHttpRequestInterceptor

class RetryClientHttpRequestInterceptor : ClientHttpRequestInterceptor {
    companion object {
        private const val ATTEMPTS = 3
        private const val ZERO = 0
    }

    private val log: Logger = LoggerFactory.getLogger(RetryClientHttpRequestInterceptor::class.java)

    private val retryableStatus = setOf(HttpStatus.TOO_MANY_REQUESTS)

    override fun intercept(
        request: HttpRequest,
        body: ByteArray,
        execution: ClientHttpRequestExecution,
    ): ClientHttpResponse {
        for (i in ZERO until ATTEMPTS) {
            val response = execution.execute(request, body)
            if (!retryableStatus.contains(response.statusCode)) {
                // Todo. ES에 사용하기 위해 json 형태 필요
                log.info("Successful attempt: $i")
                return response
            }
        }
        log.error("Retries exceeded")
        throw RetriesExceededException()
    }
}

- 타임아웃 설정

- 내부 라이브러리에서 사용하는 인코딩 제거 -> 커스텀하게 인코딩할 예정

-에러 핸들링

- 선언적 Http interface를 사용할것이기 때문에 HttpServiceProxyFactory

- 여러 api를 사용할 예정이기 때문에 제네릭하게 함수를 분리

전체 코드

package kr.weit.roadyfoody.global.client

import kr.weit.roadyfoody.common.exception.RestClientException
import kr.weit.roadyfoody.test.application.client.TestClientInterface
import kr.weit.roadyfoody.tourism.presentation.client.TourismClientInterface
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.web.client.ClientHttpRequestFactories
import org.springframework.boot.web.client.ClientHttpRequestFactorySettings
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpStatusCode
import org.springframework.http.client.JdkClientHttpRequestFactory
import org.springframework.web.client.RestClient
import org.springframework.web.client.support.RestClientAdapter
import org.springframework.web.service.invoker.HttpServiceProxyFactory
import org.springframework.web.util.DefaultUriBuilderFactory
import java.net.http.HttpClient
import java.time.Duration
import java.util.concurrent.Executors

@Configuration
class RestClientConfig {
    companion object {
        private const val CONNECT_TIME = 1L
        private const val READ_TIME = 5L
        private const val TEST_URL = "https://jsonplaceholder.typicode.com"
        private const val TOURISM_URL = "http://apis.data.go.kr/B551011/KorService1"
    }

    private val log: Logger = LoggerFactory.getLogger(RestClientConfig::class.java)

    @Bean
    fun tourismClientInterface(): TourismClientInterface {
        return createClient(TOURISM_URL, TourismClientInterface::class.java)
    }

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

        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)
    }

    private fun clientHttpRequestFactory(): JdkClientHttpRequestFactory {
        val requestSettings =
            ClientHttpRequestFactorySettings.DEFAULTS
                .withConnectTimeout(Duration.ofSeconds(CONNECT_TIME))
                .withReadTimeout(Duration.ofSeconds(READ_TIME))

        return ClientHttpRequestFactories.get(JdkClientHttpRequestFactory::class.java, requestSettings)
            ?: JdkClientHttpRequestFactory(
                HttpClient.newBuilder()
                    .executor(Executors.newVirtualThreadPerTaskExecutor())
                    .build(),
            )
    }

    private fun defaultUriBuilderFactory(baseUrl: String): DefaultUriBuilderFactory {
        return DefaultUriBuilderFactory(baseUrl).apply {
            setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE)
        }
    }
}

 

TourismClientInterface

@ClientInterface
interface TourismClientInterface {
    @GetExchange(
        "/searchKeyword1?" +
            "serviceKey={SERVICE_KEY}" +
            "&MobileApp={MOBILE_APP}&MobileOS={MOBILE_OS}&pageNo=1&numOfRows={NUM_OF_ROWS}&listYN=Y" +
            "&&arrange=C&contentTypeId={CONTENT_TYPE_ID}&keyword={KEYWORD}&_type=json",
    )
    fun searchTourismKeyword(
        @PathVariable(name = "SERVICE_KEY") serviceKey: String,
        @PathVariable(name = "MOBILE_APP") mobileApp: String,
        @PathVariable(name = "MOBILE_OS") mobileOs: String,
        @PathVariable(name = "NUM_OF_ROWS") numOfRows: Int,
        @PathVariable(name = "KEYWORD") keyword: String,
        @PathVariable(name = "CONTENT_TYPE_ID") contentTypeId: Int = 15,
    ): ResponseWrapper
}

 

TourismProperties

- application에 있는 설정값을 가져오기 위해 properties 클래스 생성

@ConfigurationProperties(prefix = "client.tour")
class TourismProperties(
    val apiKey: String,
    val mobileApp: String,
    val mobileOs: String,
)

 

TourismService

- 한글 검색어와 서비스키 인코딩 후 인터페이스 호출

@Service
class TourismService(
    private val tourismProperties: TourismProperties,
    private val tourismClientInterface: TourismClientInterface,
) {
    private val log: Logger = LoggerFactory.getLogger(TourismService::class.java)

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

        return  tourismClientInterface.searchTourismKeyword(
                        encodedServiceKey,
                        tourismProperties.mobileApp,
                        tourismProperties.mobileOs,
                        numOfRows,
                        encodedKeyword,
                        12,
                    )
    }

출처: https://myvelop.tistory.com/217#4

 

스프링의 외부 API 호출, 그리고 RestClient

애플리케이션 외부 API 호출현업에서 외부 API를 호출해야하는 일이 많다. 다른 회사의 서비스(휴대전화 인증, 결제시스템)를 이용할 때 필수적이다. 물론 클라이언트 단에서 외부 API를 호출한다

myvelop.tistory.com

 

https://digma.ai/restclient-vs-webclient-vs-resttemplate/

 

RestClient vs. WebClient vs. RestTemplate

In this article, we will compare RestClient, WebClient, and RestTemplate for choosing the right library to call REST APIs in Spring Boot.

digma.ai

https://docs.spring.io/spring-framework/reference/integration/rest-clients.html

 

REST Clients :: Spring Framework

WebClient is a non-blocking, reactive client to perform HTTP requests. It was introduced in 5.0 and offers an alternative to the RestTemplate, with support for synchronous, asynchronous, and streaming scenarios. WebClient supports the following: Non-blocki

docs.spring.io