본문 바로가기
spring

[SpringBoot] 선언형 HTTP 클라이언트 OpenFeign이란?

by moonsiri 2025. 7. 2.
728x90
반응형

마이크로서비스 아키텍처에서 서비스 간 통신은 필수입니다.
하지만 RestTemplate 또는 WebClient로 매번 HTTP 요청을 구성하고 응답을 처리하는 것은 번거롭고 중복이 많습니다.
 

1. OpenFeign 개요

OpenFeign은 인터페이스 기반으로 외부 HTTP API를 간단하고 타입 안전하게 호출할 수 있게 도와주는 선언형(Declarative) HTTP 클라이언트입니다.

연도주요 이벤트
2013Netflix가 내부 프로젝트로 Feign 개발 시작
2015Netflix OSS의 일부로 Feign 오픈 소스 공개
2016Spring Cloud Netflix 프로젝트에서 @FeignClient 으로 통합
2019OpenFeign이라는 이름으로 독립 프로젝트로 분리됨
Netfliex는 Feigb의 개발에서 손을 떼고 커뮤니티로 이전
2020 이후Spring Cloud OpenFeign이 주요한 구현체로 유지되며, Spring Boot와 통합 지원 지속

 
주요 특징

  • @FeignClient 인터페이스만으로 외부 API 호출 가능
  • JSON, Form 등 다양한 HTTP 요청 포맷 지원
  • 공통 헤더, 인증 토큰 등을 위한 RequestInterceptor 지원
  • Retry, Fallback, ErrorDecoder 등 고급 기능 내장
  • Spring Cloud와 완벽 통합 (spring-cloud-starter-openfeign)

 
 

2. 기본 구성

2.1. 의존성 추가

    <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>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-hc5</artifactId>
        </dependency>
    </dependencies>

 

2.2. @EnableFeignClients 설정

@Configuration
@EnableFeignClients(defaultConfiguration = FeignConfig.class)
public class FeignConfig {  }

 

2.3. Feign 인터페이스 정의

@FeignClient(name = "userApi", url = "https://api.example.com")
public interface UserApiClient {

    @GetMapping("/users/{id}")
    UserResponse getUser(@PathVariable("id") Long id);

    @PostMapping(value = "/users", consumes = "application/json")
    UserResponse createUser(@RequestBody CreateUserRequest request);
}

@FeignClient는 실제 구현체 없이도 자동으로 프록시가 생성됩니다.
 

3. 실전 사용 예 : 토큰 인증 붙은 API 호출

3.1. 인터셉터로 헤더 자동 삽입

@Component
public class AuthHeaderInterceptor implements RequestInterceptor {

    @Autowired
    private TokenProvider tokenProvider;

    @Override
    public void apply(RequestTemplate template) {
        // template.removeHeader("Authorization");
        template.header("Authorization", "Bearer " + tokenProvider.getToken());
    }
}

 

3.2. 토큰 만료 대응 : ErrorDecoder 사용

@Component
public class CustomFeignErrorDecoder implements ErrorDecoder {

    private final ErrorDecoder defaultDecoder = new Default();

    @Override
    public Exception decode(String methodKey, Response response) {
        String body = getBody(response);

        if (body.contains("EXPIRE_TOKEN")) {
            return new RetryableException(
                response.status(),
                "Token invalid or expired. Retrying with refreshed token...",
                response.request().httpMethod(),   // ← method
                (Long) null,                       // ← retryAfter millis (null 이면 기본 정책 사용)
                response.request()                 // ← 원래 요청 정보
            );
        }

        return defaultDecoder.decode(methodKey, response);
    }

    private String getBody(Response response) {
        try (InputStream is = response.body().asInputStream()) {
            return IOUtils.toString(is, StandardCharsets.UTF_8);
        } catch (IOException e) {
            return "";
        }
    }
}

 

4. 실전 사용 예 : 동적 통신

4.1. 동적 클라이언트 구현

public interface DynamicFeignClient {

	@GetMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
	Object get(@SpringQueryMap Object param);

	@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
	Object post(@RequestBody Object body);

	@PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
	Object post(MultiValueMap<String, ?> param);
}
@Component
@RequiredArgsConstructor
public class DynamicFeignClientFactory {

	private final Client feignClient;
	private final ObjectFactory<HttpMessageConverters> messageConverters;
	private final TokenProvider tokenProvider;

