Java/Spring REST API 클라이언트 라이브러리 완전 비교
Java/Spring 생태계에는 HTTP 클라이언트 라이브러리가 매우 다양합니다. RestTemplate, WebClient, RestClient, OpenFeign, Retrofit, Java HttpClient, OkHttp까지 선택지가 많아 어떤 것을 써야 할지 혼란스러울 수 있습니다. 이 글에서는 각 라이브러리의 동작 원리, 장단점, 실무 코드 예제까지 깊이 있게 비교합니다.
RestTemplate
동작 원리 — 동기/블로킹
RestTemplate은 Spring Framework 3.0에서 도입된 동기(synchronous) HTTP 클라이언트입니다. 호출 스레드가 HTTP 응답이 올 때까지 블로킹됩니다.
요청 스레드
│
├── RestTemplate.getForObject() 호출
│
├── [블로킹 대기 — HTTP 응답 수신까지]
│
└── 응답 반환 후 다음 로직 실행
내부적으로 ClientHttpRequestFactory를 통해 실제 HTTP 연결을 생성합니다. 기본 구현은 SimpleClientHttpRequestFactory(JDK HttpURLConnection)이며, Apache HttpClient나 OkHttp로 교체 가능합니다.
기본 사용법
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.connectTimeout(Duration.ofSeconds(5))
.readTimeout(Duration.ofSeconds(10))
.build();
}
}
@Service
public class UserService {
private final RestTemplate restTemplate;
private static final String BASE_URL = "https://api.example.com";
public UserService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
// GET 요청
public User getUser(Long id) {
return restTemplate.getForObject(BASE_URL + "/users/{id}", User.class, id);
}
// GET 요청 — ResponseEntity로 헤더/상태코드 포함
public ResponseEntity<User> getUserWithResponse(Long id) {
return restTemplate.getForEntity(BASE_URL + "/users/{id}", User.class, id);
}
// POST 요청
public User createUser(UserRequest request) {
return restTemplate.postForObject(BASE_URL + "/users", request, User.class);
}
// PUT 요청
public void updateUser(Long id, UserRequest request) {
restTemplate.put(BASE_URL + "/users/{id}", request, id);
}
// DELETE 요청
public void deleteUser(Long id) {
restTemplate.delete(BASE_URL + "/users/{id}", id);
}
// exchange — 메서드/헤더/바디 완전 제어
public List<User> searchUsers(String keyword) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + getToken());
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<List<User>> response = restTemplate.exchange(
BASE_URL + "/users/search?keyword=" + keyword,
HttpMethod.GET,
entity,
new ParameterizedTypeReference<List<User>>() {} // 제네릭 타입
);
return response.getBody();
}
}
설정 — 타임아웃, 인터셉터, 에러 핸들러
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
// Apache HttpClient 기반 — 커넥션 풀 지원
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
// 커넥션 풀 설정
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(200); // 전체 최대 커넥션
connectionManager.setDefaultMaxPerRoute(50); // 호스트당 최대 커넥션
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(RequestConfig.custom()
.setConnectTimeout(Timeout.ofSeconds(5)) // 연결 타임아웃
.setResponseTimeout(Timeout.ofSeconds(10)) // 읽기 타임아웃
.setConnectionRequestTimeout(Timeout.ofSeconds(2)) // 풀에서 커넥션 획득 타임아웃
.build())
.build();
factory.setHttpClient(httpClient);
RestTemplate restTemplate = new RestTemplate(factory);
// 인터셉터 추가 — 로깅, 인증 헤더 추가 등
restTemplate.setInterceptors(List.of(
new LoggingInterceptor(),
new AuthInterceptor()
));
// 에러 핸들러 커스터마이즈
restTemplate.setErrorHandler(new CustomErrorHandler());
return restTemplate;
}
}
// 로깅 인터셉터
public class LoggingInterceptor implements ClientHttpRequestInterceptor {
private static final Logger log = LoggerFactory.getLogger(LoggingInterceptor.class);
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution)
throws IOException {
log.info("HTTP 요청 — {} {}", request.getMethod(), request.getURI());
ClientHttpResponse response = execution.execute(request, body);
log.info("HTTP 응답 — Status: {}", response.getStatusCode());
return response;
}
}
// 커스텀 에러 핸들러
public class CustomErrorHandler extends DefaultResponseErrorHandler {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
HttpStatusCode statusCode = response.getStatusCode();
if (statusCode.is4xxClientError()) {
throw new ClientException("클라이언트 오류: " + statusCode);
} else if (statusCode.is5xxServerError()) {
throw new ServerException("서버 오류: " + statusCode);
}
super.handleError(response);
}
}
왜 Deprecated 방향인가?
Spring 공식 문서는 Spring Framework 6.1부터 RestTemplate을 유지보수 모드로 전환하고 RestClient 또는 WebClient 사용을 권장합니다.
- 블로킹 I/O: 스레드당 하나의 요청만 처리 — 동시 요청이 많으면 스레드 수가 늘어 메모리/컨텍스트 스위칭 비용 증가
- 레거시 API 설계:
exchange(),getForObject()등 메서드명이 직관적이지 않음 - Fluent API 부재: 요청 구성이 장황함
- WebFlux와의 부조화: 리액티브 스택과 함께 사용하기 어려움
WebClient (Spring WebFlux)
비동기/논블로킹 동작 원리
WebClient는 Spring WebFlux에서 제공하는 논블로킹(non-blocking) HTTP 클라이언트입니다. 요청 스레드가 응답을 기다리지 않고 다른 작업을 계속 처리합니다.
요청 스레드
│
├── WebClient.get().retrieve() 호출 → 즉시 반환 (Mono/Flux)
│
├── [스레드는 다른 작업 처리 중]
│
└── 응답 도착 시 콜백/리액티브 파이프라인 실행
Reactor Netty 기반
WebClient는 기본적으로 Reactor Netty를 사용합니다. Netty의 이벤트 루프 기반으로 적은 수의 스레드로 수천 개의 동시 연결을 처리합니다.
Reactor Netty 이벤트 루프 (CPU 코어 수 * 2개 스레드)
├── EventLoop-1 → 커넥션 100개 처리
├── EventLoop-2 → 커넥션 100개 처리
└── EventLoop-N → 커넥션 100개 처리
vs RestTemplate (Servlet 스레드 모델)
├── Thread-1 → 커넥션 1개 (블로킹)
├── Thread-2 → 커넥션 1개 (블로킹)
└── Thread-N → 커넥션 1개 (블로킹)
Mono/Flux 사용법
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
// 커넥션 풀 및 타임아웃 설정
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofSeconds(10))
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(5)));
return WebClient.builder()
.baseUrl("https://api.example.com")
.clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.codecs(configurer ->
configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)) // 2MB
.filter(ExchangeFilterFunctions.basicAuthentication("user", "pass"))
.filter(logRequest())
.build();
}
private ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
log.info("WebClient 요청 — {} {}", clientRequest.method(), clientRequest.url());
return Mono.just(clientRequest);
});
}
}
@Service
public class UserWebClientService {
private final WebClient webClient;
// GET — Mono (단일 객체)
public Mono<User> getUser(Long id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError,
response -> Mono.error(new ClientException("클라이언트 오류")))
.onStatus(HttpStatusCode::is5xxServerError,
response -> Mono.error(new ServerException("서버 오류")))
.bodyToMono(User.class);
}
// GET — Flux (스트리밍 목록)
public Flux<User> getAllUsers() {
return webClient.get()
.uri("/users")
.retrieve()
.bodyToFlux(User.class);
}
// POST — 요청 바디 포함
public Mono<User> createUser(UserRequest request) {
return webClient.post()
.uri("/users")
.bodyValue(request)
.retrieve()
.bodyToMono(User.class);
}
// 헤더 포함 요청
public Mono<ResponseEntity<User>> getUserWithHeaders(Long id, String token) {
return webClient.get()
.uri("/users/{id}", id)
.header("Authorization", "Bearer " + token)
.retrieve()
.toEntity(User.class);
}
// 복잡한 에러 처리 — ResponseSpec 활용
public Mono<User> getUserSafe(Long id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(User.class)
.timeout(Duration.ofSeconds(3)) // 개별 요청 타임아웃
.retry(2) // 실패 시 2회 재시도
.onErrorReturn(TimeoutException.class, User.defaultUser()) // 타임아웃 시 기본값
.onErrorResume(WebClientResponseException.NotFound.class,
e -> Mono.empty()); // 404 시 empty
}
// 병렬 요청 — Mono.zip
public Mono<UserProfile> getUserProfile(Long userId) {
Mono<User> userMono = getUser(userId);
Mono<List<Order>> ordersMono = getOrders(userId).collectList();
Mono<Address> addressMono = getAddress(userId);
return Mono.zip(userMono, ordersMono, addressMono)
.map(tuple -> UserProfile.of(tuple.getT1(), tuple.getT2(), tuple.getT3()));
}
}
동기 모드로도 사용 가능 (.block())
WebFlux를 사용하지 않는 환경(Spring MVC)에서도 WebClient를 동기적으로 사용할 수 있습니다.
// .block()으로 동기 변환 — Mono → 값
User user = webClient.get()
.uri("/users/{id}", 1L)
.retrieve()
.bodyToMono(User.class)
.block(Duration.ofSeconds(5)); // 최대 5초 대기
// .collectList().block()으로 Flux → List 변환
List<User> users = webClient.get()
.uri("/users")
.retrieve()
.bodyToFlux(User.class)
.collectList()
.block();
주의: 리액티브 파이프라인 내에서 .block() 호출은 데드락을 유발할 수 있습니다. Spring MVC 컨트롤러(서블릿 스레드)에서만 사용하세요.
RestClient (Spring 6.1+)
새로운 동기 HTTP 클라이언트
RestClient는 Spring Framework 6.1(Spring Boot 3.2)에서 도입된 새로운 동기 HTTP 클라이언트입니다. RestTemplate을 대체하며, WebClient의 Fluent API를 동기 방식으로 제공합니다.
RestTemplate의 후계자: RestTemplate과 동일하게 동기/블로킹으로 동작하지만, API 설계가 훨씬 직관적입니다.
Fluent API 사용법
@Configuration
public class RestClientConfig {
@Bean
public RestClient restClient() {
return RestClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultStatusHandler(HttpStatusCode::is4xxClientError,
(req, res) -> {
throw new ClientException("4xx 오류: " + res.getStatusCode());
})
.defaultStatusHandler(HttpStatusCode::is5xxServerError,
(req, res) -> {
throw new ServerException("5xx 오류: " + res.getStatusCode());
})
.requestInterceptor((req, body, execution) -> {
req.getHeaders().add("X-Request-Id", UUID.randomUUID().toString());
return execution.execute(req, body);
})
.build();
}
}
@Service
public class UserRestClientService {
private final RestClient restClient;
// GET — 단일 객체
public User getUser(Long id) {
return restClient.get()
.uri("/users/{id}", id)
.retrieve()
.body(User.class);
}
// GET — 제네릭 타입 (List, Map 등)
public List<User> getAllUsers() {
return restClient.get()
.uri("/users")
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});
}
// GET — ResponseEntity (헤더, 상태코드 포함)
public ResponseEntity<User> getUserWithMeta(Long id) {
return restClient.get()
.uri("/users/{id}", id)
.retrieve()
.toEntity(User.class);
}
// POST — 요청 바디
public User createUser(UserRequest request) {
return restClient.post()
.uri("/users")
.body(request)
.retrieve()
.body(User.class);
}
// PUT — 업데이트
public User updateUser(Long id, UserRequest request) {
return restClient.put()
.uri("/users/{id}", id)
.body(request)
.retrieve()
.body(User.class);
}
// DELETE — 상태코드 확인
public void deleteUser(Long id) {
restClient.delete()
.uri("/users/{id}", id)
.retrieve()
.toBodilessEntity(); // 바디 없는 응답
}
// 쿼리 파라미터 빌더 활용
public List<User> searchUsers(String name, int page, int size) {
return restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/users/search")
.queryParam("name", name)
.queryParam("page", page)
.queryParam("size", size)
.build())
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});
}
// 커스텀 에러 처리 — 요청별
public Optional<User> findUser(Long id) {
try {
User user = restClient.get()
.uri("/users/{id}", id)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError,
(req, res) -> {}) // 4xx 무시
.body(User.class);
return Optional.ofNullable(user);
} catch (Exception e) {
return Optional.empty();
}
}
}
RestTemplate과 RestClient API 비교
// RestTemplate — 장황한 API
ResponseEntity<List<User>> response = restTemplate.exchange(
"/users",
HttpMethod.GET,
new HttpEntity<>(headers),
new ParameterizedTypeReference<List<User>>() {}
);
List<User> users = response.getBody();
// RestClient — 간결한 Fluent API
List<User> users = restClient.get()
.uri("/users")
.headers(h -> h.addAll(headers))
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});
RestTemplate → RestClient 마이그레이션
// RestClient는 기존 RestTemplate 인프라 재사용 가능
@Bean
public RestClient restClientFromRestTemplate(RestTemplate restTemplate) {
return RestClient.create(restTemplate); // RestTemplate 설정(인터셉터, 커넥션 풀 등) 재사용
}
OpenFeign (Spring Cloud)
선언적 HTTP 클라이언트
OpenFeign은 인터페이스 선언만으로 HTTP 클라이언트를 구현하는 선언적(Declarative) HTTP 클라이언트입니다. 실제 HTTP 호출 코드를 직접 작성하지 않습니다.
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
// 메인 클래스에 활성화
@SpringBootApplication
@EnableFeignClients
public class Application { ... }
인터페이스 기반 (@FeignClient)
// Feign 클라이언트 인터페이스 선언
@FeignClient(
name = "user-service",
url = "${services.user-service.url}",
fallback = UserServiceFallback.class, // Circuit Breaker fallback
configuration = FeignClientConfig.class // 커스텀 설정
)
public interface UserServiceClient {
@GetMapping("/users/{id}")
User getUser(@PathVariable Long id);
@GetMapping("/users")
List<User> getAllUsers(
@RequestParam String name,
@RequestParam int page,
@RequestParam int size
);
@PostMapping("/users")
User createUser(@RequestBody UserRequest request);
@PutMapping("/users/{id}")
User updateUser(@PathVariable Long id, @RequestBody UserRequest request);
@DeleteMapping("/users/{id}")
void deleteUser(@PathVariable Long id);
// 헤더 전달
@GetMapping("/users/me")
User getCurrentUser(@RequestHeader("Authorization") String token);
}
// 서비스에서 일반 빈처럼 사용
@Service
public class OrderService {
private final UserServiceClient userServiceClient;
public Order createOrder(Long userId, OrderRequest request) {
User user = userServiceClient.getUser(userId); // HTTP 호출 자동 처리
// ...
}
}
Feign 설정 커스터마이즈
@Configuration
public class FeignClientConfig {
// 타임아웃 설정
@Bean
public Request.Options options() {
return new Request.Options(
5, TimeUnit.SECONDS, // connectTimeout
10, TimeUnit.SECONDS, // readTimeout
true // followRedirects
);
}
// 재시도 설정
@Bean
public Retryer retryer() {
return new Retryer.Default(
100, // 초기 대기 ms
1000, // 최대 대기 ms
3 // 최대 시도 횟수
);
}
// 로깅 수준
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL; // NONE, BASIC, HEADERS, FULL
}
// 인터셉터 — 모든 요청에 Authorization 헤더 추가
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
String token = SecurityContextHolder.getContext()
.getAuthentication().getCredentials().toString();
requestTemplate.header("Authorization", "Bearer " + token);
};
}
// 에러 디코더
@Bean
public ErrorDecoder errorDecoder() {
return (methodKey, response) -> {
if (response.status() == 404) {
return new NotFoundException("리소스를 찾을 수 없습니다.");
}
if (response.status() >= 500) {
return new ServiceException("서비스 오류: " + response.status());
}
return new Default().decode(methodKey, response);
};
}
}
Circuit Breaker 연동 (Resilience4j)
# application.yml
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true
resilience4j:
circuitbreaker:
instances:
user-service:
registerHealthIndicator: true
slidingWindowSize: 10
minimumNumberOfCalls: 5
permittedNumberOfCallsInHalfOpenState: 3
failureRateThreshold: 50
waitDurationInOpenState: 30s
// Fallback 구현
@Component
public class UserServiceFallback implements UserServiceClient {
@Override
public User getUser(Long id) {
return User.defaultUser(id); // 기본값 반환
}
@Override
public List<User> getAllUsers(String name, int page, int size) {
return Collections.emptyList(); // 빈 목록 반환
}
@Override
public User createUser(UserRequest request) {
throw new ServiceUnavailableException("사용자 서비스 일시 중단");
}
// ...
}
장단점
| 항목 | 내용 |
|---|---|
| 장점 | 선언적 코드, 보일러플레이트 최소화, Spring Cloud 통합 용이 |
| 장점 | Circuit Breaker, 로드밸런싱(Ribbon/LoadBalancer) 연동 |
| 단점 | 동기/블로킹 (WebFlux와 부조화) |
| 단점 | Spring Cloud 의존성 필요 |
| 단점 | 복잡한 요청 구성 시 한계 (인터페이스 제약) |
Retrofit
Square 라이브러리
Retrofit은 Square가 개발한 인터페이스 기반 HTTP 클라이언트입니다. Android와 서버 양쪽에서 모두 사용 가능하며, OkHttp를 기반으로 합니다.
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-jackson</artifactId>
<version>2.11.0</version>
</dependency>
인터페이스 기반 선언
// API 인터페이스 정의
public interface UserApi {
@GET("users/{id}")
Call<User> getUser(@Path("id") Long id);
@GET("users")
Call<List<User>> getUsers(
@Query("name") String name,
@Query("page") int page
);
@POST("users")
Call<User> createUser(@Body UserRequest request);
@PUT("users/{id}")
Call<User> updateUser(@Path("id") Long id, @Body UserRequest request);
@DELETE("users/{id}")
Call<Void> deleteUser(@Path("id") Long id);
@Headers("Accept: application/json")
@GET("users/profile")
Call<User> getProfile(@Header("Authorization") String token);
// 비동기 지원 (RxJava)
@GET("users/{id}")
Observable<User> getUserReactive(@Path("id") Long id);
// Kotlin Coroutines 지원
// @GET("users/{id}")
// suspend fun getUserSuspend(@Path("id") Long id): User
}
// Retrofit 인스턴스 생성
@Configuration
public class RetrofitConfig {
@Bean
public UserApi userApi() {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.addInterceptor(new HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY))
.addInterceptor(chain -> {
Request request = chain.request().newBuilder()
.addHeader("X-API-Key", "api-key-value")
.build();
return chain.proceed(request);
})
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(JacksonConverterFactory.create())
.build();
return retrofit.create(UserApi.class);
}
}
// 사용 — 동기
@Service
public class UserRetrofitService {
private final UserApi userApi;
// 동기 호출
public User getUser(Long id) throws IOException {
Response<User> response = userApi.getUser(id).execute();
if (response.isSuccessful()) {
return response.body();
}
throw new ApiException("API 오류: " + response.code());
}
// 비동기 호출 (Callback)
public void getUserAsync(Long id, Consumer<User> onSuccess, Consumer<Throwable> onError) {
userApi.getUser(id).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
if (response.isSuccessful()) {
onSuccess.accept(response.body());
} else {
onError.accept(new ApiException("오류: " + response.code()));
}
}
@Override
public void onFailure(Call<User> call, Throwable t) {
onError.accept(t);
}
});
}
}
Retrofit vs OpenFeign
| 항목 | Retrofit | OpenFeign |
|---|---|---|
| 주요 환경 | Android + 서버 | Spring Cloud 서버 |
| Spring 통합 | 직접 구성 필요 | @EnableFeignClients로 통합 |
| 비동기 | RxJava/Coroutines | 기본 동기 |
| Circuit Breaker | 직접 연동 필요 | Spring Cloud 통합 용이 |
| 어노테이션 | Retrofit 어노테이션 | Spring MVC 어노테이션 |
HttpClient (Java 11+)
JDK 내장 — 외부 의존성 없음
Java 11에서 도입된 java.net.http.HttpClient는 JDK 내장 HTTP 클라이언트입니다. 외부 라이브러리 없이 HTTP/1.1, HTTP/2, WebSocket을 지원합니다.
// HttpClient 인스턴스 생성
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // HTTP/2 우선
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NORMAL)
.executor(Executors.newFixedThreadPool(10)) // 비동기 실행 스레드 풀
.build();
@Service
public class UserHttpClientService {
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
private static final String BASE_URL = "https://api.example.com";
// 동기 GET
public User getUser(Long id) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(URI.create(BASE_URL + "/users/" + id))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(10))
.build();
HttpResponse<String> response = httpClient.send(
request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
return objectMapper.readValue(response.body(), User.class);
}
throw new ApiException("오류: " + response.statusCode());
}
// 비동기 GET — CompletableFuture
public CompletableFuture<User> getUserAsync(Long id) {
HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(URI.create(BASE_URL + "/users/" + id))
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> {
if (response.statusCode() != 200) {
throw new RuntimeException("오류: " + response.statusCode());
}
try {
return objectMapper.readValue(response.body(), User.class);
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON 파싱 오류", e);
}
});
}
// POST — JSON 바디
public User createUser(UserRequest request) throws Exception {
String requestBody = objectMapper.writeValueAsString(request);
HttpRequest httpRequest = HttpRequest.newBuilder()
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.uri(URI.create(BASE_URL + "/users"))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.build();
HttpResponse<String> response = httpClient.send(
httpRequest, HttpResponse.BodyHandlers.ofString());
return objectMapper.readValue(response.body(), User.class);
}
// 병렬 요청
public List<User> getUsersParallel(List<Long> userIds) {
List<CompletableFuture<User>> futures = userIds.stream()
.map(this::getUserAsync)
.toList();
return futures.stream()
.map(CompletableFuture::join)
.toList();
}
}
HTTP/2 지원
// HTTP/2 서버 푸시 지원
HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // HTTP/2 우선, 불가 시 HTTP/1.1로 폴백
.build();
// 응답 헤더에서 HTTP 버전 확인
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.version()); // HTTP_2 또는 HTTP_1_1
OkHttp
커넥션 풀, 인터셉터, 캐시
OkHttp는 Square가 개발한 효율적인 HTTP 클라이언트입니다. Retrofit의 기반이기도 하며, 자체적으로 사용해도 강력합니다.
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
// OkHttpClient 설정
OkHttpClient client = new OkHttpClient.Builder()
// 타임아웃
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
// 커넥션 풀 (기본: 5개, 5분 유지)
.connectionPool(new ConnectionPool(20, 5, TimeUnit.MINUTES))
// 캐시 설정
.cache(new Cache(new File("cache"), 10 * 1024 * 1024)) // 10MB 캐시
// 애플리케이션 인터셉터 (재시도 포함, 캐시 전)
.addInterceptor(new HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY))
.addInterceptor(chain -> {
// 인증 헤더 자동 추가
Request original = chain.request();
Request request = original.newBuilder()
.header("Authorization", "Bearer " + getToken())
.build();
return chain.proceed(request);
})
// 네트워크 인터셉터 (리다이렉트 후, 캐시 후)
.addNetworkInterceptor(chain -> {
Response response = chain.proceed(chain.request());
// 캐시 제어 헤더 수정
return response.newBuilder()
.header("Cache-Control", "max-age=60")
.build();
})
// 재시도 설정
.retryOnConnectionFailure(true)
.build();
@Service
public class UserOkHttpService {
private final OkHttpClient client;
private final ObjectMapper objectMapper;
private static final MediaType JSON = MediaType.get("application/json");
// 동기 GET
public User getUser(Long id) throws IOException {
Request request = new Request.Builder()
.url("https://api.example.com/users/" + id)
.get()
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new ApiException("오류: " + response.code());
}
return objectMapper.readValue(response.body().string(), User.class);
}
}
// 비동기 POST
public void createUserAsync(UserRequest userRequest, Consumer<User> onSuccess,
Consumer<IOException> onError) throws JsonProcessingException {
String json = objectMapper.writeValueAsString(userRequest);
RequestBody body = RequestBody.create(json, JSON);
Request request = new Request.Builder()
.url("https://api.example.com/users")
.post(body)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
User user = objectMapper.readValue(response.body().string(), User.class);
onSuccess.accept(user);
}
}
@Override
public void onFailure(Call call, IOException e) {
onError.accept(e);
}
});
}
}
Retrofit의 기반으로서의 OkHttp
// Retrofit이 OkHttp를 내부적으로 사용
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES))
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient) // OkHttp 공유
.addConverterFactory(JacksonConverterFactory.create())
.build();
종합 비교 표
기본 특성 비교
| 라이브러리 | 동기/비동기 | Spring 통합 | 외부 의존성 | 선언적 API | HTTP/2 |
|---|---|---|---|---|---|
| RestTemplate | 동기 | Spring MVC 내장 | 없음 | X | △ |
| WebClient | 비동기 (동기 가능) | Spring WebFlux 내장 | Reactor Netty | X | O |
| RestClient | 동기 | Spring 6.1+ 내장 | 없음 | X | △ |
| OpenFeign | 동기 | Spring Cloud | Spring Cloud | O | X |
| Retrofit | 동기/비동기 | 수동 설정 | OkHttp | O | O |
| Java HttpClient | 동기/비동기 | 없음 | 없음 (JDK) | X | O |
| OkHttp | 동기/비동기 | 없음 | 없음 | X | O |
성능 및 편의성 비교
| 라이브러리 | 처리량 | 메모리 효율 | 코드 양 | 학습 곡선 | 테스트 용이성 |
|---|---|---|---|---|---|
| RestTemplate | 보통 | 보통 | 많음 | 낮음 | 보통 |
| WebClient | 높음 | 높음 | 중간 | 높음 | 보통 |
| RestClient | 보통 | 보통 | 적음 | 낮음 | 높음 |
| OpenFeign | 보통 | 보통 | 매우 적음 | 낮음 | 높음 |
| Retrofit | 보통~높음 | 보통 | 매우 적음 | 낮음 | 높음 |
| Java HttpClient | 보통~높음 | 높음 | 많음 | 중간 | 낮음 |
| OkHttp | 높음 | 높음 | 중간 | 중간 | 중간 |
Spring 생태계 통합도
| 라이브러리 | Spring Boot AutoConfig | Spring Security 연동 | Actuator 통합 | Spring Cloud |
|---|---|---|---|---|
| RestTemplate | O (RestTemplateBuilder) | O | 제한적 | O |
| WebClient | O | O | O (Metrics) | O |
| RestClient | O | O | O | O |
| OpenFeign | O | O | O | O (핵심) |
| Retrofit | X (수동) | 수동 | X | X |
| Java HttpClient | X (수동) | 수동 | X | X |
| OkHttp | X (수동) | 수동 | X | X |
실무 선택 가이드 (상황별 추천)
의사결정 트리
Spring Boot 3.x 프로젝트인가?
├── Yes
│ ├── 비동기/리액티브 필요한가?
│ │ ├── Yes → WebClient (Reactor 기반)
│ │ └── No
│ │ ├── MSA/서비스 간 호출이 많은가?
│ │ │ ├── Yes, Spring Cloud 사용 중 → OpenFeign
│ │ │ └── No → RestClient (Spring 6.1+)
│ └── (RestTemplate은 신규 개발 지양)
│
└── No (레거시 Spring MVC)
├── 비동기 필요 → WebClient (.block() 허용)
├── MSA → OpenFeign
└── 기존 RestTemplate 유지 (레거시 유지보수)
Android/멀티플랫폼?
└── Retrofit + OkHttp
Spring 외부 Java 프로젝트?
├── 의존성 최소화 → Java HttpClient (JDK 11+)
└── 고성능 필요 → OkHttp
시나리오별 추천
시나리오 1: Spring Boot 3.x MVC, 단순 외부 API 호출
// 추천: RestClient
// 이유: Spring 내장, 간결한 Fluent API, 동기 방식으로 충분
@Bean
public RestClient restClient() {
return RestClient.builder()
.baseUrl("https://external-api.com")
.build();
}
시나리오 2: MSA 환경, 서비스 간 동기 호출
// 추천: OpenFeign
// 이유: 선언적 API, Circuit Breaker 통합, 로드밸런싱 용이
@FeignClient(name = "inventory-service", fallback = InventoryFallback.class)
public interface InventoryClient {
@GetMapping("/inventory/{productId}")
Inventory checkInventory(@PathVariable Long productId);
}
시나리오 3: 고트래픽 비동기 처리, WebFlux 스택
// 추천: WebClient
// 이유: 논블로킹 I/O, Reactor 파이프라인 통합, 높은 동시성
webClient.get()
.uri("/products")
.retrieve()
.bodyToFlux(Product.class)
.flatMap(product -> processAsync(product))
.subscribe();
시나리오 4: 외부 라이브러리 최소화, JDK만 사용
// 추천: Java HttpClient (JDK 11+)
// 이유: 의존성 없음, HTTP/2 지원
HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build();
시나리오 5: Android + 서버 공유 코드, 멀티플랫폼
// 추천: Retrofit + OkHttp
// 이유: Android 표준, 선언적 API, Kotlin Coroutines 지원
시나리오 6: 레거시 Spring 5.x 유지보수
// 추천: RestTemplate 유지 (마이그레이션 비용 vs 이점 검토)
// 신규 기능은 RestClient/WebClient 도입 검토
라이브러리별 의존성 요약
<!-- RestTemplate, RestClient, WebClient — Spring Boot에 포함 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- WebClient — WebFlux 의존성 추가 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Retrofit -->
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.11.0</version>
</dependency>
<!-- OkHttp (단독 사용) -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
정리
| 라이브러리 | 추천 상황 | 비추천 상황 |
|---|---|---|
| RestClient | Spring Boot 3.x 신규, 동기 API 호출 | 비동기, Spring 6.1 미만 |
| WebClient | 비동기/리액티브, 고동시성 | 단순 동기 호출 (.block() 남용) |
| OpenFeign | MSA 서비스 간 호출, Spring Cloud | 단독 사용, 비동기 필요 |
| RestTemplate | 레거시 유지보수 | 신규 개발 |
| Retrofit | Android/멀티플랫폼, Kotlin Coroutines | Spring 전용 서버 |
| Java HttpClient | 의존성 최소화, HTTP/2 | 복잡한 요청 구성 |
| OkHttp | Retrofit 하위, 커스텀 인터셉터/캐시 | Spring 통합이 필요한 경우 |
Spring Boot 3.x 신규 프로젝트라면 동기는 RestClient, 비동기는 WebClient, MSA는 OpenFeign 조합이 가장 자연스럽습니다. 레거시 코드의 RestTemplate은 섣불리 마이그레이션하기보다 RestClient로의 점진적 전환을 권장합니다.