Hata's Blog

Moneytree LINK API の Go HTTP Client 実装を書いた

仕事で必要になったので、Moneytree LINK API の Go HTTP Client ライブラリ go-moneytree を実装した。

GitHub - sho-hata/go-moneytree: Moneytree LINK API Go clientMoneytree LINK API Go client. Contribute to sho-hata/go-moneytree development by creating an account on GitHub.
favicon of https://github.com/sho-hata/go-moneytreegithub.com
ogp of https://opengraph.githubassets.com/65d2ba230fb40ffe4bab351f2d8b2f856fd540b4d12528d6f143877895ec9d34/sho-hata/go-moneytree

スタンダードなAPI Client実装なので、特に目新しいところはないが、実装にあたって参考にした点や、少しユニークな実装ポイントについて書いてみる。適当に。

参考にしたライブラリ

sanitize処理などのHTTP Clientライブラリに求められるノウハウについては、google/go-github が参考になった。エラーハンドリングやリクエスト/レスポンスの処理、トークン管理など、HTTPクライアント実装のベストプラクティスが詰まっている。json パッケージの(*Encoder) SetEscapeHTMLとかは初知りだった。今までいかに他の人が作ったパッケージだけを使ってきたのかがわかる。

あとは、URLから機密情報(client_secretrefresh_tokenaccess_tokenなど)を除去するテクニックをはじめとした、HTTP Client としての丁寧なアプローチは参考になった。誤ってURLに access_token などをクエリパラメータで指定してしまったときに、エラーが発生した際にログに出力されるURLから機密情報が漏洩しないよう、クエリパラメータをREDACTEDに置き換える処理などを真似させてもらっている。

https://github.com/sho-hata/go-moneytree/blob/main/gomoneytree.go#L430

少しだけユニークな実装ポイント

指数関数的バックオフアルゴリズムによる再試行

Moneytree LINK APIのレート制限に対応するため、指数関数的バックオフアルゴリズムによる再試行を自前実装した。

実装では、RetryConfig構造体でリトライ設定を管理し、デフォルトで最大3回のリトライ、ベースディレイ3000msを設定している。待機時間の計算はこんな感じ。

待機時間 = ベースディレイ × 2^リトライ回数 ± ジッター

ジッターはランダムな値を加減することで、複数のクライアントが同時にリトライする際の衝突を避ける仕組みになっている。レート制限エラー(HTTP 429)が発生した際に、このアルゴリズムに基づいて段階的に待機時間を増やしながらリトライする。

実装はこの辺。

https://github.com/sho-hata/go-moneytree/blob/main/gomoneytree.go#L210

他の方が書いた指数関数的バックオフアルゴリズムのパッケージが存在していたが、比較的簡単に書けるアルゴリズムだったので、自前実装した。作ったパッケージの依存関係をゼロに抑えたかったこともある。

Functional Pattern によるオプショナルなクエリパラメータ設定

実験的に、Get系のAPIでオプショナルなクエリパラメータを設定する際は、functional pattern によるオプショナルな引数を設定するようにしてみた。

例えば、GetPersonalAccountsメソッドでは、WithPageWithPerPageといった関数を提供している。これらはGetPersonalAccountsOption型(func(*getPersonalAccountsOptions))を返し、可変長引数として渡すことで、必要なパラメータのみを柔軟に指定できる。

ype GetPersonalAccountsOption func(*getPersonalAccountsOptions)
 
type getPersonalAccountsOptions struct {
	paginationOptions
}
 
// WithPage specifies the page number for pagination.
// Page numbers start from 1. The default value is 1.
// Valid range is 1 to 100000.
func WithPage(page int) GetPersonalAccountsOption {
	return func(opts *getPersonalAccountsOptions) {
		opts.Page = &page
	}
}
 
// WithPerPage specifies the number of items per page.
// The default value is 500. Valid range is 1 to 500.
func WithPerPage(perPage int) GetPersonalAccountsOption {
	return func(opts *getPersonalAccountsOptions) {
		opts.PerPage = &perPage
	}
}

実装:https://github.com/sho-hata/go-moneytree/blob/main/personalaccount.go#L64

呼び出し側のコードはこんな感じ。

accounts, err := client.GetPersonalAccounts(ctx,
    moneytree.WithPage(1),
    moneytree.WithPerPage(100),
)

オプショナルなパラメータは構造体で表現することが一般的だが、functional options パターンを使うのも一つの選択肢だと感じた。 この方法のメリットは、呼び出し側のコードを見ただけで「どのパラメータを指定しているか」が直感的に分かる点にある。

一方でデメリットもある。どのオプションが指定可能なのかは、go-moneytree の実装(オプション関数の一覧)を見に行かないと把握できない。構造体であれば、フィールド定義を見るだけで利用可能なパラメータが一目で分かるため、この点では構造体の方が理解しやすい。

「可読性(呼び出し時の分かりやすさ)」と「発見性(何が指定できるかの分かりやすさ)」のトレードオフがあり、現時点では後者の観点から、やはり構造体ベースの方が無難だと感じている。このパッケージは近いうちに Organization 配下のリポジトリへ移す予定なので、そのタイミングで設計を見直し、構造体ベースに書き換えることを検討中。


ということで、Go で HTTP Client を実装してみた記事でした。

x(Twitter)にポスト
Profile
Shoki Hata

決済領域のソフトウェアエンジニア。週末はコーヒー豆を焙煎しています。