Spring Cloud 환경에서 OpenFeign을 활용한 선언형 HTTP 클라이언트 구현

OpenFeign은 자바 기반 마이크로서비스 아키텍처에서 RESTful 원격 호출을 간결하고 유지보수 용이하게 만드는 선언형 HTTP 클라이언트 라이브러리입니다. 개발자는 인터페이스 정의와 애너테이션만으로 외부 서비스와의 통신 로직을 구현할 수 있으며, 별도의 HTTP 연결 관리나 직렬화/역직렬화 코드를 작성하지 않아도 됩니다.

핵심 특징 및 이점

  • 선언적 인터페이스 기반: 서비스 계약을 Java 인터페이스로 표현하고, @GetMapping, @PostMapping 등 Spring MVC 애너테이션을 그대로 사용 가능
  • 자동 부하 분산 통합: Eureka 또는 Nacos와 연동 시, 내장된 LoadBalancerFeignClient가 서비스 인스턴스 목록을 동적으로 조회해 라운드로빈 방식으로 요청 전달
  • 확장 가능한 코덱 지원: JSON, XML, Protobuf 등 다양한 포맷에 대한 커스텀 Encoder/Decoder 구현이 용이하며, 기본적으로 Jackson을 활용한 객체 매핑 제공
  • HTTP 클라이언트 교체 유연성: 기본 HttpURLConnection 대신 OkHttp, Apache HttpClient 등 고성능 클라이언트로 교체 가능

기본 사용 예시 (순수 Feign)

다음 코드는 Spring Boot 외부에서도 동작하는 순수 Feign 클라이언트 구현입니다. 커스텀 디코더와 함께 SpringMvcContract를 적용해 Spring 스타일의 애너테이션을 사용합니다.

import feign.*;
import feign.codec.Decoder;
import feign.codec.ErrorDecoder;
import feign.jackson.JacksonDecoder;
import feign.slf4j.Slf4jLogger;
import lombok.Data;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.core.ResolvableType;

import java.io.IOException;
import java.lang.reflect.Type;

public class ExternalQuoteClientExample {

    public static void main(String[] args) {
        final QuoteService client = Feign.builder()
                .logger(new Slf4jLogger())
                .logLevel(Logger.Level.BASIC)
                .contract(new SpringMvcContract())
                .decoder(new CustomJsonDecoder())
                .errorDecoder(new RetryableErrorDecoder())
                .target(QuoteService.class, "https://api.example.com/v1");

        final QuoteResponse quote = client.fetchLatestQuote("AAPL");
        System.out.println("Current price: " + quote.getPrice());
    }

    interface QuoteService {
        @RequestLine("GET /stocks/{symbol}/quote")
        QuoteResponse fetchLatestQuote(@Param("symbol") String symbol);
    }

    static class CustomJsonDecoder implements Decoder {
        private final ObjectMapper mapper = new ObjectMapper();

        @Override
        public Object decode(Response response, Type type) throws IOException {
            try (InputStream is = response.body().asInputStream()) {
                Class<?> rawType = ResolvableType.forType(type).getRawClass();
                return mapper.readValue(is, rawType);
            }
        }
    }

    static class RetryableErrorDecoder implements ErrorDecoder {
        private final ErrorDecoder defaultDecoder = new Default();

        @Override
        public Exception decode(String methodKey, Response response) {
            if (response.status() == 503) {
                return new RetryableException("Service unavailable", response.request(), response);
            }
            return defaultDecoder.decode(methodKey, response);
        }
    }

    @Data
    public static class QuoteResponse {
        private String symbol;
        private BigDecimal price;
        private LocalDateTime updatedAt;
        private String currency;
    }
}

Spring Boot 환경 통합

Spring Boot 프로젝트에서는 @EnableFeignClients를 통해 자동 구성 및 스캔을 활성화합니다. 아래는 OkHttp 기반 클라이언트로 교체하고, 커스텀 타임아웃 및 커넥션 풀을 설정하는 예제입니다.

@Configuration
public class FeignClientConfig {

    @Bean
    public Client feignClient() {
        final OkHttpClient okHttpClient = new okhttp3.OkHttpClient.Builder()
                .connectTimeout(15, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .writeTimeout(30, TimeUnit.SECONDS)
                .connectionPool(new ConnectionPool(20, 5, TimeUnit.MINUTES))
                .build();
        return new OkHttpClient(okHttpClient);
    }

    @Bean
    public Encoder feignEncoder() {
        return new JacksonEncoder();
    }

    @Bean
    public Decoder feignDecoder() {
        return new JacksonDecoder();
    }
}

클라이언트 인터페이스는 다음과 같이 정의됩니다:

@FeignClient(
    name = "stock-api",
    url = "${external.stock.api.base-url}",
    configuration = FeignClientConfig.class
)
public interface StockApiClient {

    @GetMapping("/stocks/{ticker}/summary")
    StockSummary getStockSummary(@PathVariable String ticker);

    @PostMapping("/alerts/subscribe")
    @Headers("Content-Type: application/json")
    SubscriptionResult subscribeAlert(@RequestBody AlertSubscription request);
}

로드 밸런싱 작동 원리

서비스 이름 기반 호출(@FeignClient(name = "user-service")) 시, OpenFeign은 다음 순서로 로드 밸런싱을 수행합니다:

  1. FeignClientFactoryBean이 LoadBalancerFeignClient 인스턴스를 생성
  2. LoadBalancerFeignClient.execute()가 CachingSpringLoadBalancerFactory를 통해 ILoadBalancer 획득
  3. ZoneAwareLoadBalancer는 Eureka 서버로부터 주기적으로 업데이트된 인스턴스 목록을 캐시함
  4. 각 요청 시 chooseServer(null) 메서드가 실행되어 라운드로빈 또는 가중치 기반 알고리즘으로 특정 인스턴스 선택

성능 최적화 팁

  • 공유된 OkHttpClient 인스턴스 재사용 — 각 클라이언트마다 새 인스턴스 생성 금지
  • 디코딩 성능 향상을 위해 JacksonDecoder 대신 StringDecoder + 수동 파싱 조합 고려
  • 고빈도 호출 시 @CachableView 또는 외부 캐시 계층 도입
  • 모니터링을 위해 feign-micrometer 또는 feign-slf4j 로깅 활성화

태그: OpenFeign spring-cloud load-balancing OkHttp rest-client

10월 10일 18:35에 게시됨