λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
πŸ“• Spring Framework

Spring Cloud OpenFeign 더 잘 μ‚¬μš©ν•΄λ³΄κΈ°

by GroovyArea 2024. 5. 11.

마이크둜 μ„œλΉ„μŠ€μ—μ„œ,

Spring Boot λ₯Ό μ΄μš©ν•˜μ—¬ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ κ°œλ°œν•  λ•Œλ©΄,

μ™ΈλΆ€ API λ₯Ό ν˜ΈμΆœν•΄μ•Ό ν•˜λŠ” 상황이 μ‘΄μž¬ν•œλ‹€.

 

Spring ν”„λ ˆμž„μ›Œν¬κ°€ μ§€μ›ν•˜λŠ” μ—¬λŸ¬ κ°€μ§€ Http Client κ°€ μžˆλ‹€.

 

RestTemplate 의 경우 Blocking λ°©μ‹μœΌλ‘œ Http μš”μ²­μ„ μ§„ν–‰ν•  수 μžˆλ‹€.

ν•˜λ‚˜μ˜ μš”μ²­μ„ μœ„ν•΄, μ½”λ“œλ₯Ό μž‘μ„±ν•˜λ €λ©΄,

μž¬μ‚¬μš©μ„±μ„ κ³ λ €ν•œλ‹€ ν• λ•Œ, μž‘λ”” μž‘μ€ μ»΄ν¬λ„ŒνŠΈλ‘œ 좔상화λ₯Ό 많이 μ§„ν–‰ν•˜μ—¬ 번거둜운 μ½”λ“œλ₯Ό μž‘μ„±ν•΄μ•Ό ν•œλ‹€λŠ” 점이 μžˆμ—ˆλ‹€.

무엇보닀 μ–΄λ– ν•œ μš”μ²­μ„ ν•˜λŠ”μ§€ ν•œλˆˆμ— λ“€μ–΄μ˜€μ§€ μ•Šμ•˜λ‹€.

 

WebClient 의 경우, Non-Blocking λ°©μ‹μœΌλ‘œ Http μš”μ²­μ„ μ§„ν–‰ν•  수 μžˆλ‹€.

λ¬Όλ‘  Blocking call 도 κ°€λŠ₯ν•˜λ‹€.

λΉŒλ” νŒ¨ν„΄μ„ ν™œμš©ν•œ λ°©μ‹μœΌλ‘œ, RestTemplate λ³΄λ‹€λŠ” 가독성이 많이 λ‚˜μ•„μ‘Œκ³ ,

Error Handling 도 체이닝 λ©”μ†Œλ“œλ₯Ό 톡해 μ‰½κ²Œ 진행이 κ°€λŠ₯ν•˜λ‹€λŠ” μž₯점이 μžˆλ‹€.

ν•˜μ§€λ§Œ Blocking λ°©μ‹μ˜ Call λ§Œμ„ ν•œλ‹€λ©΄ 크게 μ‚¬μš©ν•˜λ €λŠ” μ˜λ―Έκ°€ μžˆλ‚˜ μ‹Άλ‹€.

무엇보닀, WebFlux μ˜μ‘΄μ„±μ„ μΆ”κ°€ν•΄μ•Ό 될 뿐만 μ•„λ‹ˆλΌ,

WebFlux 에 λŒ€ν•œ 이해도가 ν•„μš”ν•˜λ‹€λŠ” 단점이 μ‘΄μž¬ν•œλ‹€.

 

κ·Έ 쀑 개인적으둜 μ„ μ–Έν˜• μΈν„°νŽ˜μ΄μŠ€μΈ

κ°€μž₯ μž‘μ„±ν•˜κΈ° 쉽고, 가독성이 쒋은,

Open Feign Client λ₯Ό μ‚¬μš©ν•΄λ³΄λ©° λŠλ‚€ μž₯점과 νŠΉμ§•μ— λŒ€ν•΄μ„œ 정리해 보렀 ν•œλ‹€.

 

[Open Feign λ°°κ²½]

Netflix μ§„μ˜μ΄ λ§Œλ“ , μ„ μ–Έν˜• Http Client μΈν„°νŽ˜μ΄μŠ€μ΄λ‹€.

Spring Cloud μƒνƒœκ³„λ‘œ ν†΅ν•©ν•˜λ©°,

Spring MVC μ• λ…Έν…Œμ΄μ…˜μ„ μ§€μ›ν•˜μ—¬, Spring Boot μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ λ”μš± μž‘μ„±ν•˜κΈ° μ‰¬μ›Œμ‘Œλ‹€.

