Spring BootでREST APIを呼び出す方法 - RestTemplateとWebClientの使い分け


マイクロサービスアーキテクチャや外部API連携が当たり前になった今、Spring Bootアプリケーションから外部のREST APIを呼び出す機会は非常に多いですよね。Spring Bootには主に RestTemplate と WebClient という2つのHTTPクライアントが用意されていますが、どちらを選ぶべきか迷うこともあると思います。

この記事では、RestTemplateとWebClientの基本的な使い方から使い分けの基準、実務で必須となるタイムアウト設定やエラーハンドリングまで実践的に解説します。

RestTemplateとWebClientの違い

まずは2つのHTTPクライアントの特徴を理解しましょう。

RestTemplate は従来からSpring Frameworkに含まれている同期型のHTTPクライアントです。シンプルで直感的なAPIが特徴で、多くのプロジェクトで使われてきました。ただし、Spring 5以降はメンテナンスモードとなっており、新規開発では次に紹介するWebClientが推奨されています。

WebClient はSpring 5で導入された非同期・リアクティブ対応のHTTPクライアントです。Spring WebFluxの一部として提供され、ノンブロッキングI/Oによる高いスループットと柔軟なAPIが特徴です。同期的な使い方もできるため、既存のコードベースにも段階的に導入できます。

新規プロジェクトならWebClientを選ぶのが基本ですが、既存のRestTemplateコードが問題なく動作しているなら無理に移行する必要はありません。

依存関係の設定

RestTemplateは spring-boot-starter-web に含まれているため、通常のSpring Boot Webアプリケーションなら追加の依存関係は不要です。

WebClientを使う場合は spring-boot-starter-webflux を追加します。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Gradleの場合はこのように記述します。

implementation 'org.springframework.boot:spring-boot-starter-webflux'

WebFluxを追加してもSpring MVCと共存できるので、既存のControllerには影響ありません。

RestTemplateの基本的な使い方

RestTemplateを使うには、まずBeanとして登録します。

@Configuration
public class RestTemplateConfig {
    
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

GETリクエストでデータを取得する場合は getForObject または getForEntity を使います。

@Service
public class ApiService {
    
    private final RestTemplate restTemplate;
    
    public ApiService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
    
    public User getUser(Long id) {
        String url = "https://api.example.com/users/" + id;
        return restTemplate.getForObject(url, User.class);
    }
}

POSTリクエストでデータを送信する場合は postForObject を使います。

public User createUser(User user) {
    String url = "https://api.example.com/users";
    return restTemplate.postForObject(url, user, User.class);
}

レスポンスのステータスコードやヘッダーも取得したい場合は getForEntitypostForEntity を使うと ResponseEntity が返ってきます。

RestTemplateのタイムアウト設定

本番環境では必ずタイムアウトを設定しましょう。設定しないと、外部APIが応答しない場合に無限に待ち続けてしまいます。

タイムアウトを設定するには Apache HttpClient の依存関係が必要です。

<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
</dependency>

Gradleの場合はこのように記述します。

implementation 'org.apache.httpcomponents.client5:httpclient5'

依存関係を追加したら、HttpComponentsClientHttpRequestFactoryを使ってタイムアウトを設定できます。

import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;

@Bean
public RestTemplate restTemplate() {
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    factory.setConnectTimeout(3000);  // 接続タイムアウト(3秒)
    factory.setReadTimeout(5000);     // 読み取りタイムアウト(5秒)
    return new RestTemplate(factory);
}

RestTemplateBuilderを使うと、より簡潔に設定できます。

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
    return builder
            .setConnectTimeout(Duration.ofSeconds(3))
            .setReadTimeout(Duration.ofSeconds(5))
            .build();
}

接続タイムアウトはサーバーへの接続確立までの時間、読み取りタイムアウトはレスポンスの読み取り完了までの時間を制限します。APIの特性に応じて適切な値を設定してください。

RestTemplateのエラーハンドリング

RestTemplateは4xxや5xxのレスポンスを受け取ると例外をスローします。適切にハンドリングしましょう。

public User getUser(Long id) {
    try {
        String url = "https://api.example.com/users/" + id;
        return restTemplate.getForObject(url, User.class);
    } catch (HttpClientErrorException e) {
        // 4xx系エラー(クライアントエラー)
        if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
            throw new UserNotFoundException("User not found: " + id);
        }
        throw e;
    } catch (HttpServerErrorException e) {
        // 5xx系エラー(サーバーエラー)
        throw new ExternalApiException("API server error", e);
    }
}

より柔軟なエラーハンドリングが必要な場合は ResponseErrorHandler をカスタマイズできます。RestTemplateの設定時に setErrorHandler で独自のハンドラーを登録すると、すべてのリクエストで共通のエラー処理を適用できます。

