Spring Cloud OpenFeign 완벽 튜토리얼: 선언적 REST 클라이언트 실전 가이드

마이크로서비스 간 HTTP 통신은 백엔드 개발에서 피할 수 없는 과제다. RestTemplate은 유지보수 모드에 들어가 더 이상 새 기능이 추가되지 않고, WebClient는 리액티브 스택 기반이라 전통적인 MVC 프로젝트에 도입하기엔 부담이 있다. 이번 포스팅에서는 Spring Cloud OpenFeign에 대해서 정리하고자 한다. 인터페이스 하나로 HTTP 클라이언트를 선언하고, 구현 코드 없이 서비스 간 통신을 처리하는 과정을 처음부터 끝까지 살펴본다.

Spring Cloud OpenFeign을 선택하는 이유

Spring Cloud OpenFeign은 Netflix가 만든 Feign 라이브러리를 Spring 생태계에 통합한 선언적 REST 클라이언트다. Java 인터페이스에 Spring MVC 어노테이션을 붙이면, 런타임에 프록시가 생성되어 실제 HTTP 호출을 대신 처리한다.

Spring Cloud OpenFeign이 눈에 띄는 이유가 있다. 보일러플레이트 코드가 거의 없다. URL 조립, 파라미터 바인딩, 응답 역직렬화를 프레임워크가 전부 처리한다. Spring Cloud LoadBalancer와 자동으로 연동되어 서비스 디스커버리 환경에서 별도 설정 없이 로드밸런싱이 동작하고, Circuit Breaker나 Micrometer 통합도 설정 몇 줄이면 끝난다.

참고로, Spring Cloud 2022.0.0부터 OpenFeign은 feature-complete 상태로 선언되었다. 새로운 기능 추가 없이 유지보수만 진행된다는 의미다. 신규 프로젝트라면 Spring 6.0에서 도입된 HTTP Interface Client도 검토해볼 만하다. 실제로 이전에 정리한 Spring Boot 3의 선언형 HTTP 클라이언트 — HTTP Interface란? 포스팅에서 이 대안을 다룬 바 있다. 다만 기존 프로젝트나 Spring Cloud 기반 마이크로서비스에서는 여전히 Spring Cloud OpenFeign이 가장 안정적인 선택지다.

프로젝트에 Spring Cloud OpenFeign 의존성 추가하기

Spring Boot 3.4 이상과 Spring Cloud 2024.0.x 조합을 기준으로 설정한다.

Maven 설정

<properties>
    <java.version>21</java.version>
    <spring-cloud.version>2024.0.1</spring-cloud.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
XML

spring-cloud-dependencies BOM을 통해 버전을 관리하므로 개별 의존성에 버전을 직접 명시할 필요가 없다. spring-cloud-starter-loadbalancer는 서비스 이름 기반 호출 시 클라이언트 사이드 로드밸런싱을 제공한다. Eureka 없이 단일 URL 호출만 한다면 생략해도 된다.

Gradle 설정

ext {
    set('springCloudVersion', '2024.0.1')
}

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
    implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}
Groovy

Gradle에서도 동일하게 BOM import 방식으로 버전을 일괄 관리한다. Spring Boot 3.4 기준으로 Spring Cloud 2024.0.x 라인이 호환된다. spring-cloud-starter-openfeign은 Feign 코어, Spring MVC 계약(Contract), Jackson 인코더/디코더를 포함하고 있어 별도 의존성 추가 없이 바로 사용할 수 있다.

@EnableFeignClients로 Feign 활성화하기

Spring Cloud OpenFeign을 사용하려면 메인 애플리케이션 클래스에 @EnableFeignClients를 선언해야 한다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients // Feign 클라이언트 컴포넌트 스캔 활성화
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}
Java

@EnableFeignClients는 클래스패스에서 @FeignClient가 붙은 인터페이스를 스캔하여 프록시 빈을 자동 등록한다. 기본적으로 메인 클래스가 위치한 패키지와 하위 패키지를 스캔한다.

멀티모듈 프로젝트에서 Feign 클라이언트가 다른 모듈에 위치한다면 basePackages 속성으로 스캔 경로를 명시해야 한다.

// 멀티모듈 환경에서 Feign 인터페이스가 별도 패키지에 있는 경우
@EnableFeignClients(basePackages = "com.example.clients")
Java