λ˜ν•œ, Spring Cloud 의 λ‹€μ–‘ν•œ κΈ°μˆ λ“€μΈ 유레카, μ„œν‚· 브레이컀, λ‘œλ“œ λ°ΈλŸ°μ„œμ™€μ˜ 톡합이 μ‰½λ‹€λŠ” μž₯점이 μžˆλ‹€.

 

[μ‚¬μš© 방법]

implementation("org.springframework.cloud:spring-cloud-starter-openfeign")

 

Gradle λΉŒλ“œ νˆ΄μ„ μ‚¬μš©ν•œλ‹€λ©΄, ν•΄λ‹Ή μ˜μ‘΄μ„±μ„ μΆ”κ°€ν•˜λ©΄ 끝이닀.

κ·Έ μ™Έ, κΈ°λ³Έ 적인 μ„€μ • 방법은 μƒλž΅ν•˜κ² λ‹€.

 

@FeignClient(
    name = "appleClient"
)
interface AppleClient {

    @GetMapping("/api/v1/apples/{id}")
    fun getApple(
        @PathVariable id: Long,
    ): ResponseEntity<GetAppleDTO?>
}

data class GetAppleDTO(
    val id: Long,
    val name: String,
)

 

이게 끝이닀.

정말 κ°„λ‹¨ν•˜μ§€ μ•Šμ€κ°€?

 

ν•˜μ§€λ§Œ, μš°λ¦¬κ°€ κΆκΈˆν•œ 것은 이게 끝이 아닐 것이닀.

더 세뢀적인 섀정을 톡해 Feign Client λ₯Ό μ’€ 더 잘 ν™œμš©ν•  수 μžˆλŠ” 방법을 μ•Œμ•„ 보자.

 

 

[application.yaml]

Spring Boot λŠ” application.yaml ν”„λ‘œνŒŒμΌμ„ 톡해,

μ• ν”Œλ¦¬μΌ€μ΄μ…˜ κ°œλ°œμ— ν•„μš”ν•œ λ‹€μ–‘ν•œ 섀정을 μ½”λ“œ λŒ€μ‹  κ°„κ²°ν•˜κ²Œ μ„€μ •ν•  수 있게 도와 μ€€λ‹€.

 

Spring Cloud Open Feign 의 κ²½μš°λ„ λ§ˆμ°¬κ°€μ§€μ΄λ‹€.

spring:

  cloud:
    openfeign:
      okhttp:
        enabled: true
      client:
        config:
          default:
            logger-level: full
            connect-timeout: 1000
            read-timeout: 3000
          appleClient:
            url: ${APPLE_SERVICE_HOST}
            dismiss404: true

 

기본적으둜 μœ„μ˜ μ„ΈνŒ…μ„ κ°€μ Έκ°€λŠ” νŽΈμ΄λ‹€.

Open Feign 은 κ΅¬ν˜„μ²΄λ‘œ λ‹€μ–‘ν•œ Http Client λ₯Ό μ‚¬μš©ν•˜λŠ”λ°,

이쀑 Apache Http Client λŒ€μ‹  OkHttp λ₯Ό κ΅¬ν˜„μ²΄λ‘œ μ΄μš©ν•˜μ˜€λ‹€.

κ°„λž΅ν•œ μ‚¬μš© μ΄μœ λŠ”, μ λ‹Ήν•œ μš”μ²­ 수이며, μ΄ˆλ‹Ή μš”μ²­ μˆ˜μ— 적은 λ©”λͺ¨λ¦¬λ₯Ό μ‚¬μš©ν•˜λŠ” μž₯점이 있기 λ•Œλ¬Έμ΄λ‹€.

 

Apache Http VS OkHttp 

κΆκΈˆν•˜μ‹  뢄은 μ•„λž˜ λΈ”λ‘œκ·Έλ₯Ό μ°Έμ‘°ν•˜κΈΈ λ°”λž€λ‹€.

https://mindule.tistory.com/27

 

OkHttp VS Apache HttpClient