	public DynamicFeignClient create(ApiDefInfo defInfo) {
		return Feign.builder()
			.client(feignClient)
			.contract(new SpringMvcContract())
			.encoder(new SpringFormEncodr(new SpringEncoder(messageConverters)))
			.decoder(new SpringDecoder(messageConverters))
			.requestInterceptor(new DynamicFeignRequestInterceptor(tokenProvider))
			.errorDecoder(new DynamicFeignErrorDecoder(tokenProvider))
			.options(new Request.Options(defInfo.getConnectionTimeoutMs(), defInfo.getSocketTimeoutMs()))
		.target(DynamicFeignClient.class, defInfo.getFullUrl());
	}

}

 

4.2. 호출

@Service
@RequiredArgsConstructor
public class ClientService {

	private final ApiUrlResolver apiUrlResolver;
	private final DynamicFeignClientFactory dynamicFeignClientFactory;
	private final ObjectMapper objectMapper;

	public <T extends ApiFeignRes> T get(String defId, Object sendParam, Class<T> clz, Object... args) {
		return this.executeAndConvert(HttpMethod.GET, defId, sendParam, clz, args);
	}

	public <T extends ApiFeignRes> T post(String defId, Object sendParam, Class<T> clz, Object... args) {
		return this.executeAndConvert(HttpMethod.POST, defId, sendParam, clz, args);
	}

	public <T> List<T> getAsList(String defId, Object sendParam, Class<T> clz, Object... args) {
		return this.executeAndConvertList(HttpMethod.GET, defId, sendParam, clz, args);
	}

	public <T> List<T> postAsList(String defId, Object sendParam, Class<T> clz, Object... args) {
		return this.executeAndConvertList(HttpMethod.POST, defId, sendParam, clz, args);
	}

	private <T> T executeAndConvert(HttpMethod method, String defId, Object param, Class<T> clz, Object... args) {
		JavaType returnType = TypeFactory.defaultInstance().constructType(clz);
		Object result = execute(method, defId, param, args);
		return objectMapper.convertValue(result, returnType);
	}

	private <T> List<T> executeAndConvertList(HttpMethod method, String defId, Object param, Class<T> clz, Object... args) {
		CollectionType returnType = TypeFactory.defaultInstance().constructCollectionType(List.class, clz);
		Object result = execute(method, defId, param, args);
		return objectMapper.convertValue(result, returnType);
	}

	private Object execute(HttpMethod method, String defId, Object param, Object... args) {
		ApiDefInfo defInfo = apiUrlResolver.getDefInfo(defId);
		if (defInfo == null) {
			throw new RuntimeException("잘못된 defId : " + defId);
		}

		if (args != null && args.length > 0) {
			defInfo.setFullUrl(String.format(defInfo.getFullUrl(), args));
		}

		DynamicFeignClient client = dynamicFeignClientFactory.create(defInfo);
		if (method == HttpMethod.GET) {
			return client.get(param);
		} else if (method == HttpMethod.POST) {
			return client.post(param);
		} else {
			throw new UnsupportedOperationException("지원하지 않는 HTTP 메서드: " + method);
		}
	}

	/**
	 * application/x-www-form-urlencoded
	 */
	public <T extends ApiFeignRes> T post(String defId, MultiValueMap<String, Object> paramMap, Class<T> clz, Object... args) {
		ApiDefInfo defInfo = apiUrlResolver.getDefInfo(defId);
		if (defInfo == null) {
			throw new RuntimeException("잘못된 defId : " + defId);
		}

		if (args != null && args.length > 0) {
			defInfo.setFullUrl(String.format(defInfo.getFullUrl(), args));
		}

		DynamicFeignClient client = dynamicFeignClientFactory.create(svcCd.name(), defInfo);
		Object result = client.post(paramMap);
		return objectMapper.convertValue(result, clz);
	}
}

 
 

5. 고급 기능 요약

기능설명
RequestInterceptor요청 전 헤더/쿼리 등 전처리
ErrorDecoder응답 오류 커스터마이징
Retryer재시도 정책 설정
FallbackFactory실패 시 fallback 지정
Decoder응답 JSON → DTO 매핑 (기본은 Jackson 사용)

 

6. 테스트 팁

  • FeignClient는 일반 인터페이스이므로, 단위 테스트에서는 @MockBean 또는 Stub으로 쉽게 대체 가능
  • 통합 테스트에서는 WireMock 또는 MockWebServer 사용 추천

 
 

7. 언제 Feign을 써야 하나?

Feign은 API 호출이 많고, 인증/헤더/재시도 로직이 공통화된 프로젝트에 강력한 생산성과 유지보수성을 제공합니다.
하지만, 단순한 단건 호출이나, 복잡한 커넥션 풀 관리가 필요한 경우엔 WebClient도 고려해볼 수 있습니다.
 
 
[Reference]
https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/
https://github.com/OpenFeign/feign
 

728x90
반응형

댓글