| 항목 | WebClient | RestClient | FeignClient | RestTemplate |
|---|---|---|---|---|
| 프로그래밍 모델 | 리액티브(논블로킹) | 동기(블로킹) 중심, 비동기 일부 | 선언형(인터페이스) | 동기(블로킹) |
| 스트리밍(SSE/청크) | 최고(backpressure, Flux) | 제한적(직접 스트림 처리 필요) | 제한적(라이브러리·커스터마이징 의존) | 가능하나 불편 |
| 동시성/자원 효율 | 매우 좋음 | 보통 | 보통(아래 HttpClient에 따라) | 보통 |
| 사용 난이도 | 중 | 쉬움 | 쉬움(선언형) | 쉬움(레거시) |
| 관측성/필터링 | ExchangeFilter, Reactor hook | Interceptor | RequestInterceptor | ClientHttpRequestInterceptor |
| 재시도/서킷브레이커 | Resilience4j, retryWhen | Resilience4j | Resilience4j 통합 용이 | Resilience4j |
| 권장도(신규) | 높음 | 높음 | 조건부 | 낮음 |
- 스트리밍 또는 높은 동시성/효율이 핵심이면: WebClient 단일화가 가장 안정적.
- 스트리밍이 전혀 없고, 호출이 단순하면: RestClient로 깔끔.
- 선언형 인터페이스로 여러 외부/내부 서비스 통합이 목표면: OpenFeign + (스트리밍 엔드포인트만 WebClient 보조)
결국, 무엇을 중요하게 보느냐(스트리밍/성능/개발 편의/확장)에 따라 선택하면 좋을 것 같다고 생각되었다.
이번 프로젝트에서는
이러한 요구사항을 충족하기 때문에, RestClient를 선택하는 것이 가장 적합하다고 생각되어, 구현 최소 예시를 찾아보았다.
@Configuration
public class AiRestClientConfig {
@Bean
RestClient aiRestClient(RestClient.Builder builder) {
return builder
.baseUrl("https://api.example.ai")
.requestInterceptor((req, body, ex) -> {
// Authorization 마스킹 로깅 등
})
.build();
}
}
@Service
@RequiredArgsConstructor
public class AiSyncService {
private final RestClient aiRestClient;
private final AiLogRepository aiLogRepository;
@Transactional
public AiCallLog completeOnce(AiPrompt prompt) {
long started = System.currentTimeMillis();
var response = aiRestClient.post()
.uri("/v1/chat/completions")
.body(Map.of("model", prompt.model(), "messages", prompt.messages()))
.retrieve()
.toEntity(String.class);
var log = AiCallLog.of(
prompt,
response.getStatusCode().value(),
response.getBody(),
System.currentTimeMillis() - started
);
return aiLogRepository.save(log);
}
}
ai_call_log
id (PK), request_id(외부 idempotency), model, provider, prompt_hashcreated_at, completed_at, status(PENDING/PROCESSING/SUCCESS/FAILED)