μƒˆλ²½μ— μ•Œλ¦Όμ΄ μšΈλ Έλ‹€. λΆˆκΈΈν•˜λ‹€. 예감이 λ§žλ‹€. μž₯μ• κ°€ λ°œμƒν–ˆλ‹€. ν—ˆκ²μ§€κ² PCλ₯Ό 켜고 μ—¬κΈ° μ €κΈ° μ‚΄νŽ΄λ΄€λ‹€. 둜그λ₯Ό 보고 깜짝 λ†€λžλ‹€. μ˜ˆμƒκ³Ό 달리 OkHttpμ—μ„œ μ—λŸ¬κ°€ λ°œμƒν–ˆλ‹€. λ‚œ 많이 μ“°λŠ” μ˜€ν”ˆ

mindule.tistory.com

 

config ν•˜μœ„ μ„€μ •μœΌλ‘œλŠ”,

κΈ°λ³Έ μ„€μ •μœΌλ‘œ λ‹Ήμ—°νžˆ λͺ¨λ‹ˆν„°λ§μ„ μœ„ν•΄ logger level 은 full 둜 μ„€μ •ν•˜μ˜€κ³ ,

connect timeout, read timeout 의 경우, μ„œλ²„ λ¦¬μ†ŒμŠ€μ— 맞게 μΆ©λΆ„νžˆ μ„€μ •ν•˜λ©΄ 될 것 κ°™λ‹€.

 

그리고 μ•žμ„  μ½”λ“œμ˜ appleClient 에 λŒ€ν•œ url 에 λŒ€ν•œ Host λ₯Ό ν™˜κ²½ λ³€μˆ˜λ‘œ μ‚¬μš©ν–ˆκ³ ,

dismiss404 μ˜΅μ…˜μ„ ν™œμš©ν•˜μ—¬, 404 μ˜ˆμ™Έλ₯Ό λ¬΄μ‹œν•˜λŠ” λ°©ν–₯으둜 μ„€μ •ν–ˆλ‹€.

true 둜 μ„€μ •ν•˜λ©΄ μ„œλ²„λ‘œλΆ€ν„° 404 응닡 μ‹œ λ”°λ‘œ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚€μ§€ μ•Šκ³ , λ¬΄μ‹œν•œλ‹€.

 

[Custom Logger]

Feign Client λ₯Ό μ‚¬μš©ν•  λ•Œ λͺ¨λ‹ˆν„°λ§μ„ μœ„ν•΄μ„œλŠ”,

μ™ΈλΆ€ API Call 에 logging 을 μ§„ν–‰ν•΄μ•Ό ν•  것이닀.

 

κ·Έ λ•Œ Feign 의 Logger μΆ”상 클래슀λ₯Ό μƒμ†ν•˜μ—¬ Custom 을 μ§„ν–‰ν•  수 μžˆλŠ”λ°,

기본적으둜 μš”μ²­κ³Ό 응닡에 λŒ€ν•΄μ„œλŠ” λ””ν…ŒμΌν•˜κ²Œ logging 을 μ§„ν–‰ν•˜λŠ” 편이 ν–₯ν›„ λͺ¨λ‹ˆν„°λ§μ΄ μˆ˜μ›”ν•  것이닀.

 

class FeignClientCustomLogger : Logger() {

    companion object {
        private val logger = LoggerFactory.getLogger(FeignClientCustomLogger::class.java)
    
        private const val ERROR: String = "error"
        private const val REMOTE_SERVICE: String = "remote-service"
        private const val METHOD: String = "method"
        private const val URL: String = "url"
        private const val HEADERS: String = "headers"
        private const val REQUEST: String = "request"
        private const val RESPONSE: String = "response"
        private const val STATUS: String = "status"
        private const val ERROR: String = "error"
    }

    override fun logRequest(configKey: String, logLevel: Level, request: Request) {
        val log = mapOf<String, String>(
            REMOTE_SERVICE to configKey,
            METHOD to request.httpMethod().name,
            URL to request.url(),
            HEADERS to getHeaders(request).toString(),
            REQUEST to getRequestBody(request)
        )
        logger.info(log)
    }

    override fun logAndRebufferResponse(
        configKey: String,
        logLevel: Level,
        response: Response,
        elapsedTime: Long
    ): Response {
        var chainingResponse = response
        var responseBody: Any = ""
        if (hasResponseBody(response)) {
            val bodyData: ByteArray = Util.toByteArray(response.body().asInputStream())
            responseBody = logger.jsonLogFormat(bodyData)
            chainingResponse = response.toBuilder().body(bodyData).build()
        }
        val log = mapOf(
            REMOTE_SERVICE to configKey,
            RESPONSE to responseBody,
            STATUS to response.status()
        )

        logger.info(log)
        return chainingResponse
    }