WebClientの基本的な使い方

WebClientは WebClient.builder() でインスタンスを作成します。

@Configuration
public class WebClientConfig {
    
    @Bean
    public WebClient webClient() {
        return WebClient.builder()
                .baseUrl("https://api.example.com")
                .build();
    }
}

GETリクエストは次のように実装します。block() を使うと同期的に結果を取得できます。

@Service
public class ApiService {
    
    private final WebClient webClient;
    
    public ApiService(WebClient webClient) {
        this.webClient = webClient;
    }
    
    public User getUser(Long id) {
        return webClient.get()
                .uri("/users/{id}", id)
                .retrieve()
                .bodyToMono(User.class)
                .block();
    }
}

POSTリクエストは bodyValue でリクエストボディを設定します。

public User createUser(User user) {
    return webClient.post()
            .uri("/users")
            .bodyValue(user)
            .retrieve()
            .bodyToMono(User.class)
            .block();
}

非同期処理が必要な場合は block() を呼ばずに Mono をそのまま返すこともできます。

public Mono<User> getUserAsync(Long id) {
    return webClient.get()
            .uri("/users/{id}", id)
            .retrieve()
            .bodyToMono(User.class);
}

WebClientのタイムアウト設定

WebClientのタイムアウトはReactor Nettyの設定を通じて行います。

import io.netty.channel.ChannelOption;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import java.time.Duration;

@Bean
public WebClient webClient() {
    HttpClient httpClient = HttpClient.create()
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
            .responseTimeout(Duration.ofSeconds(5));
    
    return WebClient.builder()
            .baseUrl("https://api.example.com")
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .build();
}

RestTemplateと同様に、接続タイムアウトと読み取りタイムアウトを設定できます。個別のリクエストでタイムアウトを上書きしたい場合は timeout() オペレーターも使えます。

WebClientのエラーハンドリング

WebClientは onStatus を使ってステータスコード別のエラーハンドリングができます。

public User getUser(Long id) {
    return webClient.get()
            .uri("/users/{id}", id)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError, response ->
                    response.bodyToMono(String.class)
                            .map(body -> new UserNotFoundException("User not found: " + id))
            )
            .onStatus(HttpStatusCode::is5xxServerError, response ->
                    Mono.error(new ExternalApiException("API server error"))
            )
            .bodyToMono(User.class)
            .block();
}

エラー時のフォールバック値を返したい場合は onErrorResume が便利です。

public User getUserWithFallback(Long id) {
    return webClient.get()
            .uri("/users/{id}", id)
            .retrieve()
            .bodyToMono(User.class)
            .onErrorResume(e -> Mono.just(new User("Unknown")))
            .block();
}

どちらを選ぶべきか

新規プロジェクトや大規模なリファクタリングのタイミングでは、将来性を考えてWebClientを選択するのがおすすめです。非同期処理や高スループットが求められる場合は特にWebClientの強みが活きます。

一方で、既存プロジェクトでRestTemplateが問題なく動作している場合は、無理に移行するよりも他の優先度の高い改善に時間を使う方が賢明なこともあります。シンプルな同期処理のみで十分な場合や、チームがリアクティブプログラミングに不慣れな場合は、RestTemplateでも実用上問題ありません。

RestTemplateはメンテナンスモードですが、すぐに使えなくなるわけではありません。要件とチームの状況に応じて適切に判断しましょう。

実務での設定のポイント

タイムアウト値は application.properties で外部化しておくと環境ごとに調整しやすくなります。

api.client.connect-timeout=3000
api.client.read-timeout=5000
api.client.base-url=https://api.example.com

これらの値を @Value で読み込んでBean設定に使用します。プロパティファイルの活用方法はSpring Bootのプロパティファイルで設定を管理する方法で詳しく解説しています。

複数の外部APIを呼び出す場合は、それぞれ専用のBeanを作成して @Qualifier で使い分けると管理しやすくなります。

エラーハンドリングについては、APIクライアント層で発生した例外をどうコントローラー層で扱うかも重要です。詳しくはSpring BootでREST APIの例外をいい感じにハンドリングする方法を参照してください。

まとめ

Spring BootでREST APIを呼び出す方法として、RestTemplateとWebClientの使い方を解説しました。

RestTemplateはシンプルで実績がありますが、新規開発ではWebClientが推奨されています。どちらを選ぶ場合でも、タイムアウト設定とエラーハンドリングは必ず実装しましょう。

実装したコードのテスト方法についてはSpring BootのテストをJUnitとMockitoで書く基本が参考になります。

外部APIとの連携は実務で頻繁に発生する要件なので、この記事の内容を押さえておけば自信を持って実装できるはずです。