๊ฐ์
์ต๊ทผ OpenFeign์ ์ฌ์ฉํ ๋ ์ด์ํ ๋ฌธ์ ๋ฅผ ๊ฒช์๋ค.
@FeignClient(name = "test-client")
public interface TestClient {
@GetMapping("/test")
String getData(String query);
}
์์ ๊ฐ์ด ์ ์๋ OpenFeignํด๋ผ์ด์ธํธ๋ฅผ ํตํด A ์๋ฒ์์ B ์๋ฒ์ API๋ฅผ ํธ์ถํ๋๋ฐ, 405 Method Not Allowed ์๋ฌ๊ฐ ๋ฐ์ํ๋ค. ๋ถ๋ช B ์๋ฒ์๋ ํด๋น API๊ฐ GET ๋ฉ์๋๋ก ์ ํํ ์ ์๋์ด ์๋๋ฐ๋ ๋ง์ด๋ค.
@Slf4j
public class FeignLoggingInterceptorConfig {
@Bean
public RequestInterceptor requestLoggingInterceptor() {
return template -> log.info("[Feign Request] method={}, url={}, query={}, body={}",
template.method(), template.url(), template.queryLine(), (template.body() != null) ? new String(template.body()) : "null");
}
}
A ์๋ฒ ๋ก๊ทธ: c.k.n.i.r.FeignLoggingInterceptorConfig : [Feign Request] method=GET, url=/test, query=, body=test
๋ฌธ์ ๋ฅผ ํ์ ํ๊ธฐ ์ํด A์๋ฒ์ ์์ ๊ฐ์ด ๋ก๊ทธ๋ฅผ ๋จ๊ธฐ๋๋ก ์ค์ ํ๊ณ ํ์ธํ์ ๋๋ ๋ถ๋ช A์๋ฒ์์๋ GET ๋ฐฉ์์ผ๋ก ์์ฒญ์ ๋ณด๋ด๊ณ ์์์ ํ์ธํ ์ ์์๋ค. ๊ทผ๋ฐ ๊ทธ๋ฐ๋ฐ๋ ๋ถ๊ตฌํ๊ณ B ์๋ฒ์์๋ ๋ค์๊ณผ ๊ฐ์ด POST๋ก ์ ๋ฌ๋ฐ์๋ค๋ ๋ก๊ทธ๊ฐ ์ถ๋ ฅ๋๋ค.
B ์๋ฒ ๋ก๊ทธ: DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' is not supported]
์ฆ A ์๋ฒ์์๋ GET ์์ฒญ์ ๋ณด๋๋ค๊ณ ์๊ฐํ์ง๋ง ์ค์ B ์๋ฒ์์๋ POST ์์ฒญ์ด ๋ค์ด์จ ๊ฒ์ด๋ค. ๋๋์ฒด ์ด๊ฒ ๋ฌด์จ ์ผ์ผ๊น?
์์ธ
์ฌ์ค ๋์ฐ๋ฏธ๊ฐ ์ข๋ค๋ฉด A ์๋ฒ์ ๋ก๊ทธ๋ฅผ ๋ณด๊ณ ๋ฌธ์ ์ ๋จ์๋ฅผ ์กฐ๊ธ์ด๋๋ง ํ์ธํ ์ ์๊ธด ํ๋ค. ์ A ์๋ฒ์ ๋ก๊ทธ๋ฅผ ๋ค์ ๋ณด๋ฉด ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ(query)์ ๊ฐ์ด ํฌํจ๋์ง ์๊ณ HTTP body์ ๊ฐ์ด ๋ด๊ฒจ ์๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
@FeignClient(name = "test-client")
public interface TestClient {
/**
* @RequestParam ์์ — ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ฝ๋
*/
@GetMapping("/test")
String getData(String query);
/**
* @RequestParam ์์ — ์ ์ ๋์
*/
@GetMapping("/test")
String getData(@RequestParam String query);
}
๊ฐ๋ฐ์์ ์๋๋ GET /test?query=value์ฒ๋ผ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ก ์ ์กํ๋ ค๋ ๊ฒ์ด์์ง๋ง, OpenFeign์ GET์ธ๋ฐ '@RequestParam, @PathVariable, @SpringQueryMap.. ๋ฑ'๊ณผ ๊ฐ์ HTTP ๋ฐ์ธ๋ฉ ์ด๋ ธํ ์ด์ ์ด ์ง์ ๋์ง ์์ ์ค์ ๋ก ์ ์กํ ๋ ์ ๋ฌ ๊ฐ์ ๋ณธ๋ฌธ(body)์ผ๋ก ์ฒ๋ฆฌํ๋ ค๊ณ POST๋ก ๋ณํํด ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฒ์ด๋ค.
์ฌ์ค ๋ ์ ํํ๋ Feign์ด ์ง์ HTTP ๋ฉ์๋๋ฅผ POST๋ก ๋ฐ๊พธ๋ ๊ฒ์ ์๋๊ณ Feign์ด ๋ด๋ถ์ ์ผ๋ก ์ฌ์ฉํ๋ Java์ ํ์ค HTTP ํด๋ผ์ด์ธํธ(HttpURLConnection)๊ฐ ๋ฐ๊พผ ๊ฒ์ด๋ค.
๊ทธ๋ ๋ค๋ฉด HttpURLConnection์ ์ GET์ POST๋ก ๋ฐ๊พธ๋๊ฐ?
HTTP ํ์ค(RFC 7231)์ ๋ฐ๋ฅด๋ฉด GET ์์ฒญ์ด body๋ฅผ ๊ฐ์ง๋ฉด ์ผ๋ถ ๊ตฌํ์ฒด๊ฐ ๋์ํ์ง ์์ ์๋ ์๋ค๊ณ ํ๋ค.
๊ทธ๋ ๊ธฐ์ ์ฌ์ฉ์๊ฐ GET์ ์ฌ์ฉํ๊ณ ์ ํ๋ฉด์ '@RequestParam, @PathVariable, @SpringQueryMap.. ๋ฑ' HTTP ๋ฐ์ธ๋ฉ ์ด๋ ธํ ์ด์ ์ ์ง์ ํด์ฃผ์ง ์์ผ๋ฉด HttpURLConnection์ "์, body๋ฅผ ์ฐ๊ฒ ๋ค๋ ๊ฑฐ๊ตฌ๋"๋ผ๊ณ ํด์์ ํด๋ฒ๋ฆฌ๊ณ RFC 7231 ๊ท์น์ ๋ฐ๋ผ GET์ Body๊ฐ ๋ค์ด์๊ธฐ ๋๋ฌธ์ HttpURLConnection๋ 'ํธํ์ฑ ์ ์ง ์ฐจ์์์' ์๋์ผ๋ก POST๋ก ๋ฐ๊พธ๋ ์ฐํ ๋ก์ง์ ์ํํ๋ค.
๋ช ์์ ์ธ ์์ธ๋ฅผ ๋์ง๋ ๋์ ์๋ฌต์ ์ผ๋ก HTTP ๋ฉ์๋๋ฅผ ๋ฐ๊พธ์ด ํด๋ผ์ด์ธํธ๊ฐ ๊ณ์ ๋์ํ๋๋ก ํ๊ธฐ ์ํ ๋ ๊ฑฐ์ ํธํ ๋ก์ง์ธ ๊ฒ์ด๋ค.
์ฃผ์์๋ ์ฐ์ฌ์์ง๋ง 'Backward compatibility(ํ์ ํธํ์ฑ, ๋๋ ํ์ ํธํ)'๋ ์๋ก์ด ์์คํ ์ด๋ ์ํํธ์จ์ด๊ฐ ์ด์ ๋ฒ์ ์์ ๋ง๋ค์ด์ง ํ๋ก๊ทธ๋จ์ด๋ ๋ฐ์ดํฐ์๋ ํธํ๋๋ ์ฑ์ง์ ๋งํ๋ค. ๊ณผ๊ฑฐ์๋ ๋ง์ ๊ฐ๋ฐ์๋ค์ด HTTP ๋ช ์ธ๋ฅผ ์๊ฒฉํ ๋ฐ๋ฅด์ง ์์๊ณ GET ์์ฒญ์๋ body๋ฅผ ๋ถ์ด๋ ค๋ ์๋๊ฐ ์์์ง๋ง ์ด๋ฅผ ํ์ฉํ์ง ์์ผ๋ฉด ๊ธฐ์กด ์ฝ๋๊ฐ ๊นจ์ง ์ ์์ด์ HttpURLConnection์์๋ ์กฐ์ฉํ POST๋ก ์ ํํ๋ ๋ฐฉ์์ผ๋ก 'ํธํ์ฑ ์ ์ง'๋ฅผ ์ ํํ ๊ฒ์ด๋ค.
HttpURLConnection๋ ๊ทธ๋ ๋ค๋ ๊ฑฐ๊ณ ๋ค์๊ณผ ๊ฐ์ด Feign์ด ์ฌ์ฉํ๋ HTTP ํด๋ผ์ด์ธํธ๋ฅผ HttpClient๋์ ๋ค๋ฅธ ํด๋ผ์ด์ธํธ์ธ Apcache HTTP Client 5๋ฅผ ์ฌ์ฉํ๋ฉด Apache Client๋ GET ์์ฒญ์ body๊ฐ ์์ด๋ ๋ด๋ถ์ ์ผ๋ก HTTP ๋ฉ์๋๋ฅผ ๋ณ๊ฒฝํ์ง ์์ผ๋ฏ๋ก GET ๋ฐฉ์์ผ๋ก ๊ณ์ ์ ์งํ ์ ์๋ค.(OpenFeign/feign #issues/832)
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
</dependency>
๋ค๋ง ํ์ ์์ HTTP Client๋ฅผ ๋ฐ๊ฟ ์ฐ๋ ๊ฒ์ ์ด๋ค ์ฌ์ด๋์ดํํธ๊ฐ ์์์ง ๋ชจ๋ฅด๊ธฐ ๋๋ฌธ์, GET์ ์ฌ์ฉํ ๋๋ ์ด๋ ธํ ์ด์ ์ ์ ๋ถ์ฌ์คฌ๋์ง ๊ผญ ํ์ธํ์.
OpenFeign ์คํ์์ค ๊ธฐ์ฌ
s.n.www.protocol.http.HttpURLConnection : sun.net.www.MessageHeader@68c984c97 pairs: {POST /test HTTP/1.1: null}{Content-Type: text/plain;charset=UTF-8}{Accept: */*}{User-Agent: Java/17.0.14}{Host: localhost:8090}{Connection: keep-alive}{Content-Length: 4}
์ฌ์ค ๋ก๊น ๋ ๋ฒจ์ DEBUG๋ก ์ค์ ํ๋ฉด ์์ ๊ฐ์ A ์๋ฒ์์ HttpURLConnection์ด GET์ POST๋ก ๋ฐ๊พธ๋ ๊ณผ์ ์ ๋ก๊ทธ๊ฐ ์๋์ผ๋ก ์ถ๋ ฅ๋๊ธด ํ๋ค. ๋ค๋ง.. ํ์ค์ ์ผ๋ก DEBUG ๋ ๋ฒจ์ ๋๋ฌด ๋ง์ ๋ถํ์ํ ๋ก๊ทธ๋ฅผ ๋ฐ์์ํค๊ธฐ ๋๋ฌธ์ ์ฐ๋ฆฌ ํ์ ์ผ๋ฐ์ ์ผ๋ก Spring์ ๋ก๊น ๋ ๋ฒจ์ INFO๋ก ์ค์ ํด ๋๊ณ ์ฌ์ฉํ๊ณ ์์๊ณ , ๊ทธ๋ ๋ค ๋ณด๋ ์ด์ฒ๋ผ ์ค์ํ ํํธ๋ฅผ ๋ด๊ณ ์๋ ๋ก๊ทธ๊ฐ ๋ ธ์ถ๋์ง ์์๋ค. ๊ทธ๋์ ์ ๋ฐ ๋ก๊ทธ๊ฐ ์๋์ผ๋ก ์ถ๋ ฅ๋๋์ง ๋ชฐ๋ผ ์ง์ OpenFeign ์ ์ฉ RequestInterceptor๋ฅผ ๊ตฌํํด INFO ์์ค์ผ๋ก ๋ก๊ทธ๋ฅผ ์ฐ์๋ ๊ฒ์ด์๊ณ , Feign ๋ ๋ฒจ์์๋ ์ฌ์ ํ GET ์์ฒญ์ผ๋ก๋ง ์ถ๋ ฅ๋๊ณ ์์๋ ๊ฒ์ด์๋ค.
๊ทธ๋๋ง ๋๋ ์์ฒญ์ ๋ฐ๋ B ์๋ฒ๋ ์ง์ ๊ฐ๋ฐํ๊ณ ์์๊ธฐ ๋๋ฌธ์ B์๋ฒ์ ๋ก๊ทธ๋ฅผ ๋น๊ตํ๋ฉฐ ๋น ๋ฅด๊ฒ ์์ธ์ ์ถ์ ํ ์ ์์์ง๋ง ๊ถํ์ด ์๋ ์ธ๋ถ ์๋ฒ๋ ํ์ฌ API์ ์์ฒญ์ ๋ณด๋๋ ์ํฉ์ด๋ผ๋ฉด, ์ด๋ฌํ ์์ธ ๋ชจ๋ฅผ 405 ์ค๋ฅ๋ฅผ ํ์ ํ๋ ๋ฐ ํจ์ฌ ๋ ๋ง์ ์๊ฐ์ ํ๋นํ์ ๊ฒ์ด๋ผ ์๊ฐํ๋ค.
๊ฐ์ฅ ์ค์ํ ๊ฑด GET ์์ฒญ์ ์ฌ์ฉํ ๋๋ ๋ฐ๋์ HTTP ๋ฐ์ธ๋ฉ ์ด๋ ธํ ์ด์ (@RequestParam, @PathVariable ๋ฑ)์ ์ ํํ ๋ถ์ฌ์คฌ๋์ง ํ์ธํ๋ ๊ฒ์ด์ง๋ง ๊ฐ๋ฐํ๋ค๋ณด๋ฉด... ์ฅ์ ํฐ์ฒ๋ผ ๊ผญ ํ ๋ฒ์ฉ ๋ณ๊ฑฐ ์๋ ์ด๋ฐ ๊ฒ๋ค์ด ์ ๋ณด์ด๋ ๊ฒฝ์ฐ๊ฐ ํ ๋ฒ์ฉ ์๋ค. ๊ทธ๋์ ์ ์ด์ "GET ์์ฒญ์ธ๋ฐ ํ๋ผ๋ฏธํฐ์ HTTP ๋ฐ์ธ๋ฉ ์ด๋ ธํ ์ด์ ์ด ์๋ค๋ฉด ๊ฒฝ๊ณ ๋ก๊ทธ๋ฅผ ์ถ๋ ฅํด ์ฃผ๋ฉด ์ ๋ ๊น?"๋ผ๋ ์๊ฐ์ ํ๊ณ ์ด ์๊ฐ์ ๋ฐํ์ผ๋ก OpenFeign/feign Issue#2872์ ๊ด๋ จ ๋ด์ฉ์ ์ฌ๋ ธ๋ค. ๊ทธ๋ฆฌ๊ณ ์ง์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด ๋ณด๊ธฐ ์ํด OpenFeign์ ์ฝ๋๋ฅผ ๋ถ์ํ๋ค๊ฐ ํต์ฌ ํฌ์ธํธ๊ฐ SpringMvcContract.java์ parseAndValidateMetadata() ๋ฉ์๋๋ผ๋ ๊ฒ์ ํ์ ํ๊ฒ ๋์๋ค.
์ด ๋ฉ์๋๋ OpenFeign์์ ์ฌ์ฉํ๋ ์ค์ํ ๋ก์ง์ธ๋ฐ Spring MVC์ ์ด๋ ธํ ์ด์ ๋ค(@RequestMapping, @GetMapping, @RequestParam, @PathVariable ๋ฑ)์ ๊ธฐ๋ฐ์ผ๋ก FeignClient ์ธํฐํ์ด์ค์ ๋ฉ์๋๋ค์ ๋ถ์ํ๊ณ HTTP ์์ฒญ ์ ๋ณด๋ก ๋งคํํ๋ ์ญํ ์ ํ๋ค.
๊ทธ๋ฐ๋ฐ ์ฌ๊ธฐ์ ๋ป๋ฐ์ ์ฌ์ค์ ์๊ฒ ๋์๋๋ฐ ๋ด๊ฐ ์์ ํ๋ ค๊ณ ํ๋ SpringMvcContract.java๋ OpenFeign์ feign-core์ ์๋ ๊ฒ ์๋๋ผ spring-cloud/spring-cloud-openfeign ํ๋ก์ ํธ์ ์ํด ์์๋ค. ์์๋ณด๋๊น ๋์ ์ฐจ์ด์ ์ ๋ํด์๋ ๋ค์๊ณผ ๊ฐ์๋ค.
- OpenFeign/feign: Feign์ ํต์ฌ(core) ๋ผ์ด๋ธ๋ฌ๋ฆฌ, ํ๋ ์์ํฌ์ ๋ ๋ฆฝ์ ์ธ ๊ธฐ๋ฅ์ ์ ๊ณต
- spring-cloud/spring-cloud-openfeign: Feign์ Spring ํ๊ฒฝ์ ๋ง๊ฒ ํตํฉํ๊ธฐ ์ํ ์ด๋ํฐ, Spring MVC ์คํ์ผ์ ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉ ๊ฐ๋ฅํ๊ฒ ํด ์ค
์ค์ ๋ก ์ฐ๋ฆฌ ํ์ spring-cloud-openfeign์ ์ฌ์ฉํ๊ณ ์์๊ณ , ์ฃผ๋ณ ๋ค๋ฅธ ํ์ฌ์ ์ง์ธ๋ค์๊ฒ๋ ๋ฌผ์ด๋ณด๋ ์คํ๋ง์ ์ฌ์ฉํ๋ค๋ฉด ๋๋ถ๋ถ ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉ ์ค์ด์๋ค. ๊ทธ๋์ ์ด์๋ฅผ OpenFeign core๊ฐ ์๋ ์ค์ ์ ์ฉ ๋์์ธ spring-cloud-openfeign/Issues #1191์ ๋ค์ ์ฌ๋ ธ๊ณ , ์ต์ข ์ ์ผ๋ก๋ PR #1194๋ฅผ ํตํด ๊ธฐ์ฌํ๋ค.
๋ฆด๋ฆฌ์ฆ๊ฐ ๋ฐฐํฌ๋๋ค๋ฉด ์ด์ ๋ GET ๋ฉ์๋์ HTTP ๋ฐ์ธ๋ฉ ์ด๋ ธํ ์ด์ ์ด ๋น ์ง ๊ฒฝ์ฐ ๋ค์๊ณผ ๊ฐ์ด ๋ช ํํ ๊ฒฝ๊ณ ๋ฉ์์ง๊ฐ ์ถ๋ ฅ๋์ด ๊ฐ๋ฐ์๊ฐ ์ฝ๊ฒ ๋ฌธ์ ๋ฅผ ์ธ์งํ ์ ์์ ๊ฒ์ด๋ค.
[OpenFeign Warning] Feign method 'public abstract Response com.example.Client.getSomething(java.lang.String)' is declared as GET with parameters, but none of the parameters are annotated (e.g. @RequestParam, @RequestHeader, @PathVariable, etc). This may result in fallback to POST at runtime. Consider explicitly annotating parameters.
spring-cloud-openfeign์ ์ ์ง๋ณด์ ๋ชจ๋์ด๋ค.
๊ทธ๋ฆฌ๊ณ ๋ ์ถ๊ฐ๋ก ์๊ฒ ๋ ์ ์ด ์๋๋ฐ spring-cloud-openfeign์ Spring์ด ์์ฒด์ ์ผ๋ก HTTP ํด๋ผ์ด์ธํธ ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํ๊ฒ ๋จ์ ๋ฐ๋ผ 2022.0.0 ๋ฒ์ ๋ถํฐ ์ ์ง๋ณด์ ๋ชจ๋์ ๋ค์ด๊ฐ๊ณ ์ ๊ท ๊ธฐ๋ฅ ๊ฐ๋ฐ์ ๋ ์ด์ ์งํ๋์ง ์๋๋ค๊ณ ํ๋ค.
๊ทธ๋์ ํ์ฌ๋ ํฌ๋ฆฌํฐ์ปฌ ๋ฒ๊ทธ ์์ ์ด๋ ์์ ์ฑ ์ ์ง ๋ชฉ์ ์ PR๋ง ์์ฉํ๊ณ ๋ ์ด์ ํ๋ฐํ ๊ธฐ๋ฅ ์ถ๊ฐ๋ ๊ฐ์ ์ ์งํ๋์ง ์๋๋ค๊ณ ํ๋ ์์๋๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค.
'BackEnd๐ฑ > Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
AbstractAggregateRoot์ ApplicationEventPublisher ๋์ ๊ณผ์ (0) | 2025.06.28 |
---|---|
Hibernate 5.x โ 6.x QueryDSL ๊ด๋ จ ์ด์ (0) | 2025.03.28 |
Spring ํธ๋์ญ์ ์ ์ธ์ ์ด๋ป๊ฒ ๋กค๋ฐฑ ๋ ๊น? -2ํธ(feat. rollback-only) (1) | 2024.12.22 |
Spring ํธ๋์ญ์ ์ ์ธ์ ์ด๋ป๊ฒ ๋กค๋ฐฑ ๋ ๊น? -1ํธ (2) | 2024.10.04 |
[gradle] implementation, api ์ฐจ์ด (0) | 2024.09.07 |
[JPA] deleteAll(), deleteAllInBatch(), deleteInBatch() ์ ๋ฆฌ (0) | 2024.08.12 |
๋๊ธ