    override fun logIOException(
        configKey: String,
        logLevel: Level,
        ioe: IOException,
        elapsedTime: Long
    ): IOException {
        val errorMessage = "Error occured Class: ${ioe.javaClass.simpleName}, Error message: ${ioe.message}"
        val log = mapOf(
            REMOTE_SERVICE to configKey,
            ERROR to errorMessage
        )
        logger.info(log)
        return ioe
    }

    override fun log(configKey: String, format: String, vararg args: Any?) = Unit

    private fun getHeaders(request: Request): Map<String, Any> {
        val headerMap: MutableMap<String, Any> = HashMap()
        request.headers().forEach { entry ->
            headerMap[entry.key] = entry.value.first()
        }
        return headerMap
    }

    private fun getRequestBody(request: Request): String {
        return request.charset()?.let { String(request.body(), request.charset()) } ?: ""
    }

    private fun hasResponseBody(response: Response): Boolean {
        val status: Int = response.status()
        return response.body() != null &&
            !(status == HttpStatus.NO_CONTENT.value() || status == HttpStatus.RESET_CONTENT.value())
    }
}

 

μš”μ²­μ— λŒ€ν•œ logging 으둜

μ™ΈλΆ€ μ„œλΉ„μŠ€ 호슀트,

Http Method,

url,

header 정보,

request payload λ₯Ό ν¬ν•¨ν•˜μ˜€λ‹€.

 

응닡에 λŒ€ν•œ logging 으둜

μ™ΈλΆ€ μ„œλΉ„μŠ€ 호슀트,

response payload,

response status λ₯Ό ν¬ν•¨ν•˜μ˜€λ‹€.

 

IOException 에 λŒ€ν•œ 핸듀링은,

μ™ΈλΆ€ μ„œλΉ„μŠ€μ™€ λ°œμƒν•œ μ—λŸ¬ λ©”μ‹œμ§€λ₯Ό λ‘œκΉ…ν•˜μ˜€λ‹€.

 

μœ„μ™€ 같이 μ§„ν–‰ν•˜λ©΄,

λ”μš± Feign 을 μ΄μš©ν•œ μ™ΈλΆ€ API μš”μ²­ 및 응닡에 λŒ€ν•œ λ‘œκΉ…μ„ μ„ΈλΆ€μ μœΌλ‘œ μ§„ν–‰ν•  수 μžˆμ„ 것이닀.

 

[Custom Request Interceptor]

Feign Client λ₯Ό μ‚¬μš©ν•˜μ—¬ μ„ μ–Έν˜• μΈν„°νŽ˜μ΄μŠ€λ₯Ό μž‘μ„±ν•  λ•Œ,

Request 에 λŒ€ν•œ 곡톡적인 섀정을 μ§„ν–‰ν•  수 μžˆλ‹€.

 

예λ₯Ό λ“€μ–΄, Header λ₯Ό 톡해 값을 전달해야 ν•  경우,

@RequestHeader μ• λ…Έν…Œμ΄μ…˜μ„ ν†΅ν•œ 헀더 값을 전달할 수 μžˆλ‹€.

ν•˜μ§€λ§Œ, κ³΅ν†΅μ μœΌλ‘œ 각쒅 Client λ₯Ό μ‚¬μš©ν•  λ•Œμ˜ 헀더 μ„€μ •μœΌλ‘œ Feign 의 RequestInterceptor μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜μ—¬,

Custom ν•  수 μžˆλ‹€.

 

class TraceHeaderInterceptor : RequestInterceptor {

	companion object {
    	private const val X_REQUEST_ID: String = "x-request-id"
        private const val REQUEST_ID: String = "request-id"
    }

    override fun apply(template: RequestTemplate) {
        template.header(X_REQUEST_ID, MDC.get(REQUEST_ID))
    }
}

 

μ΄λ ‡κ²Œ λͺ¨λ‹ˆν„°λ§μ„ μœ„ν•œ Trace λ₯Ό Logging ν•˜λ €κ±°λ‚˜,

νŠΉμ • 헀더 값을 μΆ”κ°€ν•  λ•Œ μ‚¬μš©ν•  수 μžˆμ„ 것이닀.

 

[Custom Error Decoder]

Feign μ—μ„œλŠ” ErrorDecoder λΌλŠ” μΈν„°νŽ˜μ΄μŠ€λ₯Ό μ§€μ›ν•œλ‹€.