이 설정을 빠뜨리면 다른 모듈의 Feign 클라이언트가 빈으로 등록되지 않아 NoSuchBeanDefinitionException이 발생한다. 멀티모듈 구성에서 가장 흔히 마주치는 실수 중 하나다.

첫 번째 Feign 클라이언트 인터페이스 작성하기

Spring Cloud OpenFeign의 핵심은 @FeignClient 어노테이션이다. 외부 서비스의 API를 Java 인터페이스로 선언하면, 프레임워크가 런타임에 구현체를 생성한다.

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.List;

// name: 서비스 디스커버리에서 사용할 서비스 이름
// url: 직접 호출 시 고정 URL (서비스 디스커버리 미사용 시 필수)
@FeignClient(name = "product-service", url = "${product.service.url}")
public interface ProductClient {

    // GET /api/products/{id} 호출
    @GetMapping("/api/products/{id}")
    ProductResponse getProduct(@PathVariable("id") Long id);

    // GET /api/products 호출
    @GetMapping("/api/products")
    List<ProductResponse> getAllProducts();

    // POST /api/products 호출, JSON 바디 전송
    @PostMapping("/api/products")
    ProductResponse createProduct(@RequestBody CreateProductRequest request);
}
Java

위 인터페이스를 선언하는 것만으로 HTTP 클라이언트 구현이 끝난다. @GetMapping, @PostMapping 같은 Spring MVC 어노테이션을 그대로 사용할 수 있어 컨트롤러 코드와 동일한 패턴으로 작성할 수 있다. @PathVariable, @RequestParam, @RequestHeader, @RequestBody 모두 지원된다.

url 속성에 ${} 플레이스홀더를 사용하면 환경별로 호출 대상 URL을 유연하게 변경할 수 있다.

# application.yml
product:
  service:
    url: http://localhost:8081
YAML

이렇게 설정하면 로컬 개발 환경에서는 localhost를, 운영 환경에서는 내부 DNS나 서비스 URL을 프로파일별로 분리할 수 있다.

Feign 클라이언트를 서비스에서 주입받아 사용하기

참고로, 위 코드에서 사용한 ProductResponseCreateProductRequest는 아래와 같은 간단한 record로 정의한다.

// Java 21 record — 불변 DTO로 Feign 응답 역직렬화에 적합하다
public record ProductResponse(Long id, String name, Long price) {}
public record CreateProductRequest(String name, Long price, String category) {}
public record OrderResponse(Long productId, String productName, int quantity, long totalPrice) {}
Java

record는 getter, equals, hashCode, toString을 자동 생성하므로 DTO 용도로 최적이다. Jackson이 record 타입을 기본 지원하므로 별도의 @JsonProperty 없이도 JSON 역직렬화가 동작한다.

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class OrderService {

    // Spring Cloud OpenFeign이 생성한 프록시 빈이 자동 주입된다
    private final ProductClient productClient;

    public OrderResponse createOrder(Long productId, int quantity) {
        // 인터페이스 메서드 호출 = HTTP GET 요청 실행
        ProductResponse product = productClient.getProduct(productId);

        // 비즈니스 로직 처리
        long totalPrice = product.getPrice() * quantity;

        return new OrderResponse(productId, product.getName(), quantity, totalPrice);
    }
}
Java

ProductClient를 일반적인 Spring 빈처럼 생성자 주입으로 사용한다. 메서드를 호출하면 내부적으로 HTTP 요청이 실행되고, 응답 JSON이 ProductResponse 객체로 역직렬화되어 반환된다. 호출하는 쪽에서는 HTTP 통신이 일어나는지조차 의식할 필요가 없다.

Spring Cloud OpenFeign 타임아웃 설정으로 장애 전파 차단하기

Spring Cloud OpenFeign을 운영 환경에 적용할 때 가장 먼저 설정해야 하는 항목이 타임아웃이다. 기본값은 연결 타임아웃 10초, 읽기 타임아웃 60초인데, 이 값은 대부분의 서비스에서 지나치게 길다.

spring:
  cloud:
    openfeign:
      client:
        config:
          # 모든 Feign 클라이언트에 적용되는 기본 설정
          default:
            connectTimeout: 3000  # 연결 타임아웃 3초
            readTimeout: 5000    # 읽기 타임아웃 5초
          # 특정 클라이언트에만 별도 타임아웃 적용
          product-service:
            connectTimeout: 2000  # 연결 타임아웃 2초
            readTimeout: 3000    # 읽기 타임아웃 3초
