ヘキサゴナルアーキテクチャでハイブリッドな GraphQL + REST API ゲートウェイを実現する

書きたいことを詰め込んだらタイトルが長くなってしまいました。
インフラ、フロントエンドと記事を書いてきたのでようやくバックエンドに触れます。
今回もサンプルを用意していますので最後までお付き合いください。

# 対象読者
- マイクロサービス化を見据えたモノリシックアプリケーションを検討されている方
- GraphQL サーバーと REST API サーバーのコードを共通化したい方
- 中規模以上のプロジェクト / プロダクトの技術選定を任された方
- Scala や ZIO、関数型プログラミングに興味がある方

# 注意事項
- サンプルはドメイン駆動設計(DDD)を厳密に実践していません
- サンプルではドメインロジックの関心の分離ではなくテクノロジの分離にフォーカスしています
- 本来 DTO や DPO で扱うべき部分を簡略化しているためレイヤー間のデータの受け渡しについては密結合を許容しています

# TL;DR
サンプルコードです。言語やビルド環境のセットアップには [anyenv](https://github.com/anyenv/anyenv) を使用しています。

https://github.com/aoyagi9936/hexagonal-architecture-example-with-zio

使用している技術は以下の通りです。
- Scala3
- ZIO2
- Caliban (GraphQL）
- Tapir (REST API）

Scala は LTS となった 3.3+ に対応しています。

# Why モノリス
今回のサンプルは **スケールを見据えたモノリシックなモダンアプリケーション開発** をイメージしています。
モダンアプリケーションのベストプラクティスとしては *[The Twelve-Factor App](https://12factor.net/ja/)* や *[Beyond The Twelve-Factor App](https://tanzu.vmware.com/content/blog/beyond-the-twelve-factor-app)* が有名ですが、各項目に適合するために始めからマイクロサービスを実践することは開発組織の習熟度や所属エンジニアのスキルなど中々ハードルも高く、まずはモノリシックなアプリケーションから始めたい...と感じられているエンジニアも多いのではないかと思います（それが私です）。
GitHub の元CTO のJason Warner氏も **開発初期はモノリスが最適** という主旨の発言を X(Twitter) でされています。

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">I&#39;m convinced that one of the biggest architectural mistakes of the past decade was going full microservice<br><br>On a spectrum of monolith to microservices, I suggest the following:<br><br>Monolith &gt; apps &gt; services &gt; microservices<br><br>So, some thoughts</p>&mdash; Jason Warner (@jasoncwarner) <a href="https://twitter.com/jasoncwarner/status/1592227285024636928?ref_src=twsrc%5Etfw">November 14, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

とはいえ、闇雲にモノリスなアプリケーションを作ってしまうと、たとえ依存関係に気を遣っていても気が付くと密結合なアプリケーションになってしまいます。
将来的なサービス分割に備えたモノリスなアプリケーションのアーキテクチャとしては *モジュラモノリス* などの手法がありますが、本記事では１つの例としてモジュールに分割しないモノリスアプリケーションとしてヘキサゴナルアーキテクチャを採用した実装サンプルをご紹介します。

# ヘキサゴナルアーキテクチャによる疎結合アプリケーション構成
ヘキサゴナルアーキテクチャはレイヤードアーキテクチャの一種です。
疎結合なコンポーネントにアプリケーションを分割するという性質上、異なるテクノロジを効率的に管理しつつサービス分割を見据えたアプリケーションの実装に適したデザインパターンであると言えます。
設計手法そのものについては本記事では詳しく説明しませんが、サンプルがどのようにヘキサゴナルアーキテクチャを用いて GraphQL と REST API を表現しているかを解説します。
全てを説明すると長くなり過ぎてしまうため記事ではポイントだけの解説にとどめ、基本的にはソースコードを読むようにしていただければと思います。

ユースケースとして、以下の２つの API をフロントエンドに公開することを考えます。
- 自社アプリケーションからコールされる GraphQL（認可あり）
- 外部ベンダー向けに自社サービスの一部機能を公開する REST API（認可なし）

下の図はサンプルアプリケーションの全体構想を表現したものです。
![hexagonal_architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2767381/21960cc9-7fc6-702f-f7ca-b974df795d90.png)
ヘキサゴナルアーキテクチャではアプリケーションは大きく３つの層で構成され、クライアントからのリクエストを処理するインバウンド(Primary)、設定やビジネスロジックを記述するアプリケーション(Application)、データベースや外部サービスに向けてリクエストを発行するアウトバウンド(Secondary) に分けられます[^1]。
[^1]:Inbound/Outbound は Driver/Driven とも呼ばれます

サンプルではそれぞれの API はキャラクターのデータを操作するビジネスロジックである `CharactersService` を参照し、 `CharactersService` はデータストアに接続する `CharactersRepository` を介してキャラクターの永続データを操作することとします。
関係性を分かりやすくフローで表現すると以下のようになります。
![hexagonal_architecture_flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2767381/23081581-b89b-d661-e361-8f589ae6e64d.png)
フローのそれぞれの箱をインターフェースで抽象化し依存性を逆転させることで、ハードコーディングしたモック(Mock)や本番用コードである実装(Live)に切り替えられるようにしています。
このようにアプリケーションを疎結合なコンポーネントに分割することで、テスト容易性を確保し将来的なサービス分割（マイクロサービス化）に備えることができます。

# Caliban と Tapir によるエンドポイント実装
ヘキサゴナルアーキテクチャでは外部と接続するコンポーネントを *アダプター* と表現します。
GraphQL と REST API のアダプターはフォルダの以下で定義しています。
```
.
└── adapters/
    └── primary/
        ├── graphql/
        │   ├── apis
        │   ├── schemas
        │   └── GraphqlResolver.scala
        └── rest/
            ├── apis
            ├── endopints
            └── RestResolver.scala
```
サンプルでは GraphQL の実装には [Caliban](https://ghostdogpr.github.io/caliban/) 、 REST API の実装には [Tapir](https://tapir.softwaremill.com/en/latest/index.html) ライブラリをそれぞれ使用しています。
どちらも Scala のコードベースでスキーマや入出力パラメータを記述することができ、定義と実装を分離することができます（ちなみに Caliban の内部では Tapir が用いられています）。
また、後述する ZIO をフル活用するため `tapir-zio` モジュールも使用しています。

以下のコードは両者の定義部分の記述です。
`import` 文が `ports` パッケージを指定している通り、定義はあくまで実装に依存しないインターフェースを参照しています。

```scala:adapters/primary/graphql/schemas/CharactersSchema.scala
import com.example.ports.primary.CharactersApi

object CharactersSchema {
...
  val api =
    graphQL(
      RootResolver(
        Queries(
          args => CharactersApi.getCharacters(args.origin),
          args => CharactersApi.findCharacter(args.name)
        ),
        Mutations(args => CharactersApi.deleteCharacter(args.name)),
        Subscriptions(CharactersApi.deletedEvents)
      )
    )
...
}
```

```scala:adapters/primary/rest/endpoints/CharactersPublicEndpoint.scala
import com.example.ports.primary.CharactersPublicApi

object CharactersPublicEndpoint {
...
  val charactersLogic = charactersEndpoint.zServerLogic {
    origin => CharactersPublicApi.getCharacters(origin)
  }
...
}
```
２つの API の実装 `xxxApiLive.scala` はどちらも `CharactersService` に依存していますが、インターフェースを参照しているだけですので実体である依存性を注入する仕組みが必要となります。
そこで登場するのが Scala の ZIO ライブラリです。

# 複雑な依存関係とエラーハンドリングを ZIO で解決する
ZIO はエラーハンドリング、リソース管理、非同期および並行処理をサポートする Scala の関数型プログラミングライブラリです。

https://zio.dev/

ZIO に関する詳細な説明は本記事では割愛しますが、ここではパワフルな DI エンジンとしての機能とヘキサゴナルアーキテクチャとの相性の良さについてご紹介します。

まず、先ほどの API の実装について見てみます。
`CharactersPublicApiLive.scala` では **ZLayer[-RIn(Requirements), +E(Error), +ROut(Value)]** という宣言で自身の依存関係を表現しています。
```scala:adapters/primary/rest/apis/CharactersPublicApiLive.scala
object CharactersPublicApiLive {

  val layer: ZLayer[CharactersService, PrimaryError, CharactersPublicApi] = ...

}
```
上記ソースコードは
**「`CharactersPublicApiLive` は `CharactersService` を用いて `CharactersPublicApi` を実装する。発生するエラーは `PrimaryError` である。」**
という宣言をしていることとなり、実装するコードに制限を加えながら依存性を定義することができます。

フローをもう一度思い出してみましょう。
下図ではレイヤーとの対応を分かりやすくし発生するエラータイプについても補足しました。
![hexagonal_architecture_flow2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2767381/eeacc929-1afb-c88b-9cec-cf5c2ac1144e.png)
あらためて `CharactersPublicApiLive` を見てみましょう。
```scala:adapters/primary/graphql/apis/CharactersApiLive.scala
object CharactersPublicApiLive {

  val layer: ZLayer[CharactersService, PrimaryError, CharactersPublicApi] = ZLayer {
    for {
      svc   <- ZIO.service[CharactersService]
    } yield new CharactersPublicApi {
      def getCharacters(origin: Option[Origin]): IO[PrimaryError, List[Character]] =
        svc.getCharacters(origin)
          .mapError(_ => InternalServerError)
      def findCharacter(name: String): IO[PrimaryError, Option[Character]] =
        svc.findCharacter(name)
          .mapError(_ => InternalServerError)
    }
  }

}
```
```scala:application/constants/Errors.scala
// Primary Layer
sealed trait PrimaryError(errorCode: String, message: String) extends Throwable {
  def code = errorCode
}
//401 Unauthorized
case object UnAuthorizedError extends PrimaryError("UNAUTHORIZED_ERROR", "You are not authenticated. You need to create an account or accept an invitation.")
//403 Forbidden
case object ForbiddenError extends PrimaryError("FORBIDDEN_ERROR", "Permission denied for this resource. Add the roles you need to access the resource.")
//500 Internal Server Error
case object InternalServerError extends PrimaryError("INTERNAL_SERVER_ERROR", "An Internal Server Error has occurred. Please contact support.")
```

`CharactersPublicApiLive` では `ZIO.service[CharactersService]` を用いて `CharactersPublicApi` のインスタンスを返しているので **先ほどの ZLayer の宣言と一致します。**

**ポイント**
- `for {...} yield ...` は [For Comprehensions](https://docs.scala-lang.org/tour/for-comprehensions.html) という Scala の特徴的な表記法で、関数型プログラミングのアプローチで複数の生成結果を評価して１つの結果として返すことができます
- `ZIO.service[T]` で依存するサービスの実体を取得することができます [^2] [^3]

[^2]:https://zio.dev/reference/service-pattern/
[^3]:https://zio.dev/reference/di/dependency-injection-in-zio/#dependency-injection-when-writing-services


**上記のコードから `ZIO.service[CharactersService]` を削除したり別の `ZIO.service[T]` を追加すると ZLayer の宣言と異なるためコンパイルエラーとなります。**
また、ビジネスロジックで発生するエラーは `DomainError` としていますが、プライマリアダプターではフロントエンドに返すエラータイプ `PrimaryError` に変換するように制約をかけています。
ZIO ではエラーの変換に `.mapError` メソッドを利用しますが、 **宣言通りに `PrimaryError` に変換しないとこちらもコンパイルエラーとなります。**
これにより、各レイヤー（コンポーネント）の依存関係や発生するエラーに制約を設けて適切に制御することができます。

依存性を注入している記述は以下のコードとなります。
```scala:application/core/AppContext.scala
object AppContext {
...
  def restLayer: ZLayer[Any, Throwable, RestApp] = ZLayer.make[RestApp](
    // Inbound
    CharactersPublicApiLive.layer,

    // Application
    restServerConfig,
    CharactersService.layer,

    // Outbound
    CharactersRepositoryMock.layer
  )
...
}
```
ZLayer で **「CharactersPublicApiLive は CharactersService を利用する」** と宣言しているので、もし上記コードから `CharactersService.layer` を削除するとこちらもコンパイルエラーとなります。
また、`CharactersRepositoryMock` を `CharactersRepositoryLive` に切り替えてデータストアに接続する本番用コードに切り替えることもできます。

**余談**
JVM 上で動作する最もメジャーな Web アプリケーションフレームワークである Spring では DI コンテナを制御するオブジェクトは `ApplicationContext` なので、サンプルでは Spring ユーザーにも馴染み易いように `AppContext` と命名しています。
ZIO における `ZIO.service[T]` は Spring における `@Autowired` のような感覚で扱いますが、 エラーハンドリングやコンポーネントの階層的な依存関係の制御にまで踏み込んでいる ZIO はよりレイヤードアーキテクチャに適したライブラリと言えるでしょう（個人の感想）。
`ZLayer.Debug.tree` や `ZLayer.Debug.mermaid` を利用することで依存関係を綺麗なツリー構造で表示できる[^4]ので DI コンテナ同士の依存関係に悩んでいる方にも ZIO はお勧めです（ZIO1では [zio-magic](https://github.com/kitlangton/zio-magic) というモジュールでしたが ZIO2 で組み込まれました）。
`ZLayer.Debug.mermaid` を使用して自動生成された Mermaid Live Editor のリンクは以下の通りです。
[GraphQL Mermaid Graph](https://mermaid-js.github.io/mermaid-live-editor/edit/#eyJjb2RlIjoiZ3JhcGggQlRcbkwwKFwiQXV0aG9yaXphdGlvbkZpbHRlci5sYXllclwiKSAtLT4gTDEoXCJDaGFyYWN0ZXJzQXBpTGl2ZS5sYXllclwiKVxuTDIoXCJDaGFyYWN0ZXJzU2VydmljZS5sYXllclwiKSAtLT4gTDFcbkwwXG5MMyhcIkNoYXJhY3RlcnNSZXBvc2l0b3J5TW9jay5sYXllclwiKVxuTDQoXCJncmFwaHFsU2VydmVyQ29uZmlnXCIpXG5MMyAtLT4gTDIiLCJtZXJtYWlkIjogIntcInRoZW1lXCI6IFwiZGVmYXVsdFwifSJ9)
[RestAPI Mermaid Graph](https://mermaid-js.github.io/mermaid-live-editor/edit/#pako:eNpVz00LwjAMBuC_UnJS2MSP2w6CX7cJoh57iW3mils7slYYw_9uRUV3C3kfwpselNMEGVwZm1Ksz9Lm05EEptafiO_EG2cLc5UwjsksJpsSGZUnbo_UuNZ4x93eqdukwo7440SaLkU-H_DXOaPo383fbjFwh3CpjFo1Jjf3n4YEauIajY5le2mFkOBLqklCFkdNBYbKS5D2EWloNHra6Vc7yAqsWkoAg3enzirIPAf6oq3B-Hv9Xj6eMOxdDw)

[^4]:https://zio.dev/reference/di/automatic-layer-construction/#zlayer-debugging

# 並列プロセスでハイブリッドに API を公開する
Scala の HTTP サーバー / クライアントライブラリである `http4s` を使用してそれぞれの API を公開します。現在のサンプルでは実行時の引数で何も処理していませんが、例えば引数によって片方のサーバーだけを起動する、といった制御も可能です。
`AppContext.gqlLayer` と `AppContext.restLayer` でそれぞれのサーバーにレイヤーを `Provide` し `zipPar` で並列に起動します。
```scala:Main.scala
object Main extends ZIOAppDefault {  
...
  val graphql = (args: Chunk[String]) =>
    ZIO
      .runtime[AppContext.GqlApp]
      .flatMap(implicit runtime =>
        for {
          config      <- ZIO.service[ServerConfig]
          interpreter <- GraphqlResolver.api.interpreter
          _           <- EmberServerBuilder
          .default[GqlAuthzTask]
          .withHost(Host.fromString(config.host).getOrElse(host"localhost"))
          .withPort(Port.fromInt(config.port).getOrElse(port"8088"))
          .withHttpWebSocketApp(wsBuilder =>
            Router[GqlAuthzTask](
              "/api/graphql" -> 
                CORS.policy(
                  AuthzMiddleware(
                    Http4sAdapter.makeHttpService(
                      HttpInterpreter(
                        withErrorCodeExtensions[AppContext.GqlApp](interpreter)
                      )
                    )
                  )
                ),
              "/ws/graphql" ->
                CORS.policy(
                  AuthzMiddleware(
                    Http4sAdapter.makeWebSocketService(wsBuilder,
                      WebSocketInterpreter(
                        withErrorCodeExtensions[AppContext.GqlApp](interpreter)
                      )
                    )
                  )
                ),
              "/graphiql" -> Kleisli.liftF(StaticFile.fromResource("/graphiql.html", None))
            ).orNotFound
          )
          .build
          .toScopedZIO *> ZIO.never
        } yield ()
      )
      .provideSomeLayer[Scope](AppContext.gqlLayer)

  val rest = (args: Chunk[String]) =>
    ZIO
      .runtime[AppContext.RestApp]
      .flatMap(implicit runtime =>
        for {
          config      <- ZIO.service[ServerConfig]
          _           <- EmberServerBuilder
          .default[RIO[RestResolver.Apis, *]]
          .withHost(Host.fromString(config.host).getOrElse(host"localhost"))
          .withPort(Port.fromInt(config.port).getOrElse(port"9000"))
          .withHttpApp(
            Router("/" -> RestResolver.routes).orNotFound
          )
          .build
          .toScopedZIO *> ZIO.never
        } yield ()
      )
      .provideSomeLayer[Scope](AppContext.restLayer)

  override def run =
    (for {
      args <- getArgs
      _    <- graphql(args) zipPar rest(args)
    } yield()).exitCode

}
```

それでは実行してみましょう。

```shell
sbt run
```

`localhost:8088/graphiql` にアクセスすると GraphQL の IDE である `GraphiQL` を表示できます。
クエリを実行するとキャラクターの一覧を取得できます。
![graphiql.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2767381/a4a38017-7e58-e0fe-5898-223c2222dba8.png)
`localhost:9000/docs` にアクセスすると `Swagger UI` を表示できます。
`Try it out` を実行することでこちらもキャラクターの一覧を取得できます。
![swagger-ui.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2767381/28c18f72-789a-2de1-30c3-e6547570f4ee.png)

# まとめ
かなり説明を端折ってしまいましたが、ヘキサゴナルアーキテクチャでハイブリットなゲートウェイを構築するサンプルをご紹介しました。
ゲートウェイと呼んではいますが、サンプル自体はモノリシックに全ての機能を詰め込んだサーバーアプリケーションです。
アプリケーションが成長しサービスの分割が見えてきた段階で `Application` 内の `Services` をマイクロサービス化し、セカンダリアダプターにマイクロサービスのクライアントを実装することで BFF (Backend For Frontends) のようなゲートウェイとして機能させることが可能です。

最後に、ZIO は タイプセーフ を超えて レイヤーセーフ[^5] にアプリケーションを構築できる非常に素晴らしいライブラリなので、情報が少なく学習に苦労するかもしれませんが是非興味を持っていただければと思います。
公式ドキュメントは更新が遅かったりするので、学習には以下のリポジトリを活用するのが良いと思います。

本記事は以上です。

https://github.com/zio/zio-quickstarts

https://github.com/softwaremill/realworld-tapir-zio

[^5]:記事を書いていて思いついた造語でありそのような用語は（たぶん）ありません