μ—λŸ¬μ— λŒ€ν•œ 핸듀링을 μ§„ν–‰ν•  수 있게 ν•΄μ£ΌλŠ” μΈν„°νŽ˜μ΄μŠ€μΈλ°,

 

νŠΉλ³„ν•œ 섀정이 μ—†λ‹€λ©΄ κ·Έ κ΅¬ν˜„μ²΄μΈ Default ν΄λž˜μŠ€κ°€ λ™μž‘ν•œλ‹€.

    @Override
    public Exception decode(String methodKey, Response response) {
      FeignException exception = errorStatus(methodKey, response);
      Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER));
      if (retryAfter != null) {
        return new RetryableException(
            response.status(),
            exception.getMessage(),
            response.request().httpMethod(),
            exception,
            retryAfter,
            response.request());
      }
      return exception;
    }

 

Retryable Exception 을 기본적으둜 λ°œμƒμ‹œν‚¨λ‹€.

이 κΈ°λ³Έ 섀정을 μ™ΈλΆ€ μ• ν”Œλ¦¬μΌ€μ΄μ…˜λ“€μ˜ ν˜•νŽΈμ— 맞게 Custom ν•˜κ³  싢을 λ•Œκ°€ μžˆμ„ 것이닀.

 

예λ₯Ό λ“€λ©΄,

μ„œλ²„κ°€ μ£½μ–΄ μžˆμ„ λ•Œμ— μ»¨ν…Œμ΄λ„ˆ 응닡에 λŒ€ν•œ μ„€μ •

νƒ€μž„ 아웃 μ‹œμ˜ μ„€μ • λ“±

λ‹€μ–‘ν•œ Custom 상황이 μžˆμ„ 것이닀.

 

이 뢀뢄을 κ΅¬ν˜„ν•˜λ©΄ 될 것 κ°™λ‹€.

 

class CustomErrorDecoder(
	private val objectMapper: ObjectMapper
): ErrorDecoder {

    override fun decode(methodKey: String, response: Response): Exception {
        val exception = FeignException.errorStatus(methodKey, response)
        val retryStatus = listOf(
            HttpStatus.BAD_GATEWAY.value(),
            HttpStatus.SERVICE_UNAVAILABLE.value(),
            HttpStatus.GATEWAY_TIMEOUT.value()
        )
        
        // μΆ”κ°€μ μœΌλ‘œ Apple Service μ—λŸ¬ 핸듀링에 ν•„μš”ν•œ μƒνƒœλ₯Ό μ •μ˜ν–ˆλ‹€.
        val appleServerErrorStatus = listof(
        	HttpStatus.BAD_REQUEST.value(),
            HttpStatus.UNAUTHORIZED.value(),
            HttpStatus.NOT_FOUND.value(),
            HttpStatus.INTERNAL_SERVER_ERROR.value()
        )
 
        val errorResponseBytes = response.body().asInputStream().readAllBytes()
        val appleServerErrorResponseDTO = objectMapper.readValue(
            errorResponseBytes,
            AppleServerErrorResponseDTO::class.java
        )
        
        return when (response.status()) {
            in retryStatus -> RetryableException(
                response.status(),
                exception.message,
                response.request().httpMethod(),
                exception,
                null,
                response.request()
            )
            
            in appleServerErrorStatus -> InternalServerAppleException(
                errorResponseBody = appleServerErrorResponseDTO
            ) // apple service μ—λŸ¬μΌ λ•Œ, Custom Exception 을 λ°œμƒμ‹œμΌœ μƒμœ„ ν΄λž˜μŠ€μ—μ„œ μ œμ–΄ μ§„ν–‰.

            else -> exception
        }
    }
}

 

μ΄λ ‡κ²Œ μ»¨ν…Œμ΄λ„ˆ μ‚¬μš© μ‹œ, 기본적으둜 λ°œμƒν•  수 μžˆλŠ” 502, 503, 504 응닡 μ½”λ“œ 이외에,

apple service 만의 error status code λ₯Ό λ”°λ‘œ μ§„ν–‰ν•˜μ—¬ custom exception 을 λ°œμƒ μ‹œμΌ°λ‹€.

κ·Έλ ‡κ²Œ ν•˜λ©΄ μƒμœ„ apple client service [ apple client λ₯Ό ν˜ΈμΆœν•˜μ—¬ 인프라 단 λΉ„μ¦ˆλ‹ˆμŠ€λ₯Ό μž‘μ„±ν•˜λŠ” 클래슀 ]