YAML

default 키로 전체 클라이언트에 공통 타임아웃을 지정하고, @FeignClientname 값을 키로 사용하면 특정 클라이언트에 개별 타임아웃을 덮어쓸 수 있다. connectTimeout은 TCP 연결 수립까지의 대기 시간이고, readTimeout은 연결 후 응답 데이터를 받을 때까지의 대기 시간이다.

외부 결제 API처럼 응답이 느린 서비스는 readTimeout을 10초 이상으로 잡아야 할 수 있고, 내부 마이크로서비스 간 호출은 3초 이내로 짧게 가져가는 것이 장애 전파를 막는 데 유리하다.

로깅 레벨 설정으로 디버깅 효율 높이기

Spring Cloud OpenFeign은 네 단계의 로깅 레벨을 지원한다. 개발 환경에서 요청/응답 내용을 확인하거나, 운영 환경에서 장애 원인을 추적할 때 요긴하다.

레벨출력 내용
NONE로그 없음 (기본값)
BASIC요청 메서드, URL, 응답 상태 코드, 실행 시간
HEADERSBASIC + 요청/응답 헤더
FULLHEADERS + 요청/응답 바디, 메타데이터 전체
# application.yml
spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            loggerLevel: BASIC  # 전체 클라이언트 기본 로깅
          product-service:
            loggerLevel: FULL   # product-service만 상세 로깅

# Feign 로거는 DEBUG 레벨에서만 동작하므로 패키지 레벨도 설정해야 한다
logging:
  level:
    com.example.client: DEBUG
YAML

Feign 로거는 SLF4J의 DEBUG 레벨에서만 출력된다. loggerLevelFULL로 설정해도 해당 패키지의 로깅 레벨이 DEBUG가 아니면 아무 로그도 나오지 않는다. 운영 환경에서는 BASIC으로 설정하여 응답 시간과 상태 코드만 추적하고, 장애 발생 시 일시적으로 FULL로 올려 바디 내용까지 확인하는 방식이 실무에서 흔히 쓰인다.

RequestInterceptor로 공통 헤더 일괄 처리하기

마이크로서비스 간 호출에서 인증 토큰, 트레이스 ID, 커스텀 헤더를 매 요청마다 수동으로 붙이는 것은 실수가 잦고 중복 코드가 쌓인다. Spring Cloud OpenFeign의 RequestInterceptor를 사용하면 모든 요청에 공통 헤더를 자동으로 주입할 수 있다.

import feign.RequestInterceptor;
import feign.RequestTemplate;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Configuration
public class FeignInterceptorConfig {

    @Bean
    public RequestInterceptor authForwardingInterceptor() {
        return (RequestTemplate template) -> {
            // 현재 요청의 Authorization 헤더를 가져와 Feign 요청에 전파
            ServletRequestAttributes attributes =
                    (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes != null) {
                HttpServletRequest request = attributes.getRequest();
                String authHeader = request.getHeader("Authorization");
                if (authHeader != null) {
                    template.header("Authorization", authHeader);
                }
            }
            // 공통 커스텀 헤더 추가
            template.header("X-Client-Name", "order-service");
        };
    }
}
Java

이 인터셉터가 하는 일은 현재 HTTP 요청의 Authorization 헤더를 Feign 요청에 그대로 넘기는 것과, 호출 측 서비스 이름을 X-Client-Name 헤더로 붙여 수신 측에서 호출자를 식별할 수 있게 하는 것이다.

다만 RequestContextHolder는 같은 스레드에서만 동작한다. @Async나 별도 스레드 풀에서 Feign 호출을 실행하면 getRequestAttributes()null을 반환한다. Spring Boot 3.2+에서 가상 스레드(spring.threads.virtual.enabled=true)를 활성화한 경우에도 동일한 문제가 발생한다. 가상 스레드 환경에서는 RequestContextHolder 전파가 보장되지 않으므로, 인터셉터 대신 OAuth2 자동 전파 설정이나 명시적 헤더 전달 방식을 사용하는 것이 안전하다.

특정 클라이언트에만 인터셉터 적용하기

모든 클라이언트가 아닌 특정 클라이언트에만 인터셉터를 적용하려면 별도 Configuration 클래스를 만들어 @FeignClientconfiguration 속성에 지정한다.

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;

