マイクロサービス化に伴うAPIのGo移行をRails側で対応してみた

この記事は、2021年の[アトラエアドベントカレンダー](https://qiita.com/advent-calendar/2021/atrae)22日目の記事です。
昨日の記事は、@kinoshitatraeさんによる[「モードレスUIデザイン ~ ユーザーをクリエイティブにする」](https://note.com/pitagorasukippa/n/nfcb654923e1c)でした。
***
こんにちは！People Techカンパニー、アトラエに22卒として入社予定で、内定者インターンをしている家令（かれい）と申します。Wevoxでバックエンドエンジニアをやっています。

今回は内定者インターンを始めてから多くの時間を費やしてきた「Wevoxのマイクロサービス化に伴う、APIのGo移行」について紹介したいと思います。
## Wevoxのマイクロサービス化について
Wevoxのアーキテクチャは現在、モノリシックからマイクロサービスへと移行している真っ只中にあります。モノリシックの時代には、Ruby on Railsが多くの責務を担っていたのですが、以下の方針でマイクロサービス化を事業全体で推し進めています。

- FrontはRailsから分離し、SPA化を進める
- Railsの機能を切り出し、Goで小さなAPIにする
- 新機能はコア部分はGoで作り、認証等はRailsに任せる

並列処理を基本とするGoに置き換えることで処理の高速化を実現できる、Dockerとの相性が良くメモリの消費量を抑えることができるなど、Goへの移行のメリットは数多く存在するわけですが、Goへと移行されたAPIにRailsからリクエストを送り、Front側に値を返す部分をメインで実装していたので、その内容についてまとめてみたいと思います。

※Go側の実装については触れていないのでご注意を！

## Rails側の実装方針
Rails側の実装方針としては以下の手順で行なっていました。

1. app/apis/~ディレクトリを作成し、GoのAPIをコールする処理を書く
2. 対象のコントローラに、1.で作成した処理を追加し、Front側に値を返す 
3. 期待している値が返ってきているか入念な動作確認をし、デバッグをする

それぞれ見ていきましょう

## 1. app/apis/~ディレクトリを作成し、GoのAPIをコールする処理を書く
GoのAPIをコールする処理は大きく5つの層に分けて実装していきます。

1. Service層
2. Usecase層
3. Response層
4. Model層
5. Base層

この構成では、Service層⇄Usecase層⇄Response層⇄Model層で責務を分離し、オブジェクト指向に則り、処理の抽象化・隠蔽化をすることで、変更に強い設計になっています。Base層では、各層で必要になる共通の値を設定しています。

具体的なコードはそれぞれ以下の通りです

```ruby:Service層
class Hoge::GetFugaService
  private_class_method :new

  def self.call(id:)
    new(id: id).send(:call)
  end

  private

  def initialize(id:)
    @id = id
  end

  def call
    Hoge::Usecase::FugaGet.new(id: @id).execute.result
  end
end
```

```ruby:Usecase層
module Hoge
  module Usecase
    class FugaGet
      PATH = Hoge::Url::Fuga_GET
      attr_accessor :id

      def initialize(id:)
        self.id = id
      end

      def execute
        # 実コードではhostとportを指定してGoのAPIへリクエストを飛ばし、レスポンスを受け取っています
        response = get(PATH)

        case response
        when Net::HTTPOK then
          return Hoge::Response::FugaGet.new(
            Hoge::Response::Base::HTTP_OK, response
          )
        when Net::HTTPBadRequest then
          return Hoge::Response::FugaGet.new(
            Hoge::Response::Base::HTTP_BAD_REQUEST, response
          )
        when Net::HTTPInternalServerError
          return Hoge::Response::FugaGet.new(
            Hoge::Response::Base::HTTP_INTERNAL_SERVER_ERROR, response
          )
        else
          return Hoge::Response::FugaGet.new(
            Hoge::Response::Base::UNEXPECTED_ERROR, response
          )
        end
      end
    end
  end
end
```

```ruby:Response層
module Hoge
  module Response
    class FugaGet
      include Hoge::Response::Base

      attr_accessor :status, :error_hash, :success_hash

      def initialize(status, response)
        self.status = status
        if error?(status)
          self.error_hash = hash_json(response.body)
        elsif status == UNEXPECTED_ERROR
          self.error_hash = ERROR_HASH
        else
          self.success_hash = hash_json(response.body)
        end
      end

      def result
        return parse_fuga(success_hash) if status == HTTP_OK
        error_hash
      end

      private

      def parse_fuga(success_hash)
        Hoge::Model::Response::FugaGet.new(
          fuga: success_hash[:fuga]
        )
      end
    end
  end
end
```

```ruby:Model層
module Hoge
  module Model
    module Response
      class FugaGet
        attr_accessor :fuga

        def initialize(fuga:)
          @fuga = fuga
        end

      end
    end
  end
end
```

```ruby:Base層
module Hoge
  module Response
    module Base
      HTTP_OK = 200
      HTTP_CREATED = 201
      HTTP_NO_CONTENT = 204
      HTTP_BAD_REQUEST = 400
      HTTP_NOT_FOUND = 404
      HTTP_INTERNAL_SERVER_ERROR = 500
      UNEXPECTED_ERROR = 600
      UNEXPECTED_ERROR_CODE = "ERR-NOT-IMPLEMENTED"
      ERROR_HASH = { errors: [{ code: UNEXPECTED_ERROR_CODE }] }

      def hash_json(body)
        JSON.parse(body).deep_symbolize_keys
      end

      def error?(status)
        status == Hoge::Response::Base::HTTP_BAD_REQUEST ||
          status == Hoge::Response::Base::HTTP_INTERNAL_SERVER_ERROR
      end
    end
  end
end

```

1. Service層からUsecase層に必要な値を渡す
2. Usecase層で、指定されたエンドポイント（Hoge::Url::GETという定数に記載）にリクエストを飛ばし、GoのAPIから受け取ったレスポンスをステータスと一緒にResponse層に渡す
3. Response層で、GoのAPIから受け取ったレスポンスをFront側に渡す形に整えたModel層に合わせて、Usecase層に返す
4. Usecase層に返されたResponse層の値をService層で呼び出す

ここまでで一旦、GoのAPIをコールする処理の作成が完了したので、次に実際にコールする箇所を特定し、処理を追加していきます。

## 2. 対象のコントローラに、1.で作成した処理を追加し、Front側に値を返す

1.で用意したGoのAPIをコールする処理の内、Service層を使ってGoのAPIを叩きます。

``` ruby:該当のコントローラ
module Apis
  module Views
    class FugaController < ActionController::API

      def index  
        @fuga = Hoge::GetFugaService.call(id: 1)
      end

    end
  end
end
```

そして、GoのAPIから取得できた値をjbuilderを使ってFront側に返します

``` ruby:index.json.jbuilder
json.fuga @fuga
```

jbuilderについての参照資料はこちら→https://qiita.com/tanutanu/items/1dd1ea027dd142e84dc8

## 3. 期待している値が返ってきているか入念な動作確認をし、デバッグをする
最後に入念な動作確認を行います。
今回追加した処理で意図したエンドポイントに正しくリクエストが飛ばされているのか、open_apiに記載されている要件通りGoのAPIとの疎通ができているのか、Front側に期待している値を渡せているのか、正しくエラーハンドリングができているのかなどを一つ一つ確認し、不備があれば1~2の処理を修正します。

## 終わりに
長らくAPIのリプレイスを行なってきた中で、

- コミュニケーションを丁寧に行うこと
- 想定外を想定すること

がとても重要だと感じました。

前者に関しては、Front⇄Rails⇄Goとそれぞれ別の人が実装している中で、FrontとGoの橋渡しをするRails側の実装者が、いかに関係者の認識の齟齬をなくし、手戻りが発生しないよう前提を揃えながら進めていけるかがとても大事だと感じました。

後者に関しては、一つでも間違ったリプレイスをしてしまうと、回答結果が命のWevoxにおいては、間違った回答データをユーザーさんに渡してしまうことに繋がるため、品質を担保する上で意識しなければならないことだと思いました。

この二つは別の場面でも重要なことだと思うので、これからも肝に銘じて実装していきたいなと思います。

***
最後まで読んでいただき、ありがとうございました！
明日は@yuki_engさんの記事です！お楽しみに！