μ—μ„œ 핸듀링을 μ§„ν–‰ν•  수 μžˆμ„ 것 κ°™λ‹€.

 

[Default Feign Client Configuration]

μœ„ κΈ°λ³Έ 섀정을 ν•œλ²ˆμ— λ¬Άμ–΄λ³΄μž

 

@Configuration
class DefaultFeignConfig {

    @Bean
    fun customLogger(): Logger = FeignClientCustomLogger()

    @Bean
    fun defaultRetryPolicy(): Retryer {
        return Retryer.Default()
    }

    @Bean
    fun errorDecoder(): ErrorDecoder {
        return CustomErrorDecoder()
    }

    @Bean
    fun headerInterceptor(): TraceHeaderInterceptor {
        return TraceHeaderInterceptor()
    }
}

 

κΈ°λ³Έ 섀정은 λ‹€μŒκ³Ό κ°™λ‹€.

ν•˜μ§€λ§Œ apple service μ—λŠ” λ”°λ‘œ Retry λ₯Ό μ§„ν–‰ν•˜μ§€ μ•Šκ³  μ‹Άλ‹€.

 

κ·Έλ ‡κ²Œ ν•˜κΈ° μœ„ν•΄μ„œ, apple service configration 으둜 override ν•˜κ³  μ‹Άλ‹€.

 

class AppleClientConfig(
    private val objectMapper: ObjectMapper,
) {

    @Bean
    fun appleClientHeaderInterceptor() = AppleClientHeaderInterceptor()

    @Bean
    fun errorDecoder(): ErrorDecoder = AppleCustomErrorDecoder(objectMapper)
}

 

μ΄λ ‡κ²Œ μΆ”κ°€ ν›„ AppleClient μΈν„°νŽ˜μ΄μŠ€μ˜ 

@FeignClient μ• λ…Έν…Œμ΄μ…˜ configuration 속성에 μΆ”κ°€ν•˜λ©΄ 될 것 κ°™λ‹€.

 

@FeignClient(
    name = "appleClient",
    configuration = [AppleClientConfig::class.java]
)

 

μ΄λ ‡κ²Œ ν•˜λ©΄,

apple service 만의 feign client 섀정이 μ™„μ„± λœλ‹€.

 

[정리]

Open Feign Client λ₯Ό μ‚¬μš©ν•˜λ©°,

μ‹€μ œ μš΄μ˜μ—μ„œ 보닀 λ””ν…ŒμΌν•œ 섀정을 톡해 μ–»μ–΄κ°„ κ²½ν—˜κ³Ό 정보λ₯Ό 정리해보고 μ‹Άμ—ˆλ‹€.

이 κΈ€λ‘œ λ§Žμ€ 뢄듀이 Open Feign 섀정을 νŽΈν•˜κ²Œ ν•˜μ…¨μœΌλ©΄ ν•˜λŠ” λ°”λž¨μ΄λ‹€.

 

Open Feign 은 정말 μ‚¬μš©ν•˜λ©΄ ν•  수둝 νŽΈν•œ http client 라 생각 λœλ‹€.

동기 ν˜ΈμΆœμ— μžˆμ–΄μ„œ κΈ°λ³Έ 적으둜 μž‘μ„±ν•΄μ•Ό ν•  μ½”λ“œλ„ 적고, 가독성이 정말 μ’‹κ³  κ·Έ μ™Έ λ§Žμ€ μž₯점듀이 μžˆλ‹€.

 

μ΄λ²ˆμ— Spring 6 μ—μ„œ Rest Client λΌμ΄λΈŒλ¦¬κ°€ λ‚˜μ™”λŠ”λ°,

WebClient 의 λΆˆν•„μš”ν•œ 단점을 κ°œμ„ ν•œ 동기 호좜 λͺ©μ μ˜ http client 이닀.

μ‚¬μš© 방식과 체이닝 νŒ¨ν„΄ λ˜ν•œ λ™μΌν•˜λ˜λ°,

이 κΈ°μˆ λ„ ν•œλ²ˆ μ‚¬μš©ν•΄λ³΄κ³  μž₯점 정리와 Feign 과의 비ꡐλ₯Ό 해보고 μ‹Άλ‹€.

 

κΈ΄ κΈ€ μ½μ–΄μ£Όμ…”μ„œ κ°μ‚¬ν•©λ‹ˆλ‹€~

λ°˜μ‘ν˜•