// @Configuration을 붙이지 않는다 — 전역 적용 방지
public class PaymentClientConfig {

    @Bean
    public RequestInterceptor paymentApiKeyInterceptor(
            @Value("${payment.service.api-key}") String apiKey) {
        return (RequestTemplate template) -> {
            // 결제 서비스 전용 API 키를 헤더에 추가 (값은 환경변수 또는 Vault에서 주입)
            template.header("X-API-Key", apiKey);
        };
    }
}
Java
// configuration 속성으로 PaymentClientConfig를 지정
@FeignClient(
    name = "payment-service",
    url = "${payment.service.url}",
    configuration = PaymentClientConfig.class
)
public interface PaymentClient {

    @PostMapping("/api/payments")
    PaymentResponse processPayment(@RequestBody PaymentRequest request);
}
Java

여기서 핵심은 PaymentClientConfig@Configuration을 붙이지 않는 것이다. @Configuration을 붙이면 해당 빈이 전역 컨텍스트에 등록되어 모든 Feign 클라이언트에 적용된다. 특정 클라이언트 전용 설정은 반드시 @Configuration 없이 POJO 클래스로 작성해야 한다.

ErrorDecoder로 예외 처리 세밀하게 제어하기

Spring Cloud OpenFeign은 HTTP 응답 상태 코드가 200번대가 아닐 때 기본적으로 FeignException을 던진다. 그런데 이 예외는 상태 코드와 응답 바디를 원시 형태로 담고 있어서, 비즈니스 로직에서 활용하기 불편하다. ErrorDecoder를 커스터마이징하면 상태 코드별로 의미 있는 예외를 던질 수 있다.

import feign.Response;
import feign.codec.ErrorDecoder;
import org.springframework.http.HttpStatus;

public class ProductClientErrorDecoder implements ErrorDecoder {

    private final ErrorDecoder defaultDecoder = new Default();

    @Override
    public Exception decode(String methodKey, Response response) {
        HttpStatus status = HttpStatus.resolve(response.status());

        if (status == null) {
            return defaultDecoder.decode(methodKey, response);
        }

        // 상태 코드별로 비즈니스 예외로 변환
        // 아래 예외 클래스들은 RuntimeException을 상속하는 커스텀 예외다
        return switch (status) {
            case NOT_FOUND -> new ProductNotFoundException(
                    "상품을 찾을 수 없습니다. method=" + methodKey);
            case BAD_REQUEST -> new InvalidProductRequestException(
                    "잘못된 상품 요청입니다. method=" + methodKey);
            case SERVICE_UNAVAILABLE -> new ProductServiceUnavailableException(
                    "상품 서비스를 일시적으로 사용할 수 없습니다.");
            default -> defaultDecoder.decode(methodKey, response);
        };
    }
}
Java

ErrorDecoder에서 반환하는 예외가 RetryableException이면 Feign의 Retryer가 재시도를 수행하고, 그 외 예외는 즉시 호출자에게 전파된다. 404 응답을 비즈니스 예외(ProductNotFoundException)로 변환하면 서비스 레이어에서 try-catch 없이 @ExceptionHandler로 일관되게 처리할 수 있다.

ErrorDecoder를 등록하는 방법은 두 가지다.

# application.yml 방식
spring:
  cloud:
    openfeign:
      client:
        config:
          product-service:
            errorDecoder: com.example.client.ProductClientErrorDecoder
YAML
// Java Configuration 방식
public class ProductClientConfig {

    @Bean
    public ErrorDecoder productErrorDecoder() {
        return new ProductClientErrorDecoder();
    }
}
Java

YAML 방식은 클래스의 FQCN(Fully Qualified Class Name)을 문자열로 지정하고, Java 방식은 빈으로 등록한다. 두 방식 모두 동일하게 동작하지만, IDE의 리팩토링 지원을 받으려면 Java 방식이 유리하다.

Spring Cloud OpenFeign Circuit Breaker와 Fallback으로 장애 격리하기

외부 서비스가 죽으면 우리 서비스도 같이 죽는다. 이 장애 전파를 끊어내는 것이 Circuit Breaker 패턴이다. Spring Cloud OpenFeign은 Spring Cloud Circuit Breaker와 연동하여 Fallback 메커니즘을 기본 제공한다.

