마이크로서비스 아키텍처에서 서비스 간 통신은 핵심 과제입니다. OpenFeign은 Spring Cloud 기반 환경에서 선언적 방식으로 HTTP 클라이언트를 구성할 수 있게 해주는 강력한 도구입니다.
환경 구성 및 활성화
먼저 의존성을 추가하고 애플리케이션에서 Feign 기능을 활성화합니다.
<!-- build.gradle 예시 -->
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
// 또는 Maven
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
@SpringBootApplication
@EnableFeignClients(basePackages = "com.example.client")
public class PaymentApplication {
public static void main(String[] args) {
SpringApplication.run(PaymentApplication.class, args);
}
}
클라이언트 인터페이스 정의
원격 서비스 호출을 메서드 선언으로 추상화합니다. @FeignClient의 value는 서비스 디스커버리에 등록된 이름입니다.
@FeignClient(
value = "inventory-service",
url = "${inventory.service.url:}", // 선택적 직접 URL 지정
path = "/v1/stock"
)
public interface InventoryRemoteClient {
@GetMapping("/items/{sku}")
StockResponse checkAvailability(@PathVariable String sku);
@PutMapping("/items/{sku}/deduct")
DeductResult reserveStock(
@PathVariable String sku,
@RequestParam("quantity") Integer qty,
@RequestHeader("X-Transaction-Id") String txId
);
@PostMapping("/batch/query")
List<StockResponse> batchQuery(@RequestBody List<String> skuList);
}
실제 비즈니스 로직에서 주입하여 사용합니다.
@Component
public class PaymentProcessor {
private final InventoryRemoteClient inventoryClient;
public PaymentProcessor(InventoryRemoteClient inventoryClient) {
this.inventoryClient = inventoryClient;
}
public PaymentResult process(PaymentRequest req) {
// 원격 호출이 로컬 메서드 호출처럼 읽힘
StockResponse stock = inventoryClient.checkAvailability(req.getSku());
if (!stock.isAvailable()) {
throw new OutOfStockException();
}
// 후속 처리...
}
}
세밀한 통신 설정
서비스별로 다른 특성을 가질 수 있으므로, 개별 설정이 가능합니다.
feign:
client:
config:
# 전역 기본값
default:
connect-timeout: 2000
read-timeout: 8000
follow-redirects: false
default-request-headers:
X-Source-System: payment-service
# 특정 클라이언트 오버라이드
inventory-service:
connect-timeout: 1000
read-timeout: 3000
logger-level: headers
# 느린 서비스용 관대한 설정
legacy-pricing-service:
connect-timeout: 5000
read-timeout: 30000
요청/응답 흐름 가시화
운영 환경과 개발 환경에서 로깅 전략을 다르게 적용합니다.
public class FeignLoggingConfiguration {
@Bean
@Profile("dev")
public Logger.Level verboseLogging() {
return Logger.Level.FULL;
}
@Bean
@Profile("prod")
public Logger.Level essentialLogging() {
return Logger.Level.BASIC;
}
}
로그 레벨 상세:
- NONE: 완전한 생략 (프로덕션 권장)
- BASIC: 메서드, URL, 상태코드, 소요시간
- HEADERS: BASIC + 헤더 정보
- FULL: 전체 바디 포함 (민감정보 유출 주의)
컨텍스트 전파: 인증 정보 전달
마이크로서비스 간 인증 컨텍스트를 유지하는 패턴입니다.
@Component
public class SecurityContextPropagator implements RequestInterceptor {
private static final String AUTH_HEADER = "Authorization";
private static final String TENANT_HEADER = "X-Tenant-Id";
@Override
public void apply(RequestTemplate template) {
// 현재 스레드의 보안 컨텍스트에서 추출
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getCredentials() instanceof String) {
template.header(AUTH_HEADER, "Bearer " + auth.getCredentials());
}
// MDC에서 테넌트 정보 추출
String tenantId = MDC.get("tenantId");
if (tenantId != null) {
template.header(TENANT_HEADER, tenantId);
}
}
}
장애 대응: Fallback 구현
네트워크 장애 시 대체 동작을 제공하여 시스템 안정성을 확보합니다.
@FeignClient(
name = "shipping-service",
fallbackFactory = ShippingClientFallback.Factory.class
)
public interface ShippingClient {
@PostMapping("/shipments")
ShipmentInfo createShipment(@RequestBody ShipmentRequest request);
@GetMapping("/shipments/{trackingNo}")
ShipmentStatus trackPackage(@PathVariable String trackingNo);
}
@Component
@Slf4j
public class ShippingClientFallback implements ShippingClient {
private final Throwable failureCause;
public ShippingClientFallback(Throwable cause) {
this.failureCause = cause;
}
@Override
public ShipmentInfo createShipment(ShipmentRequest request) {
log.warn("배송 서비스 호출 실패, 대기열에 저장: {}", failureCause.getMessage());
// 비동기 재처리를 위한 이벤트 발행
return ShipmentInfo.pending(request.getOrderId());
}
@Override
public ShipmentStatus trackPackage(String trackingNo) {
// 캐시된 마지막 상태 반환 또는 기본값
return ShipmentStatus.unknown(trackingNo);
}
@Component
public static class Factory implements FallbackFactory<ShippingClient> {
@Override
public ShippingClient create(Throwable cause) {
return new ShippingClientFallback(cause);
}
}
}
Fallback 활성화를 위한 설정:
feign:
circuitbreaker:
enabled: true
# Resilience4j 또는 Sentinel 연동 시
resilience4j:
circuitbreaker:
configs:
default:
failure-rate-threshold: 50
wait-duration-in-open-state: 10s
고급 패턴: 동적 URL 구성
런타임에 대상 서비스를 결정해야 하는 시나리오를 처리합니다.
@FeignClient(name = "dynamic-client", url = "PLACEHOLDER")
public interface DynamicTargetClient {
// ...
}
// 수동 빌더로 동적 인스턴스 생성
public class FeignClientFactory {
public <T> T createClient(Class<T> clazz, String actualUrl) {
return Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(clazz, actualUrl);
}
}
성능 최적화 팁
- 연결 풀: Apache HttpClient 또는 OkHttp로 대체하여 연결 재사용
- 압축:
feign.compression.request.enabled활성화 - 응답 캐싱: 변하지 않는 데이터는
@Cacheable과 결합 - 비동기 처리: CompletableFuture 반환 타입과
AsyncFeign고려
// Apache HttpClient 연동 예시
feign:
httpclient:
enabled: true
max-connections: 200
max-connections-per-route: 50
time-to-live: 900
OpenFeign은 선언적 방식으로 복잡한 HTTP 통신을 단순화하며, Spring Cloud 생태계와의 긴한 통합으로 마이크로서비스 간 협업을 효율적으로 구현할 수 있게 합니다.