์ ๋ฒ ๊ธ์์๋ WireMock์ ํ์ฉํด Clova AI(์ธ๋ถ) ์๋ฒ๋ฅผ ๋ชจํนํ๋ ๋ฐฉ๋ฒ์ ๋ค๋ค์ต๋๋ค. ์ด๋ฅผ ํตํด Clova ์๋ฒ๋ฅผ ํธ์ถํ์ง ์์์ผ๋ก์จ ๋น์ฉ ๋ฐ์ ์์ด, @FeignClient ์ logger-level: FULL ์ค์ ์ด ์ ์ ์ฉ ๋์๋์ง๋ฅผ ํ์ธํ๋ ๊ณผ์ ์ ์ดํด๋ดค์ต๋๋ค.
์ด๋ฒ ๊ธ์์๋ INFO ๋ ๋ฒจ์์ Feign ๋ก๊ทธ๋ฅผ ์ถ๋ ฅํ๋๋ก ์ปค์คํฐ๋ง์ด์งํ๋ ๋ฐฉ๋ฒ์ ์๊ฐํ๋ ค ํ์ง๋ง, ์ด๋ฏธ ์ด๋ฅผ ๋ค๋ฃฌ ์ข์ ๊ธ๋ค์ด ๋ง์ ํด๋น ์ฃผ์ ๋ฅผ ๊ฐ๋จํ ๋์ด๊ฐ๊ณ , OpenFeign์ @Configuration ์ค๊ณ ๊ณผ์ ์์ ๊ณ ๋ฏผํ๋ ์ ์ ๊ณต์ ํ๊ณ ์ ํฉ๋๋ค.
Feign ์ปค์คํฐ๋ง์ด์ง ๊ด๋ จ ์ข์ ๊ธ
- ์ฐ์ํ feign ์ ์ฉ๊ธฐ
- ๋ง๋๋ ๊ฐ๋ฐ์๋์ OpenFeign ์ค๋ช ๊ณผ Github
- (tistory) FeignClient Logging ๋ฐฉ๋ฒ ์ ๋ฆฌ
1. INFO ๋ ๋ฒจ ๋ก๊ทธ๋ฅผ ์ํ @Configuration ์ค์
INFO ๋ก๊ทธ๊ฐ ๋ชจ๋ @FeignClient์ ์ ์ฉ๋๋๋ก ํ๊ธฐ ์ํด @Configuration์ ํ์ฉํ์ฌ ์ค์ ํด๋์ค๋ฅผ ์์ฑํ์ต๋๋ค.
@Configuration์ ๋ด๋ถ์ @Bean ๋ฉ์๋๋ค์ CGLIB ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํตํด ์ฒ๋ฆฌํ๋ฉฐ, ์๋์ผ๋ก ๋ฑ๋ก๋ ๋น์ ์ฑ๊ธํค์ผ๋ก ๊ด๋ฆฌํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ , @Configuration ๋ด์์ ์ค์ ๋ ๊ฒ๋ค์ @EnableFeignClients๊ฐ ํ์ํ๋ ๋ชจ๋ Feign ํด๋ผ์ด์ธํธ์ ์ ์ฉ๋ฉ๋๋ค.
๋ก๊ทธ ์ปค์คํฐ๋ง์ด์ง ์ฝ๋
logRequest์ logAndRebufferResponse ๋ฉ์๋๋ฅผ ์ฌ์ ์ํด INFO ๋ ๋ฒจ์์ ํ์ํ ๋ก๊ทธ๋ฅผ ์ถ๋ ฅํ๋๋ก ์ค์ ํฉ๋๋ค. ์ ์ฒด ์ฝ๋๋ Github์ ์์ต๋๋ค.
@EnableFeignClients๋ฅผ @SpringBootApplication ์ด ์๋ @Configuration์ ๋ถ์ธ ์ด์
ํด๋น ๊ธ์ ์ฐธ๊ณ ํ์ฌ WebMvcTest, DataJpaTest์ ๊ฐ์ด Feign ๊ด๋ จ ๋น์ด ํ์ํ์ง ์์ ํ ์คํธ์์ ๋ถํ์ํ Feign ๋น์ด ์๋์ผ๋ก ์ค์ ๋๋ ๊ฒ์ ๋ฐฉ์งํฉ๋๋ค.
@EnableFeignClients("bsise.server")
@Configuration
public class FeignGlobalConfig {
@Bean
Logger.Level feignLoggerLevel() {
return BASIC; // Log.LEVEL ์ NONE -> BASIC -> HEADERS -> FULL ์์ผ๋ก ์์ธํ๊ฒ ๋ก๊ทธ๊ฐ ์ถ๋ ฅ๋จ
}
@Bean
public CustomFeignLogging customFeignLogging() {
return new CustomFeignLogging();
}
@Slf4j
public static class CustomFeignLogging extends Logger {
@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
if (logLevel.ordinal() >= HEADERS.ordinal()) {
super.logRequest(configKey, logLevel, request);
return;
}
String stringBody = createRequestStringBody(request);
log.info("[threadId={}] ---> {} {} {} [Body]: {}",
Thread.currentThread().getId(),
request.httpMethod(),
request.url(),
request.protocolVersion(),
stringBody);
}
private String createRequestStringBody(Request request) {
return request.body() == null ? "" : new String(request.body(), StandardCharsets.UTF_8);
}
// ์๋ต์ด 400๋ฒ๋ ์ด์์ด๋ฉด INFO ๋ ๋ฒจ์ด ์๋ WARN / ERROR ๋ก ๋จ๊ธฐ๊ณ ์ ํจ.
@Override
protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime)
throws IOException {
if (logLevel.ordinal() >= HEADERS.ordinal() || response.status() >= 400) {
return super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime);
}
byte[] byteArray = getResponseBodyByteArray(response);
log.info("[threadId={}] <--- {} {} ({}ms) [Body]: {}",
Thread.currentThread().getId(),
response.protocolVersion(),
response.status(),
elapsedTime,
new String(byteArray, StandardCharsets.UTF_8));
return response.toBuilder().body(byteArray).build();
}
private byte[] getResponseBodyByteArray(Response response) throws IOException {
if (response.body() == null) {
return new byte[]{};
}
return StreamUtils.copyToByteArray(response.body().asInputStream());
}
/**
* CustomFeignLogging ์ ๋น์ผ๋ก ๋ฑ๋กํ๊ณ , log()์ format()์ด ์์ผ๋ฉด feign ๋ก๊ทธ ๋ ๋ฒจ์ด HEADERS ์ด์์ผ ๋, DEBUG ๋ ๋ฒจ์ ๋ก๊ทธ๊ฐ ๋์ค์ง ์์
*/
@Override
protected void log(String configKey, String format, Object... args) {
log.debug(format(configKey, format, args));
}
protected String format(String configKey, String format, Object... args) {
return String.format(methodTag(configKey) + format, args);
}
}
}
2. ErrorDecoder ๊ตฌํ
์์ ๊ฐ์ ์ค์ ์ผ๋ก ๋๋ด๋ฉด 300๋ฒ๋~500๋ฒ๋ ์๋ต์ด ์์ ๋, customFeignLogging ์ log() ์ํด DEBUG ๋ ๋ฒจ๋ก ์ถ๋ ฅ๋ฉ๋๋ค. ์ ๋ 400๋ฒ๋์ 500๋ฒ๋ ์๋ต์ด ์์ ๋, ERROR ๋ ๋ฒจ์ ๋ก๊ทธ๋ก ์ถ๋ ฅํ๊ธธ ์ํ์ต๋๋ค. Clova ์๋ฒ๋ฅผ ํธ์ถํ์ง ๋ชปํ๋ ๊ฒ์ ์ ํฌ ํ๋ก์ ํธ ์ ๋งค์ฐ ์ค์ํด์ ์ฆ๊ฐ ๋์ํด์ผ ํ๊ธฐ ๋๋ฌธ์ ๋๋ค. (OpenFeign์ ๊ธฐ๋ณธ์ ์ผ๋ก 200๋ฒ๋ ์๋ต๋ง์ ์ฑ๊ณต์ผ๋ก ๊ฐ์ฃผํ๋ฉฐ, ๊ทธ ์ธ์ ์๋ต์ ๋ํด์๋ ErrorDecoder๊ฐ ๋์ํฉ๋๋ค.)
Implement this method in order to decode an HTTP Response when Response.status() is not in the 2xx range. Please raise application-specific exceptions where possible
* ErrorDecoder ๋ฌธ์ ์ค ์ผ๋ถ
์ด์ ๋ฐ๋ผ, 200๋ฒ๋ ์ธ์ ์๋ต์ ํธ๋ค๋งํ ์ ์๋๋ก ErrorDecoder๋ฅผ ์์ํ ์ค์ ํด๋์ค๋ฅผ ์ถ๊ฐ๋ก ์์ฑํ์ต๋๋ค.
GlobalFeignConfig์ ClovaFeignConfig๋ฅผ ๋ถ๋ฆฌํ ์ด์
- ErrorDecoder๋ Feign ํด๋ผ์ด์ธํธ๋ณ๋ก ๊ฐ๋ณ ์ค์ ์ด ํ์ํ๋ค๊ณ ํ๋จํ์ต๋๋ค.
- ๋ฐ๋ผ์, ClovaFeignConfig ๋ ๋ชจ๋ @FeignClient์ ์ ์ฉ๋์ง ์๋๋ก @Configuration์ ์๋ตํ์ต๋๋ค.
. ์ ์ฒด ์ฝ๋๋ Github์ ์์ต๋๋ค.
@Slf4j
public class ClovaFeignConfig {
@Bean
public CustomClovaFeignErrorDecoder customClovaFeignErrorDecoder() {
return new CustomClovaFeignErrorDecoder();
}
@Slf4j
static class CustomClovaFeignErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
... Response ์์ 300๋ฒ๋ ~ 500๋ฒ๋์ ์ผ ๊ฒฝ์ฐ ๋น์ฆ๋์ค์ ๋ง๊ฒ ์์ธ ์ฒ๋ฆฌ
}
}
}
๊ทธ๋ฆฌ๊ณ , ClovaFeignConfig ๋ฅผ ์ฌ์ฉํ๋ ค๋ @FeignClient ์ ์์ฑ์ ์ถ๊ฐํฉ๋๋ค.
@FeignClient(name = "clova-service", url = "${feign.clova.url}", configuration = ClovaFeignConfig.class)
3. @Configuration ์์ด @Bean ๋ฉ์๋ ํธ์ถ
customFeignErrorDecoder๋ @Bean ๋ฉ์๋๋ก ์ ์๋์์ง๋ง, ํด๋น ํด๋์ค(ClovaFeignConfig)์ @Configuration์ด ์๊ธฐ ๋๋ฌธ์ ๋น์ผ๋ก ๋ฑ๋ก๋์ง๋ง Spring ์ปจํ ์ด๋๊ฐ ์ฑ๊ธํค์ผ๋ก ๊ด๋ฆฌํ์ง ์๋ ๋น(Bean)์ ๋๋ค. ๋ฐ๋ผ์ ์ด ๋ฉ์๋๊ฐ ํธ์ถ๋ ๋๋ง๋ค ์๋ก์ด ์ธ์คํด์ค๊ฐ ์์ฑ๋ฉ๋๋ค.
์๋ฌธ: ์์ฑ๋ ์ธ์คํด์ค๋ Spring ์ปจํ ์คํธ ์ข ๋ฃ ์๊น์ง ์ด์๋จ์๊น?
์ ๋ง ๋ง์ ํ๋ @Bean ์ด๋ ธํ ์ด์ ์ด ๋ถ์ด ์๊ธฐ ๋๋ฌธ์, Spring ์ปจํ ์คํธ๊ฐ ์ข ๋ฃ๋ ๋ ์์ฑ๋ ์ธ์คํด์ค๋ค๋(@Bean) ํจ๊ป ์๋ฉธํ ๊ฐ๋ฅ์ฑ์ด ์์ง ์์๊น๋ผ๋ ์๋ฌธ์ด ๋ค์์ต๋๋ค. ๋ง์ฝ, ์ด ๋ฉ์๋๊ฐ ํธ์ถ๋ ๋๋ง๋ค ์์ฑ๋ ์ธ์คํด์ค๋ค์ด Spring ์ปจํ ์คํธ ์ข ๋ฃ ์์ ๊น์ง ๋ฉ๋ชจ๋ฆฌ์ ๋จ์ ์๋ค๋ฉด, ์ด๋ ๋ฉ๋ชจ๋ฆฌ ๋ฆญ ๋ฌธ์ ๋ก ์ด์ด์ง ์ ์์ต๋๋ค.
Spring ๊ณต์ ๋ฌธ์์ ๋ฐ๋ฅด๋ฉด
- @Configuration์ด ์๋ ํด๋์ค์์ ์ ์ธ๋ @Bean ๋ฉ์๋๋ Spring ์ปจํ ์ด๋์ ์ํด ๊ด๋ฆฌ๋์ง ์์ต๋๋ค.
- ์ด๋ฌํ ๋ฉ์๋๋ ์ผ๋ฐ์ ์ธ ํฉํ ๋ฆฌ ๋ฉ์๋์ฒ๋ผ ๋์ํ๋ฉฐ, ํธ์ถ ์๋ง๋ค ์๋ก์ด ์ธ์คํด์ค๋ฅผ ์์ฑํฉ๋๋ค.
- ์์ฑ๋ ์ธ์คํด์ค๋ Spring ์ปจํ ์ด๋์ ๋ฌด๊ดํ๊ฒ ๋์ํฉ๋๋ค.
์ด๋ฌํ ์ด์ ๋ก @Configuration ์๋ @Bean ๋ฉ์๋๋ ์ผ๋ฐ ๋ฉ์๋ ํธ์ถ์ฒ๋ผ ๋์ํ๋ฉฐ, Spring์ ์๋ช ์ฃผ๊ธฐ ๊ด๋ฆฌ ๋์์ด ์๋์ ๋ช ํํ ์ ์ ์์ต๋๋ค.
Spring ๊ณต์ ๋ฌธ์ ์ค ์ผ๋ถ
When @Bean methods are declared within classes that are not annotated with @Configuration - or when @Configuration(proxyBeanMethods=false) is declared -, they are referred to as being processed in a "lite" mode. In such scenarios, @Bean methods are effectively a general-purpose factory method mechanism without special runtime processing (that is, without generating a CGLIB subclass for it). A custom Java call to such a method will not get intercepted by the container and therefore behaves just like a regular method call, creating a new instance every time rather than reusing an existing singleton (or scoped) instance for the given bean.
๋ง์น๋ฉฐ ..
์ด๋ฒ ํ๋ก์ ํธ์์ ๋ฐ์ํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด OpenFeign์ ์ผ๋ถ ๊ธฐ๋ฅ, ํนํ ๋ก๊น ์ ์ง์คํด ํ์ตํ๊ณ ํ์ฉํด ๋ณด์์ต๋๋ค. ํ์ง๋ง ๊ณต์ ๋ฌธ์์ ์ผ๋ถ๋ง ๋ค๋ฃฌ ๊ฒ์ ๋ถ๊ณผํ๋ค๋, OpenFeign๊ณผ FeignClient๋ฅผ ํ์ฉํ ๋ค์ํ ์ํฉ์ ๋ ๊ฒฝํํด๋ณด๊ณ ์ถ์ด์ง๋๋ค. ์์ง ๋ถ์กฑํ ์ ์ด ๋ง์ง๋ง, ๋ถ์กฑํ ๋ถ๋ถ๊ณผ ๋ค์ํ ๊ฒฝํ์ ๋๊ธ๋ก ์๋ ค์ฃผ์๋ฉด ๊ณ์ ๋ฐฐ์ฐ๊ณ ๋ ธ๋ ฅํ๋ฉฐ ์ฑ์ฅํด ๋๊ฐ๊ฒ ์ต๋๋ค. ๋์์๋ ๊ธ์ ๋๊น์ง ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค.
์ฐธ๊ณ