의존성 추가

<!-- Resilience4j Circuit Breaker -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
XML

Circuit Breaker 활성화 및 Resilience4j 설정

spring:
  cloud:
    openfeign:
      circuitbreaker:
        enabled: true            # Feign에 Circuit Breaker 연동 활성화
        alphanumeric-ids:
          enabled: true          # Circuit Breaker ID에 특수문자 대신 영숫자 사용

# Resilience4j 세부 설정
resilience4j:
  circuitbreaker:
    configs:
      default:
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 10          # 최근 10개 요청 기준으로 판단
        failureRateThreshold: 50       # 실패율 50% 이상이면 회로 열림
        waitDurationInOpenState: 10s   # 열린 상태에서 10초 대기 후 반열림으로 전환
        permittedNumberOfCallsInHalfOpenState: 3  # 반열림 상태에서 3개 요청 시도
YAML

slidingWindowSize: 10failureRateThreshold: 50은 최근 10번의 호출 중 5번 이상 실패하면 회로가 열리도록 설정한 것이다. 회로가 열리면 10초 동안 실제 HTTP 호출 없이 즉시 fallback이 실행되어 외부 서비스 장애가 전파되는 것을 차단한다. Resilience4j에 대한 자세한 내용은 Resilience4j 공식 문서를 참고하면 된다.

FallbackFactory를 활용한 Fallback 구현

import java.util.List;

import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

@Component
public class ProductClientFallbackFactory implements FallbackFactory<ProductClient> {

    @Override
    public ProductClient create(Throwable cause) {
        return new ProductClient() {
            @Override
            public ProductResponse getProduct(Long id) {
                // 장애 시 기본값 반환 또는 캐시된 데이터 제공
                return new ProductResponse(id, "상품 정보 일시 조회 불가", 0L);
            }

            @Override
            public List<ProductResponse> getAllProducts() {
                return List.of();
            }

            @Override
            public ProductResponse createProduct(CreateProductRequest request) {
                // 쓰기 작업은 fallback에서 실패를 명시적으로 알리는 것이 안전하다
                throw new ProductServiceUnavailableException(
                        "상품 서비스 장애로 생성 요청을 처리할 수 없습니다. 원인: " + cause.getMessage());
            }
        };
    }
}
Java

FallbackFactory는 단순 fallback 클래스와 달리 장애의 원인(Throwable cause)에 접근할 수 있다. 이를 통해 타임아웃인지, 서비스 다운인지, 비즈니스 에러인지에 따라 다른 fallback 전략을 적용할 수 있다. 조회 API는 기본값이나 캐시 데이터를 반환하고, 쓰기 API는 예외를 던져 호출자에게 실패를 명확히 전달하는 것이 일반적인 패턴이다.

Feign 클라이언트에 FallbackFactory 연결

@FeignClient(
    name = "product-service",
    url = "${product.service.url}",
    fallbackFactory = ProductClientFallbackFactory.class
)
public interface ProductClient {
    // 기존 메서드 동일
    @GetMapping("/api/products/{id}")
    ProductResponse getProduct(@PathVariable("id") Long id);

    @GetMapping("/api/products")
    List<ProductResponse> getAllProducts();

    @PostMapping("/api/products")
    ProductResponse createProduct(@RequestBody CreateProductRequest request);
}
Java

fallbackFactory 속성에 팩토리 클래스를 지정하면 Circuit Breaker가 열렸을 때 자동으로 fallback 메서드가 호출된다. 별도로 try-catch를 작성할 필요가 없다.

요청/응답 압축으로 네트워크 비용 줄이기

마이크로서비스 간에 대용량 JSON 데이터를 주고받는 경우, 요청과 응답을 GZIP으로 압축하면 네트워크 전송량을 상당히 줄일 수 있다. Spring Cloud OpenFeign은 설정만으로 압축을 활성화할 수 있다.

spring:
  cloud:
    openfeign:
      compression:
        request:
          enabled: true
          mime-types: text/xml,application/xml,application/json  # 압축 대상 MIME 타입
          min-request-size: 2048  # 2KB 이상인 요청만 압축 (바이트 단위)
        response:
          enabled: true
YAML

