サーバサイドで複数Web APIを呼び出すときのデザインパターン

最近はエンタープライズのシステムでも、Web APIによるシステム間連携が増えてきました。そうしたときに、1リクエストで複数の連携先APIを実行し、結果をクライアントに返すということがままあります。

どう作りましょうか、という問題です。

前提として、サーバサイドでHTMLレンダリングせずに、Web APIの中継することとします。中継する意義は、流量やキャッシュをサーバサイドでコントロールできるところにあります。

クライアントから直接連携先のAPIにアクセスする設計にすると、リロードボタン連打などのDDoS攻撃うけたときに、自分たちでは対処できず、連携先に迷惑をかけてしまいますよね。特に「課金の関係などで直接APIをアクセスしなきゃいけないんだ」、とかでなければ、中継するように設計しておいた方がベターです。

## Web APIの呼び出し

業務システムで使う場合は、ちゃんとリクエスト、レスポンスがBeanにマッピングされてた方がよいでしょう。JAX-RS Clientでもよいのですが、API実行メソッドをインタフェースさえ作っておけば、よしなに実行してくれるRetrofitを使うことにします。

```xml
<dependency>
    <groupId>com.squareup.retrofit</groupId>
    <artifactId>retrofit</artifactId>
    <version>2.0.0-beta2</version>
</dependency>
```

API呼び出しServiceクラスは、こんな感じです。簡単ですね。

```java
public interface HatenaHotentryService {
    @GET("/hotentry?mode=rss")
    Call<BookmarkEntries> list();
}
```

## :snail: パターンA: ふつうに順々に呼び出す

![image](https://qiita-image-store.s3.amazonaws.com/0/8475/919b195d-efdb-38ec-9d87-fd1eec7e904a.png)

特に何の工夫もしないとこうなるでしょう。

```java
@WebServlet(urlPatterns = "/sequentialsync")
public class SequentialSyncServlet extends ApiBaseServlet {
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        try {
            OkMessage okMessage = new SlowApiService().get().get();
            BookmarkEntries bookmarkEntries = getBookmarkEntries();
            WeatherForecasts weatherForecasts = getWeatherForecasts();

            response.setContentType("application/json; charset=UTF-8");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(toJson(bookmarkEntries, weatherForecasts, okMessage));
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }

    }

    protected BookmarkEntries getBookmarkEntries() throws IOException {
        Response<BookmarkEntries> response = hatenaHotentryService.list().execute();
        if (response.isSuccess()) {
            return response.body();
        } else {
            throw new IOException(response.message());
        }
    }

    protected WeatherForecasts getWeatherForecasts() throws IOException {
        Response<WeatherForecasts> response = livedoorWeatherService.list("130010").execute();
        if (response.isSuccess()) {
            return response.body();
        } else {
            throw new IOException(response.message());
        }
    }
```

当然ながら、Web APIを1本ずつ実行するので、速度的なところは期待できません。

## :camel: パターンB: 並列実行して、結果をまとめてレスポンスを返す

![image](https://qiita-image-store.s3.amazonaws.com/0/8475/2904aa46-36d9-942b-a566-a6c33de0868f.png)

異なるサーバへのAPIは、できるだけ並列に実行したいものです。RxJavaを使って待ち合わせると簡単です。

```java
@WebServlet(urlPatterns = "/parallelsync")
public class ParallelSyncServlet extends ApiBaseServlet {
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("application/json; charset=UTF-8");
        response.setCharacterEncoding("UTF-8");

        List<Observable<? extends ResponseRoot>> responseObservables = new ArrayList<>();
        responseObservables.add(hatenaHotentryService.listRx());
        responseObservables.add(livedoorWeatherService.listRx("130010"));
        responseObservables.add(new SlowApiService().getRx());

        List<ResponseRoot> responseRoots = new ArrayList<>();

        Observable.merge(responseObservables).subscribe(responseRoot -> {
            responseRoots.add(responseRoot);
        });
        response.getWriter().write(toJson(responseRoots.toArray()));
    }
}
```

Retrofit2は、RxJavaのObservableを直接返せますので、これをサブスクライブして結果を収集し、JSON化して実行します。

パターンBの問題点は、遅いAPIが1つでもあるとそれに引きずられて、結局全体のレスポンスが遅くなってしまうことにあります。全体を待ち合わせる必要があるときは、適切にタイムアウトして最悪のケースでも一定の時間以内に、レスポンスが返るようにしましょう。

## :racehorse: パターンC: 並列かつ順次レスポンスを返す (Server Sent Event)

![image](https://qiita-image-store.s3.amazonaws.com/0/8475/368abafe-dafd-6857-778f-27e5d198865a.png)

全体の結果を待つ必要がないときは、Server Sent Eventを使うとWeb APIごとに結果をブラウザ側に順次返すことができます。Javaの場合、Servlet 3.0以上で使えます。が、未だどのIEでも[使えないようです](http://caniuse.com/#feat=eventsource)。
Servletのプロパティとして、asyncSupportedをtrueにし、非同期レスポンスが可能にします。`request.startAsync()
`で`AsyncContext`を取得し、API呼び出しのコールバックで、ここにレスポンスを書き込みます。

```java
@WebServlet(urlPatterns = "/sse", asyncSupported = true)
public class SSEServlet extends ApiBaseServlet {
    private static final Logger LOG = LoggerFactory.getLogger(ApiBaseServlet.class);

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException{
        response.setContentType("text/event-stream");
        response.setCharacterEncoding("UTF-8");

        final AsyncContext ac = request.startAsync();

        // ……… 中略 ……

        hatenaHotentryService.list().enqueue(new Callback<BookmarkEntries>() {
            @Override
            public void onResponse(Response<BookmarkEntries> response, Retrofit retrofit) {
                if (response.isSuccess()) {
                    try {
                        Writer writer = ac.getResponse().getWriter();
                        writer.write("data: " + toJson(response.body()) + "\n\n");
                        writer.flush();
                    } catch (IOException e) {
                        LOG.error("Hatena Bookmark access error", e);
                    }
                } else {
                    LOG.error("Hatena Bookmark access error", response.message());
                }
            }

            @Override
            public void onFailure(Throwable throwable) {
                LOG.error("Hatena Bookmark access error", throwable);
            }
        });

        // ……… 中略 ……
    }
}
```

クライアントサイドから呼ぶときは、EventSouceオブジェクト経由で呼びます。こんなイメージ。

```javascript
function start_sse() {
  var source = new EventSource('/sse');

  source.onmessage = function(event) {
    var data = event.data;
    $('#response').val($('#response').val() + event.data + "\n");
  };
}
```

これで、結果が戻ってきたAPIから順にレスポンスを取得できるようになります。

![image](https://qiita-image-store.s3.amazonaws.com/0/8475/bff7d7e1-da79-cf95-2e83-fb5a65584fe9.png)

パターンCは実装は、サーバサイド・クライアントサイドともに簡単なのですが、ブラウザの対応度合いが課題にはなるでしょう。

## まとめ

Exampleコードは、以下にあります。
https://github.com/kawasima/parallel-api-example

SSEは単純なプロトコルなので、スマフォアプリでも実装可能です。サポートするライブラリもあるようです。
「[Server Sent Events(SSE)の使いどころと使い方](http://labs.gree.jp/blog/2014/08/11070/)」を見ると、Androidブラウザでは辛みがあるようですが、サーバサイドの実装はServlet仕様でも簡単なので、UserAgentでフォールバックして、パターンB,Cを使い分けることもできそうです。

何れにせよ、パターンAはやめるようにしましょう。