문제의 시작

FeignClient는 선언형 client라서 편하지만, 실제 요청은 내부 HTTP client 구현체의 제약을 그대로 받는다. 그래서 annotation만 맞아 보여도 특정 method가 실패할 수 있다. 이 글은 PATCH 요청이 동작하지 않던 상황을 따라가며 어떤 지점을 확인해야 하는지 정리한 기록이다.

FeignClient 는 Spring Cloud 의 HTTP client 이다. microservice 등의 멀티모듈 환경 구성시 HTTP request 와 response 를 쉽게 주고받을 수 있도록 한다.

RestTemplate 과 WebClient 등의 선택지도 있지만 이들보다 코드가 훨씬 보기 편하고 간결하다는 장점이 있다.

구현하면서 확인한 흐름

implementation("org.springframework.cloud:spring-cloud-starter-feign:1.4.7.RELEASE")

프로젝트 버전에 맞춰 dependency를 추가한다.

@FeignClient(name = "feignclient")
sealed interface FeignController {
    @PatchMapping("/test")
    fun test(
        @RequestBody
        body: String,
    ): String
}

한 모듈에서는 Feign client를 이렇게 선언하고,

@RestController
class Controller {
    @PatchMapping("/test")
    fun test(
        @RequestBody
        body: String,
    ): String {
        return "test"
    }
}

다른 모듈에서는 이에 대응하는 controller를 열어둔다. URI, 요청 인자, response type이 맞으면 두 모듈은 정상적으로 통신한다. 문제는 PATCH method를 사용할 때만 오류가 발생했다는 점이었다. 우선 로그부터 확인했다.

java.net.ProtocolException: Invalid HTTP method: PATCH
	at java.base/java.net.HttpURLConnection.setRequestMethod(HttpURLConnection.java:489) ~[na:na]
	at java.base/sun.net.www.protocol.http.HttpURLConnection.setRequestMethod(HttpURLConnection.java:598) ~[na:na]
	at feign.Client$Default.convertAndSend(Client.java:171) ~[feign-core-13.1.jar:na]
	at feign.Client$Default.execute(Client.java:106) ~[feign-core-13.1.jar:na]
	at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:100) ~[feign-core-13.1.jar:na]
	at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:70) ~[feign-core-13.1.jar:na]
	at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:99) ~[feign-core-13.1.jar:na]
	at jdk.proxy3/jdk.proxy3.$Proxy73.test(Unknown Source) ~[na:na]

일반적인 HTTP method인 PATCH가 invalid라고 나온다. 두 번째 줄에 원인이 보인다. FeignClient는 별도 설정이 없으면 HTTP 통신에 Java의 HttpURLConnection을 사용한다. IntelliJ에서 해당 library code를 열어보면 제한이 바로 드러난다.

    /* valid HTTP methods */
    private static final String[] methods = {
        "GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE"
    };

지원되는 method 목록이 이렇게 hard-coded 되어 있다. 배경을 잠깐 보면 HTTP/1.1 RFC 2068은 1997년 1월에 발표되었다. https://www.rfc-editor.org/info/rfc2068 Java 1.1에 포함된 HttpURLConnection은 이 시기의 구현체다. 반면 PATCH는 2010년 3월에 발표된 RFC 5789에 포함되었다. 부분 수정을 위한 method라서 전체 교체를 의미하는 PUT과 semantics가 다르고, 이 늦은 표준화 시점이 오래된 HTTP client 구현체의 제약으로 남아 있는 셈이다.

해결하려면 FeignClient가 사용하는 HTTP client 구현체를 바꿔야 한다. 여기서는 OkHttp를 사용했다.

implementation("io.github.openfeign","feign-okhttp")

dependency를 추가한 뒤 Feign이 HTTP 통신에 OkHttp client를 사용하도록 configuration을 등록한다. client bean을 만들고, 해당 configuration을 @FeignClient annotation의 configuration 값에 연결하면 된다.

@Configuration
class FeignOkHttpConfiguration {
    @Bean
    fun client(): OkHttpClient {
        return OkHttpClient()
    }
}
@FeignClient(name = "feign", configuration = [FeignOkHttpConfiguration::class])

디버깅 기준

HTTP client 문제는 controller나 API spec만 보면 잘 보이지 않는다. Feign 설정, underlying client, dependency version, method support를 함께 봐야 한다. 이런 bug는 추상화가 어디까지 책임지고 어디서부터 실제 구현체로 내려가는지를 확인하는 훈련이 된다.