min-request-size를 2048로 설정하면 2KB 미만의 작은 요청은 압축 오버헤드를 피하기 위해 그대로 전송된다. 응답 압축은 서버 측에서 Content-Encoding: gzip 헤더를 보내면 자동으로 디코딩된다. 대량의 목록 조회 API에서 특히 효과적이며, 응답 크기에 따라 전송 시간이 30~50% 감소하는 경우도 있다.

Spring Cloud OpenFeign vs RestClient vs WebClient, 무엇이 다른가

Spring Boot 3.2에서 RestClient가 도입되면서 HTTP 클라이언트 선택지가 세 가지로 늘어났다. 각각의 특성을 비교하면 다음과 같다.

비교 항목Spring Cloud OpenFeignRestClientWebClient
프로그래밍 모델선언적 (인터페이스)명령적 (Fluent API)명령적 (Fluent API)
블로킹/논블로킹블로킹블로킹논블로킹 (리액티브)
서비스 디스커버리LoadBalancer 자동 연동수동 설정 필요수동 설정 필요
Circuit Breaker설정만으로 연동직접 구현직접 구현
적합한 환경Spring Cloud 마이크로서비스단순 외부 API 호출고동시성 리액티브 앱
Spring 지원 상태유지보수 (feature-complete)활발한 개발 중활발한 개발 중

Spring Cloud 기반 마이크로서비스 아키텍처에서 서비스 간 동기 호출을 처리한다면 Spring Cloud OpenFeign이 보일러플레이트를 가장 적게 만든다. 단일 외부 API 호출이나 Spring Cloud를 사용하지 않는 프로젝트라면 RestClient가 간결하다. 초당 수백~수천 건의 동시 요청을 처리해야 하는 I/O 바운드 환경이라면 WebClient의 논블로킹 모델이 스레드 고갈 없이 높은 처리량을 제공한다.

실무에서 놓치기 쉬운 Spring Cloud OpenFeign 설정 체크리스트

Spring Cloud OpenFeign을 운영 환경에 배포하기 전에 확인해야 할 설정 항목을 정리한다. 마이크로서비스 보안 설정이 궁금하다면 Spring Security JWT 인증 구현 가이드도 함께 참고하자.

OAuth2 토큰 자동 전파

spring:
  cloud:
    openfeign:
      oauth2:
        enabled: true
        client-registration-id: my-service-client
YAML

OAuth2 클라이언트가 설정되어 있다면 위 두 줄만으로 Feign 요청에 Bearer 토큰이 자동으로 추가된다. 별도의 RequestInterceptor를 만들 필요가 없다.

런타임 설정 갱신 (@RefreshScope)

spring:
  cloud:
    openfeign:
      client:
        refresh-enabled: true
YAML

Spring Cloud Config와 함께 사용하면 @RefreshScope를 통해 Feign 클라이언트의 타임아웃 등 설정 속성을 애플리케이션 재시작 없이 갱신할 수 있다. 운영 중 긴급하게 타임아웃을 조정해야 할 때 유용하다.

dismiss404 옵션

spring:
  cloud:
    openfeign:
      client:
        config:
          product-service:
            dismiss404: true  # 404 응답을 예외가 아닌 빈 응답으로 처리
YAML

dismiss404true로 설정하면 404 응답 시 예외를 던지지 않고 null 또는 빈 Optional을 반환한다. “존재하지 않음”이 정상적인 비즈니스 케이스인 API에 유용하다.

Spring Cloud OpenFeign의 전체 설정 옵션은 공식 레퍼런스 문서에서 확인할 수 있고, OpenFeign의 다양한 활용법은 Baeldung의 Spring Cloud OpenFeign 가이드에서도 잘 정리되어 있다.

지금까지 Spring Cloud OpenFeign에 대해서 정리해 보았다. 실무에서 RestTemplate으로 외부 서비스를 호출하던 코드를 OpenFeign으로 전환했을 때 가장 체감되는 변화는 보일러플레이트의 감소보다 ErrorDecoder와 Circuit Breaker 설정이 자연스럽게 강제된다는 점이었다. RestTemplate 시절에는 try-catch로 감싸고 넘기기 일쑤였던 예외 처리가, OpenFeign에서는 구조적으로 분리되면서 장애 대응 코드의 품질이 올라간 경험이 있다. feature-complete 상태가 아쉬울 수 있지만, API가 안 바뀌니까 메이저 업그레이드 때 신경 쓸 게 줄어들어 오히려 편한 측면도 있다.