๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

ํ”„๋กœ์ ํŠธ/๊ณ ๋ฏผ ์ƒ๋‹ด ํ”Œ๋žซํผ

OpenFeign์— INFO ๋ ˆ๋ฒจ ๋กœ๊ทธ ์ถ”๊ฐ€ํ•˜๊ธฐ (2/2): ๋กœ๊ทธ ์„ค์ •๊ณผ Configuration ๊ณ ๋ฏผ

์ €๋ฒˆ ๊ธ€์—์„œ๋Š” WireMock์„ ํ™œ์šฉํ•ด Clova AI(์™ธ๋ถ€) ์„œ๋ฒ„๋ฅผ ๋ชจํ‚นํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋‹ค๋ค˜์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด Clova ์„œ๋ฒ„๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š์Œ์œผ๋กœ์จ ๋น„์šฉ ๋ฐœ์ƒ ์—†์ด, @FeignClient ์— logger-level: FULL ์„ค์ •์ด ์ž˜ ์ ์šฉ ๋˜์—ˆ๋Š”์ง€๋ฅผ ํ™•์ธํ•˜๋Š” ๊ณผ์ •์„ ์‚ดํŽด๋ดค์Šต๋‹ˆ๋‹ค.

 

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” INFO ๋ ˆ๋ฒจ์—์„œ Feign ๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•˜๋„๋ก ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์†Œ๊ฐœํ•˜๋ ค ํ–ˆ์ง€๋งŒ, ์ด๋ฏธ ์ด๋ฅผ ๋‹ค๋ฃฌ ์ข‹์€ ๊ธ€๋“ค์ด ๋งŽ์•„ ํ•ด๋‹น ์ฃผ์ œ๋ฅผ ๊ฐ„๋‹จํžˆ ๋„˜์–ด๊ฐ€๊ณ , OpenFeign์˜ @Configuration ์„ค๊ณ„ ๊ณผ์ •์—์„œ ๊ณ ๋ฏผํ–ˆ๋˜ ์ ์„ ๊ณต์œ ํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.

 

Feign ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๊ด€๋ จ ์ข‹์€ ๊ธ€

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๋ฅผ ํ™œ์šฉํ•œ ๋‹ค์–‘ํ•œ ์ƒํ™ฉ์„ ๋” ๊ฒฝํ—˜ํ•ด๋ณด๊ณ  ์‹ถ์–ด์ง‘๋‹ˆ๋‹ค. ์•„์ง ๋ถ€์กฑํ•œ ์ ์ด ๋งŽ์ง€๋งŒ, ๋ถ€์กฑํ•œ ๋ถ€๋ถ„๊ณผ ๋‹ค์–‘ํ•œ ๊ฒฝํ—˜์„ ๋Œ“๊ธ€๋กœ ์•Œ๋ ค์ฃผ์‹œ๋ฉด ๊ณ„์† ๋ฐฐ์šฐ๊ณ  ๋…ธ๋ ฅํ•˜๋ฉฐ ์„ฑ์žฅํ•ด ๋‚˜๊ฐ€๊ฒ ์Šต๋‹ˆ๋‹ค. ๋‘์„œ์—†๋Š” ๊ธ€์„ ๋๊นŒ์ง€ ์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

 

์ฐธ๊ณ  

- Spring Cloud OpenFeign Docs