UPSIDER Tech Blog

KotlinコルーチンでDBアクセスするときのアンチパターン

こんちには!

UPSIDERのWebチームでサーバサイドKotlinを書いているエンジニアのおかだです。

突然ですが、Kotlinコルーチン使ってますか? もちろん使ってますよね。

KotlinでWebアプリケーションを書くのであれば、リクエストごとにコルーチンを起動してハンドリングするでしょうし、その処理の中でDBにアクセスすることも多いだろうと思います。ですが、このありがちな処理を、ごく普通に書くだけで、わかりにくいバグを生む場合がある、そんな話を紹介します。

やってはならないことは単純。


アンチパターン
DBトランザクション内でsuspendする処理を、大量(DBコネクション総数を超える数)のコルーチンで並行実行してはならない


やってしまうとしたらバッチ処理ですかね。これをやってしまうと、コルーチンはコネクションを使い果たし、マイルドデッドロックとでも言うべき状態に陥ります。特に気をつけてほしいのは、これはスレッド数とはほぼ無関係だということです。スレッド数がコネクション数よりも少ないから大丈夫、とはならないのです*1

どのような機制でこの問題が起きるのか。図で見ていきましょう。

DBコネクションが8つ、スレッドが3つ、コルーチンが20個、という状況を考えてみます。車がスレッド、円柱の下に生えてる触手みたいなやつがコネクションです。スレッドが車のようなものだとは全然思ってないですけどね。コルーチンとの関係性で言えば、コルーチンが乗ってる感あるかなと・・・。図にするのって難しい。

まず、スレッドの数と同じ3つのコルーチンが動き出します。トランザクションを開始してコネクションをプールから借りてきます。

さてここで、アンチパターンに記載したとおり、この処理はトランザクションの途中でサスペンドします。たとえば、DBに何か保存したあとにサスペンド関数でメール送信する、とか。

ここで重要なのは、トランザクションを維持するためには、コネクションはそれぞれのコルーチンが持ち続けなければならないということです。UPSIDERではORMとしてExposedを使っていますが、Exposedはこのように振る舞います。仮にExposedでなくとも、ロジカルに考えて、こうする以外の選択肢ってあまり思いつきません。

最初の3つのコルーチンがサスペンドしたことで、スレッドが空きました。というわけで、実行されるのをワクワクと待ち構えている次のコルーチンがスレッドを割り当てられて、処理を始めます。またコネクションが3つ消費されます。

そしてこのコルーチンたちもトランザクション途中でサスペンドします。

問題は次です。コルーチンがサスペンドして行った処理は(この3台の車とは別のスレッド上で行ったことにしておきますが)、それほど時間のかかる処理ではなく、すでに完了しているとします。つまり、左上で -_- みたいな顔をしている連中の一部は、スレッドが空くのを待っているのです。

ですが、彼らはスレッドを割り当ててもらえません。最初から待ち続けている左下のコルーチンが優先されます。このコルーチンのスケジューリングアルゴリズムCoroutineDispatcher に依存するので、変更できる可能性がないわけではないですが、これをうまくコントロールする CoroutineDispatcher の実装を試みるのはおそらく無理筋です。

最終的には、コネクションはすべて、サスペンド中のコルーチンに持っていかれ、スレッド上にはコネクションを待つコルーチンが居座り続けることになります。

空くはずのないコネクションをどのくらいの間待ち続けるのかはコネクションプールの設定によりますが、仮に30秒だとすると、まだコネクションを得ていない12個のコルーチンがすべてコネクション取得を諦めてタイムアウトするまでに 30秒 * (12コルーチン / 3スレッド) = 120秒 かかります。そのあとようやく左上のコルーチンがスレッドを割り当てられ処理を再開、コネクションを解放していきます。

解決策

アンチパターンを構成する、トランザクションサスペンドか、大量のコルーチン起動、どちらかを回避しましょう。

1. トランザクション内でサスペンドするのをやめる

Kotlinコルーチンのアドバンテージをすこし犠牲にしてしまうかもしれませんが。条件次第では大した代償なく対応できるかもしれません。チーム開発で、この内部処理ではサスペンド禁止、というのを周知し守り続けるのはなかなか難しいものがありそう。

2. 一度に起動するコルーチン数を制限する

先にスレッド待ちしているコルーチンがいなければ、サスペンドしているコルーチンがすぐにスレッドを獲得して処理再開し、コネクションを解放できます。コルーチン数を制限する方法については「Kotlinでコルーチンの並行処理数を制限する」という記事で触れていますので、そちらも合わせて読んでいただければと思います。


career.up-sider.com

herp.careers

*1:スレッド数が逆にコルーチン数を上回るほど多い場合はこの問題は起きないのですが、そういうシチュエーションは普通なさそう

アカウント開設フォームのUI設計における大失敗と爆速改善ストーリー

こんにちは。Webチームでエンジニアとして働いている久保です。

今回は、アカウント開設フォームのUI設計における失敗と爆速改善ストーリーについて紹介していきます。


アカウント開設フォームの通過率が劇的に下がってしまった

UPSIDERをご利用頂く前に、独自の審査を行うために、企業ごとにアカウントを開設していただく過程があります。しかし、とあるリリース以降、アカウント開設のプロセスを通過する割合が極端に減少してしまった事がありました。これは弊社にとって、新規のユーザーが増えず、サービス全体の信頼感も落としかねない大きな問題となりました。

eKYCを完全に終えられていない・・・?

原因を調べたところ、どうも「eKYCを最後まで終えられていない」ということがカスタマーサポートからのヒアリングで分かりました。

具体的な数字を出すと、eKYCの審査通過率が30%ほどと正常な審査通過率(70%)の半分以下になってしまっており、そのせいでカスタマーサポートのスタッフのにも大量に問い合わせが来てしまい、リソースを逼迫する状態にもなっていました。

その原因の一つとして、eKYCフォームに外部ヘルプページに飛ぶリンクを複数設置していて、それらを閲覧しないと理解ができない入力フォームになっていたことが挙げられます。

「eKYC」は法律の概念が関わることから、現在のユーザーの状態を分かりやすく表示したり、これから何をする必要があるのかを適切に表示することが難しい部分です。そのため、さまざまな箇所でヘルプページを利用して解決しようとしていたことが原因の根底にあると、フロントエンドチームの分析により分かりました。

ヘルプページへ遷移せずともアカウント開設できるUIを目指す!!

フロントエンドチームでは、これらの問題を解決するために「ヘルプページへ遷移せずともアカウント開設できるUI」を最低の条件に置くとともに、改めて下記を共通認識とし、UIを刷新しました。

  • 適切なタイミングで適切な情報を提示する
  • 必要のない情報は出さないようにする
  • 直感的に理解できるUIを目指す

また、アカウント開設の入力フォームから開設が完了するまでのフローをFigmaに張り出し、他にボトルネックになってる部分がないかも調査しました。

下記画像は、改善に取り組んでいた際のFigmaです。このページ以外にも4,5ページFigmaが存在し、ボトルネックの洗い出しにプロジェクトメンバー全体で取り組んでいました。

正常な審査通過率に戻り、ボトルネックを解消!

結果的にUI刷新直後から、eKYC導入後の正常な審査通過率と言われる70%前後に戻り、カスタマーサポートへのお問合せも激減しました。

この経験があってからUIを設計する際に「本当にヘルプページがないとダメなのか?」と言う意識がチーム内で生まれ、あるべきUIを目指す姿勢ができたように思います。

まとめ

「アカウント開設フォームのUI設計における大失敗と爆速改善ストーリー」を紹介させていただきました。

最後までお読みいただきありがとうございました。

限られた時間の中で、どのようなUIを提供すべきなのかの判断は難しいところはありますが、それでもあるべきUIを考え、全力で開発・提供するというのはUPSIDERならではなのかなと思っています。

実は一方でリソース的な要因として、当時フロントエンドエンジニア、UI/UXデザイナー、PdMが不足していたことがあります。次の宣伝に続きますが、UPSIDERでは全方位採用を行っております。ぜひUPSIDERにお力をお貸しください。

宣伝

UPSIDERでは、挑戦者を応援する法人カード「UPSIDER」と、すべてのBtoB取引でクレジットカードを利用できるビジネスあと払いサービス「支払い.com」を提供しております。

UI/UXを向上し、ユーザーが使いやすいプロダクトを開発するために全力を注いでくれるエンジニアを募集しています。

career.up-sider.com

herp.careers

Kotlinでコルーチンの並行処理数を制限する

こんにちは!

UPSIDERのWebチームでサーバサイドKotlinを書いているエンジニアのおかだです。

突然ですが、コルーチンの並行処理数、制限したくなったことありませんか? 

ぼくはよくあります。

数千数万という数のコルーチンを同時に起動することもできる、と軽量さが強調されることもありますが、それはメモリ消費が少ないとかそういう話であって、コルーチンによって呼び出される側がその同時大量アクセスをさばけるかは別問題です。たとえば次のような制約が問題になることが時々あります。

特に、DBコネクションの取り扱いについてはKotlinコルーチンならではの落とし穴があったりするので、また別の記事で取り上げたいと思います。

追記:書きました → KotlinコルーチンでDBアクセスするときのアンチパターン - UPSIDER Techblog


まずはシンプルに実装してみる

さて、コルーチン数制限、雑でよければいろいろ書きようはありますが、結構あちこちで必要になってくるのでライブラリ化しておきたいところ。とりあえず、なるべくシンプルに作ってみましょう。

class LimitedCoroutineScope(
    private val underlying: CoroutineScope,
    limit: Int
) : CoroutineScope by underlying {

    private val semaphore: Semaphore = Semaphore(limit)

    private fun <T> withPermit(block: suspend CoroutineScope.() -> T): suspend CoroutineScope.() -> T =
        {
            semaphore.withPermit {
                block()
            }
        }

    fun launch(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> Unit
    ): Job = underlying.launch(context, start, withPermit(block))

    fun <T> async(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> T
    ): Deferred<T> = underlying.async(context, start, withPermit(block))
}

suspend fun <T> limitedCoroutineScope(
    limit: Int,
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend LimitedCoroutineScope.() -> T
): T =
    withContext(context) {
        LimitedCoroutineScope(this, limit).block()
    }

使い方はこう。こちらもシンプルです。

fun doSomething() = limitedCoroutineScope(20, context = Dispatchers.IO) {
  val users = userRepository.getAll().map { user ->
    async { findExtraInfoOf(user) }
  }.awaitAll()
  // ...
}

説明の必要はあまりないと思いますが、一点だけ。3行目の async は、 CoroutineScope.async ではなく、 LimitedCoroutineScope クラスの async メソッドです。これは、ある型に対して同じシグネチャの拡張関数とメンバ関数が定義されている場合、メンバ関数が優先される、というKotlinの仕様に依拠しています。ちょっと紛らわしいかもしれない。

CoroutineContextを使った実用的な実装

実際のプロジェクトでコルーチンまわりを共通化しようとするとき、上記のように新しい型を作ってそこに機能をもたせるやり方だと拡張が難しくなります。 CoroutineScope ってネストさせることもよくあり、たとえば次のように書くだけで、コルーチン数制限はできなくなります。

fun doSomething() = limitedCoroutineScope(20, context = Dispatchers.IO) {
  withTimeout(60.seconds) {
    // thisはLimitedCoroutineScopeではないので、コルーチン数制限はできない
  }
}

そもそも解決の仕方がKotlinコルーチンの設計思想とマッチしてないんでしょうね、たぶん。同時処理数をセマフォで制限する、という点は問題なさそうなので、そこは踏襲しつつ、もっとKotlinコルーチンらしい設計に変えてみましょう。こういうときに使えそうなのが CoroutineContext です。

たとえば、UPSIDERでも使っているORMのExposed。コルーチンがサスペンドしてスレッドを失ってもDBトランザクションを保つためには、スレッドローカルではなくコルーチン側にトランザクションを紐付けておく必要があります。Exposedは CoroutineContext 内にトランザクションを保持することでそれを実現しています。トランザクションの途中で新しい CoroutineScope を起ち上げた場合も、 CoroutineContext なら親から子へ引き継がれるので、その処理は同じトランザクションの一部となります。

まず、セマフォをラップして、 CoroutineContext の要素( CoroutineContext.Element 型)として扱えるようにします。要素と言いましたが、 CoroutineContext は構成要素ひとつひとつが CoroutineContext でもあります。Compositeパターンですね。

internal data class CoroutineLimit(
    val limit: Int
) : AbstractCoroutineContextElement(CoroutineLimit) {

    internal val semaphore: Semaphore by lazy {
        Semaphore(limit)
    }

    companion object Key : CoroutineContext.Key<CoroutineLimit>

    override fun toString(): String = "CoroutineLimit($limit)"
}

続いて、 CoroutineLimitCoroutineContext に付け足してくれる CoroutineScope ビルダを作ります。 limit が0以下のときはコルーチン数制限はされない、ということにしておきます。

suspend fun <T> limitedCoroutineScope(
    limit: Int,
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend CoroutineScope.() -> T
): T {
    val newContext = coroutineContext + context +
        (if (limit > 0) CoroutineLimit(limit) else EmptyCoroutineContext)
    return withContext(newContext, block)
}

最後に、 CoroutineLimit が見つかったらコルーチン数を制限するという機能付きのコルーチンビルダを、 CoroutineScope の拡張関数として用意します。最初の例と違ってわかりやすい名前にしてみました。

fun CoroutineScope.launchWithLimitedConcurrency(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = coroutineContext + context
    return this.launch(context, start, newContext.createLimitedBlock(block))
}

fun <T> CoroutineScope.asyncWithLimitedConcurrency(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = coroutineContext + context
    return this.async(context, start, newContext.createLimitedBlock(block))
}

private fun <T> CoroutineContext.createLimitedBlock(block: suspend CoroutineScope.() -> T): suspend CoroutineScope.() -> T =
    this[CoroutineLimit]?.let {
        {
            it.semaphore.withPermit {
                this.block()
            }
        }
    } ?: block

使い方は、 launchasync の関数名が異なるだけで、基本的に最初の例と同じです。この実装なら、 CoroutineScope入れ子にしても正しく動いてくれます。

fun doSomething() = limitedCoroutineScope(20, context = Dispatchers.IO) {
  val users = userRepository.getAll().map { user ->
    asyncWithLimitedConcurrency { findExtraInfoOf(user) }
  }.awaitAll()
  // ...
}

fun doSomething() = limitedCoroutineScope(20, context = Dispatchers.IO) {
  withTimeout(60.seconds) {
    repeat(100) {
      // 内側のCoroutineScopeにCoroutineLimitが引き継がれているので、
      // 並行処理数は期待通り20個までに制限される
      launchWithLimitedConcurrency { /* do something */ }
    }
  }
}

CoroutineLimit を共有しさえすれば、スコープが入れ子になっていなくても同じコルーチン数制限が適用されてしまう点は、役に立つ場合もあるかもしれない反面、自由度が高すぎてシステムの挙動が読めなくなる可能性がありそうです。 CoroutineLimitinternal としているのはそのためです。


宣伝

UPSIDERでは、成長企業のための法人カード「UPSIDER」と、すべてのBtoB取引でクレジットカードを利用できるビジネスあと払いサービス「支払い.com」を提供しています。

ぼくが所属するWebチームはもちろん、UPSIDERの開発組織のいたるところでエンジニアが足りません。いろんな新しい技術にもこれから挑戦していきます。

カジュアル面談もやっておりますので、少しでもご興味のある方は、ぜひご連絡ください!

career.up-sider.com

herp.careers

決済チームの「当たり前の品質」を支えているもの

こんにちは。決済チームでエンジニアとして働いている小須田です。

今回は、多様なメンバーが増えていく決済チームで、「当たり前の品質を保ち続ける」ための取り組みをご紹介します。

TL;DR

  • Issueの形式を定め、紐づくPull Requestの粒度をモジュール単位で確定させる
  • Pull Requestの形式を定め、マージの影響を明確にする
  • コードの満たす品質をCIで担保する

前置き

UPSIDERはエンジニアの採用に力を入れており、決済チームにも多様なバックグラウンドを持つ優秀なメンバーが次々と参加してくれています。それは、新たな強みが多く手に入る環境である一方で、一定以上の品質を保つこととの戦いの場でもあります。

その環境下で私たちが品質を保つ、つまり「当然なされるべきことが、確実になされる」ためには、仕組み化・自動化が必要になってきます。だれがやっても小さい労力で、必須部分が担保される状態が理想ということですね。そこが省力できればできるほど、より本質的な課題に注力する余裕も生まれてくるというものです。

今回ご紹介する内容は、上記の仕組み化・自動化に関するもののうち、タスク・コードの質を担保するためのGitHub上での取り組みに焦点を当てたものです。

逆に今回は、下記の内容には言及しません。

  • Issueに落とす前の、要件定義と基本的な設計
  • ディレクトリ構成など、ソースコードに関する具体的な内容
  • Pull Requestがマージされた後の、デプロイなどの内容

※また、本記事は決済チームのメインである、Go言語によるマイクロサービスを前提とした内容となっています。


Issueの形式を定め、紐づくPull Requestの粒度をモジュール単位で確定させる

IssueもPull Requestも、ドキュメント作成業務には共通することですが、一定以上の質を保ちつつ省力したいなら、定型化してしまうのが効果的です。

定型化で重要なのは、どこになにをどの粒度で書けば良いかが決めることです。そうすることによって、書く側も読む側も迷うことがなくなり、また新しいメンバーの記載漏れもグッと減ります。

UPSIDERでは、チームごとにIssue Templateを作成しており、決済チームでは機能開発時のIssueに下記の内容を記載することにしています。

  • Goals
    • 目的。Issueのタイトルと密接に関連するものとする
    • このセクションにはあまり多くの詳細を記入せず、端的に書く
  • Background
    • 背景。具体的な事例も書くことを推奨する
  • Implementation Ideas
    • 設計。概要とともに、テーブル定義、API定義、必要に応じた実装の詳細を記載する
  • References
    • 参照物。関連Issue、Slackでの重要な会話リンクなどを記載する

画像のように、必須項目なども指定できるIssue Formsが、Public向けのベータ版機能として存在しており、Privateリポジトリでも組織単位で申請することで利用できる場合があります。(参考情報: GitHubコミュニティでの投稿。2022/10/01現在の情報であり、変更の可能性があります) Issue Formsを使えない場合もTemplate自体は利用できるので、ガイドとしてのTemplateを作成し、それに沿わないIssueにはコメントで伝えていくのが地道ながら効果的です。

また、少し特殊な点として「Implementation Ideasの部分でPull Requestの粒度を確定させる」ことをしています。

粒度は基本的にGoのモジュール単位です。決済チームではマイクロサービスの単位でモジュールを切っているため、Pull Requestの粒度と合わせやすくなっています。

具体的には、下記の単位でモジュール = Pull Requestが切られています。

  • テーブル定義
  • マイクロサービスごとのAPI定義 (gRPC + protobuf)
  • マイクロサービスごとの実装

この粒度には賛否あると思うのですが、決済チームではPull Requestは小さく保つことを目指しています。これはレビューのハードルを下げ、マージを迅速に行うためです。 また、この粒度まで確定するルールにすると、実装に入るために一定の内容まで設計を進める必要があります。タスクの進め方も均質化を図ることができ、えいやっで実装してしまうようなケースを抑制する側面もあるのです。

このように、Issueでは形式・内容とともにPull Requestの粒度について、Templateや基準を設けることで均質化を図っています。


Pull Requestの内容と形式を定め、マージの影響を明確にする

Pull Requestも基本理念はIssueと同じです。 定型化して内容を担保し、できた余裕をそのPull RequestのSpecificな内容の議論に当てます。

Pull RequestでもTemplateを作成していますが、担保したい内容は当然Issueとは異なります。プロダクションコードへのマージが行われるため、Issueに比べ注意深く影響を確認できるような項目を増やしています。

下記にシンプルにまとめてみました。

  • What: 何を変えたか
  • Why: なぜ変えたか
  • Reference: 関連Issue, Pull Requestなど
  • Rollback Procedure: 単純にRollbackできるか、できない場合は何が必要か
  • Expected Impact: マージされた際の現行への影響は何か
    • ここには特に、影響や副作用が出やすい項目のチェックリストを用意
  • (Optional)Background: 特殊な事情など
  • (Optional)Not Implemented: ToDoとして残したもの
  • (Optional)Test Details: 特殊なテスト方式など

これらの項目は一般的なようで、実はメンバーそれぞれが経験してきた職場環境などで文化が異なるため、自分で書き方を考えたり他の人の書き方を読んだりするのにエネルギーを使う部分だと思います。 実際にレビューしている身としては、決まった場所に決まったものが書いてある(なければ指摘をすれば良い)という状態は、かなりストレスフリーです。現状決済チームのメンバーは、これらをきちんと書いてくれており、とてもいい状態だと思っています。


コードの満たす品質をCIで担保する

詳細なコードレビューに関しては、コーディングルールなど用意しているものの、メンバーの強みに頼っている部分が大きいと思っています。それらは今回のテーマとは少し外れるものなので、いまは言及しません。ただし、本質的な部分のレビューに注力するためにも、仕組み化による余裕が大事になってきます。

決済チームのPull Requestのコード品質に関する仕組みは主に2点で、CIによるBuild & Testが正常終了することと、特定権限のレビュアーからApproveされることです。(このレビュアーは自動でCode Ownerが任命されます)

今後追加したい内容としては、テックブログの第2回でも触れていたテストカバレッジの観点をPull Request上から確認できるようにしたいと思っています。検討は必要ですが、カバレッジをマージ条件に加える可能性もあるでしょう。

決済チームのCIはGitHub Actionsを利用していますが、UPSIDERは基本的にmonorepoを採用しているため、他チームの環境に合わせたWorkflowも数多く存在します。

当然変更内容に応じたWorkflowのみを動かしたいのですが、その場合はブランチ保護がうまく働かず、「動かしたWorkflowのみ正常終了すればマージ可能」ということができません。

これを解決するため、UPSIDERではMerge GatekeeperというPull Requestマネジメント用の機能を作成しました。これにより上述でやりたかった細かなマネジメントが可能になります。

実はMerge Gatekeeperは決済チーム独自のものではないのですが、MITライセンスでOSS化してあり、導入もとてもシンプルなので、この機会に紹介させてもらいました。ぜひお試しください。


あとがき

決済チームの当たり前の品質を支える取り組み、いかがでしたか?

シンプルに均質化、省力化しており、できた余力でメンバーにバリバリ活躍してもらおうという姿勢が伝わっていたら幸いです。今回はいわゆる守りのお話でしたので、今後は決済チームの攻めのお話もしていきたいと思います。

お楽しみに!

宣伝

UPSIDERでは、成長企業のための法人カード「UPSIDER」と、すべてのBtoB取引でクレジットカードを利用できるビジネスあと払いサービス「支払い.com」を提供しております。

まだまだプロダクトで実現したいことがたくさんあり、プロダクトの急激な成長に伴う課題も増えている中で、一緒に事業を前に進めてくれるエンジニアを絶賛募集中です。

カジュアル面談もやっておりますので、少しでもご興味のある方は、ぜひご連絡ください!!!

career.up-sider.com

herp.careers

支払い.comのカオスで整備中なエンジニア組織について

支払い.com で主にバックエンド全般を担当しているエンジニアの水村です。

支払い.comのエンジニア組織はまだまだ整備中で課題も多く、カオスな状況です。そんなカオスな状況を改善するため、やらないようにしていることや、意識的にやっていることなどをまとめました。

支払.com のサービスについては、支払い.comのサイトをご覧いただけるとめちゃくちゃ嬉しいです。(https://shi-harai.com/

TL;DR

  • 我々がやらないようにしていること
  • 意識的にやっていること
  • 開発フロー
  • エンジニア紹介

やらないようにしていること

いきなり話がブッ飛びますが、エンジニア組織でやらないと決めていることは次の通りです。また、いくつかの項目については詳細を記載しました。

アジャイル開発やスクラム開発をベースに開発を行っているのですが、デイリースクラムやスプリントなど、検討した結果採用していない手法もあります。

  • エンジニア抜きでの意思決定
  • スプリント
  • 無駄な会議
  • デイリースクラム的な毎日のミーティング、稼働の管理
  • 過剰なタスクの管理
  • 休日、深夜の Slack メンションや DM を遠慮して控えること
  • Slack DM によるコミュニケーション
  • ダメな物事を無視する行為
  • 偉そうにすること
  • 聖域を作ること
  • 働きすぎること

エンジニア抜きで意思決定をしない

エンジニアが意思決定に大きな裁量と権限を持っているので、基本的に意思決定にはエンジニアが関与します。

エンジニアが意思決定に大きな裁量と権限を持っているのは、カード側の事業も同じです。支払い.com事業でも同様に、ビジネスサイドの偉い人がこういったからそれに従う、といったような組織ではありません。

よって、エンジニアには自立的に動いて意思決定をすることが求められます。言い方を変えると、自走力のあるメンバーであれば、正社員でなくとも一定以上の権限と裁量を持って取り組むことができる環境です。

むしろ、このように大きな権限と裁量を持っているフリーランスや副業のエンジニアが多いのがUPSIDERという会社の特徴の一つです。

スプリントをやらない

フルリモート、フルフレックス、コアタイムなし、稼働時間に制限がなくバラバラ、副業が多いという環境であるからこそ、スプリントを採用することが難しいので採用していません。

スプリントを採用した場合、毎週リリースしなければいけないというプレッシャーから、品質がおろそかになったり、テストがおざなりになって本番障害につながるというリスクがあると思っています。

微妙な状態でリリースするのかどうかという判断を毎回するのも面倒ですし、バグがある状態でリリースブランチにマージされていると revert するのも面倒ですし、とにかく面倒なことが多いわけです。

リリース対象が増えれば増えるほどリリースに対する負荷が高くなり、テスト対象が増え、影響範囲も大きくなるので、細かくテストして細かくリリースできた方が高い品質を維持できるという認識からスプリントを採用していません。

ですが、スプリントを採用することによって開発サイクルにリズムが生まれて開発がやりやすくなるというメリットがあることも認識しているので、スプリントで開発することが最適だと思ったら採用すると思います。

無駄な会議、デイリースクラム的な毎日のミーティング、稼働の管理

会議などにより必要以上に時間を拘束することは、エンジニアという職種の特性上、業務に支障がでることが多いので、無駄な会議を行わないようにしています。

必要に応じて会議は設定していますが、副業でジョインしているメンバーが多いためデイリースクラムを行うことは実質不可能です。働く時間帯がメンバーそれぞれ異なるため、作業を開始するときに Slack で「稼働開始、終了」といった報告も不要としています。

各々のエンジニアの稼働については、性善説に基づいています。不正をしたところで github のコントリビュートが少なければわかりますし、その人が働いているかどうかは、一緒に働いていれば大体わかります。

ただ、デイリースクラムができないことにより、日々のコミュニケーションが少なくなってしまうことや、エンジニア同士の関係性を築くことが難しくなってしまうと思っています。

そのために、エンジニア同士が会話をしやすいような雰囲気づくりを心がけたり、ちょっとした雑談をするような任意参加のミーティングを設けたりするといった施策もおこなっています。

休日、深夜の Slack メンションや DM を遠慮しない

休日、深夜の Slack メンションなどは基本的に気を遣う必要はないというスタンスです。

理由は以下の通りで、どうしても今すぐに反応してほしい緊急事態の場合は電話をする運用になりました。

  • 非同期コミュニケーション前提なので、後で返事すれば問題ない
  • 通知は各自でオフにできるので休日、深夜はオフにしても問題ない

また、Slack DM に関してはあまり使用を推奨していません。会社的にもオープンなコミュニケーションを行うようにしたいという考えがあるので、基本的に Slack チャンネルでの発言を推奨しています。

ダメな物事を無視しない

ダメな物事を無視していると、割れ窓理論的に秩序が保たれなくなり、優秀な人がいなくなるので、なるべく早く手を打つように心がけています。

ですが、ある程度放置しても問題なさそうなことに関してはあえて放置して、成り行きに任せるケースもあるので、その辺りの見極めが必要だなと思っております。また、問題によっては「この問題は放置しても問題ないよね」という合意が必要だと思うので、事前に合意するという運用が必要になってくると考えています。今のところまだそのケースは発生していないですが、人が増えてくると必要になりそうかなという認識でいます。

偉そうにしない

まず、UPSIDERでは偉い人という概念がありません。基本的に全員フラットで役割の違いがあるという状態を目指しています。

「Tomo さん(弊社代表の水野)が言っているから正しい」というようなことはなく、間違っていると思ったら反対意見を言います。関係性としてはフラットですが、お互いへのリスペクトや礼節はある、という組織です。

あとは、パフォーマンスや日々の行動によって、自然発生的にリーダーやマネージャー的なポジションが決まっていっていくような感じです。実際、僕はリーダーをやってくださいと任されたわけではないですが、自然とそうなっています。もっとイケてるリーダーは他にたくさんいると思うので、いつでも代わってもらえるように環境を整備中です!

聖域を作らない、働きすぎない

これらの施策はまだ取り組み中ですが、目指している課題でもあります。

主体性と属人化、コードオーナーと責任感は、複雑に絡み合って切り離すのが難しいと個人的には考えています。ですが、同時にその人にしか作業できない聖域を作ってしまうので、共有したい業務の仕様を少しずつ切り出して他の人にやってもらったり、モブプロを行って他の人に仕様を共有する、ドキュメントを整備するなどで対応していっている最中です。

働きすぎない、というのは文字通り働きすぎないようにすることです。仕事の Slack を昼夜休日問わず見続けたり、仕事が無限にあるからといって毎日遅くまで働くと脳がおかしくなるので、働かない時間を意図的に設けています。スタートアップではこれがかなり難しくなってしまう傾向があるので、意識的にやる必要がある認識です。


意識的にやっていること

エンジニア組織で意識的にやっていることを次に記載しました。

  • 小さく頻繁にリリースする
  • リファクタリングを頻繁に行う
  • 技術負債を早めに解消する
  • コミュニケーションしやすい環境づくりをする
  • ジョインした人がミスマッチだった場合はすぐに対応する

小さく頻繁にリリースする

支払い.comでは早く作って早くリリースすることを心がけています。

リリースサイクルは特に定めていないので、機能が出来上がってテストが完了したら、すぐにリリースすることが多いです。簡単な修正や小さな機能は、すぐにテストが完了することも多いので、週に数回リリースすることも割とあります。スプリントを採用していないのでリリースまでの待ち時間(リードタイム)がほぼ存在しません。全体に関わる影響が大きい機能に関してはリリースタイミングの調整が必要になるので、もちろんそのようなケースではリリース時期を調整することはあります。

リファクタリングを頻繁に行う

細かいリファクタリングを頻繁に行うようにしています。

放っておくと負債はどんどん積み上がっていってしまい、品質を維持できなくなり、かつ素早くリリースすることが困難になるためです。というよりも、エンジニアが自発的にリファクタリングを行なって、コードベースをきれいな状態に保っているので、気がついたらなんかコードベースが綺麗になっていてありがたいという感じです。

もしかしたら AI が勝手にコードを綺麗にしているのかもしれません。

技術負債を早めに解消する

大きめな技術負債は早めに解消するように心がけています。

ローンチ当初は Nuxt.js とAnt Design という UI ライブラリを採用して画面を開発していたのですが、Nuxt.js が2系で Ant Design のレイアウト制約などがきつくて両方とも技術負債になってしまいました。

そこで、Nuxt.js と Ant Design をやめるという判断をローンチから3ヶ月ほど経過した際に行い、1ヶ月ほどかけて Nuxt.js をやめて、 Next.js にリプレイスを行いました。機能がまだそれほど多くなかったので、約1ヶ月という短期間でリプレイスは完了し、かつReact に強い優秀なエンジニアが何名かジョインしてくれたので、Next.js にリプレイスしてよかったと思っています。Ant Design をやめてからは、基本的に UI ライブラリは使用せず、必要に応じて小さなライブラリを導入するようにしています。管理系の画面は Nuxt.js のままなので、こちらもそのうちリプレイスするか、Nuxt.js の3系にバージョンアップしたいところです。

コミュニケーションしやすい環境づくり

フルリモート、フルフレックス、コアタイムなし、働く時間も自由という環境は、とても自由度が高く働きやすい環境ではあるのですが、コミュニケーションが希薄になってしまい、チームの一員として動くことに障壁が存在します。

僕自身も多くのメンバーとリアルで会ったことがありません。よって、コミュニケーションをしやすくするための施策として、定期的に雑談をする任意参加のカジュアルなミーティングを設定しました。

English-speaker が何名かいるので、基本的には英語で話すことを推奨しています。また、英語が苦手な人でも話しやすいような雰囲気づくりを心がけたり、仕事の話だけではなく、お互いのプライベートな話や最近の出来事などを英語で話すようにしています。こうすることによって、仕事のちょっとした相談などをしやすくしたり、ミーティングの場でも話しやすい環境づくりにつながればと思っています。

このような施策以外にも、例えば新しくジョインした人に対しては、毎日軽く話すような軽めのミーティングを設定することも考えています。さらに、UPSIDER 全体の施策としては、毎週木曜日 WeWork にエンジニアが集まってカジュアルに話す WeWork day というのを設けていますので、もし UPSIDER のエンジニアとちょっと話してみたい方がいらっしゃいましたら、是非ご連絡ください。いつでもウェルカムです。

ジョインした人がミスマッチだった場合はすぐに対応する

正直なところ、面談をしただけでは判断できないことが多く、一緒に働いてみないとわからないことが多いです。

一緒に働いていると、成果物が出るまでの早さや適切なタイミングでのコミュニケーション、コードの品質など、いろいろとわかってくることが多いので、そこで初めてちゃんと評価ができる認識でいます。もちろん、面談時に判断できることも一定あるので、面談の際にはあらかじめ決めた質問をして、人によって面談の内容にばらつきが出ないように標準化して面談をするようにしています。

ミスマッチが発生した場合は早めに手を打つようにしており、こちら側で改善が可能なことに関しては改善し、相手に改善が必要だと思ったことは率直に伝えるようにしています。結果はどうあれ、なるべく早く対策を打つことが、お互いにとってポジティブであると考えています。


開発のフローについて

特にこれといって変わったことをやっているわけではなく、一般的な開発フローです。

git のブランチ構成は develop, main ブランチと各機能単位のブランチを作成する一般的な構成です。タスクの管理は主に github project を issue で管理しており、Epic issue を作成し Epic issue の中に各種 sub task の issue を作成して追記するようにしています。Epic issue のサンプルは次のような記載です。English-speaker のエンジニアがいるため、基本的に全て英語で Issue を記述するようにしていますが、限定的に日本語で書いても OK としています。

開発のフローは notion に定義し、どのような流れで機能を開発するのか、意思決定の基準は何かなどを記載しています。ただ、一般的な開発の流れだと思うので、普通のエンジニアであれば迷うことなく開発が進められる認識です。


エンジニア紹介

支払い.comで活躍しているエンジニアの特徴を紹介します。

  • 素早く機能を作り上げること
  • コードの品質が高くバグが少ないこと
  • 自立性があること
  • 重要な部分に関してはきちんと相談ができており、その判断を間違えることがない
  • 相談の必要ない部分に関しては自分で判断して機能を作る

これだけ書くとものすごく仕事ができて、人間的にも非常に優れているように思うかもしれません。ですが、実際のところ、新しいゲームが発売されると気配が消えるエンジニアもいますし、部屋がめちゃくちゃ汚かったり、そもそも机と椅子がない状態でベッドの上働いているエンジニアもいたりします。

また、学歴がとても良いというわけではなく、僕は IT 系の専門学校卒(日本電子専門学校という素晴らしい専門学校です)ですし、新しいゲームが発売されると気配が消えてしまうエンジニアも同じ専門卒です。高卒のエンジニアもめちゃくちゃ活躍してますので、学歴はぶっちゃけ関係ないなって感じです。

エンジニアとして濃い経験をしているメンバーが多いので、自走力やタフネスなど、それぞれがそれぞれの強みを持ったチームだと自負しています。ブラック企業で叩き上げられたエンジニアが多いので、とにかくタフネスが違います。

このように、非常に優秀だけどどこか欠点がある(?)エンジニアに支えられています。

支払い.comのエンジニアを何名かピックアップして簡単にご紹介します。エンジニアの多くが30代であり、経験豊富なメンバーが多く、ほぼ全員がフリーランスです。とてもいい人が多いので大変助かっておりますし、みんな優秀で自律的にいろいろと動いて取り組めるタイプです。

フロントエンド

  • Oさん
    • フロントエンドの全般を担当
    • とにかく実装がめちゃくちゃ早くてクオリティも高いし、レスポンスも早い
    • 副業で週2-3日の稼働だが、夜に働いていると思って翌朝に気づいたらフロントエンドの機能がほぼ出来上がっているので妖精かもしれない
  • Oさんが連れてきたOさん
    • 副業でジョイン(週1ぐらいの稼働)
    • 実装が早くバグが少なくて最高
    • Slack のアイコンが猫
  • Mさん
    • 副業でジョイン(週1の稼働だがおそらく週2〜ぐらい稼働している)
    • タスクをお願いして気づいたら終わっている
    • OpenAPI のスキーマ分割をお願いしたら、なんか知らんけどいつの間にか終わっていた
  • 僕の友達のOさん
    • 別プロジェクトで Go をメインに書いているが、 React の経験豊富なのでジョインしていただいた
    • 今は稼働少なめだが、能力はめちゃ高いので将来的にいろいろやってもらえそうな感じ

バックエンド

    • バックエンド全般を担当していたが人が増えたので最近はマネジメントを担当
    • エンジニアとビジネスサイドが協調して働ける環境整備をおこなっている
    • 最近ジョインした English-speaker のエンジニアと3歳児並みの英語力で週に2-3回打ち合わせをしている(大変ご迷惑をおかけしています。。)
  • Aさん
    • バックエンドの支払い登録周りの API など、コアな機能を担当
    • 三十代最後の夏が終わった
    • 大作のゲームが発売されると1ヶ月ほど姿が消える
    • ちゃんと働いているときはきちんと成果を出すので黙認している
    • 自宅でとんこつラーメンのスープを作りながら MacBook で仕事をしていたらとんこつスープの脂分で MacBook のファンが壊れて MacBook も壊れた
  • もう一人のOさん
    • 別の会社で正社員をやりながら副業で支払い.comを手伝っていただいている(週1ぐらいの稼働)
    • バックエンドを主に担当していただいているが、僕からの無茶振りで管理画面(Nuxt.js)も作成していただいており、積極的にいろいろ手を動かしていただいている
    • 医療系の業界からエンジニアに転身してエンジニアのキャリアはまだ浅いがすごくしっかりしている
  • Vさん
    • 最近ジョインした外国籍で English-speaker のエンジニア
    • 別の会社で正社員をやりながら副業で支払い.comを手伝っていただいている(週2ぐらいの稼働)
    • 本業では PM, スクラムマスターなどマネジメントを担当されているが、支払い.comではバックエンドの api 開発などを担当していただいている
    • マネジメントができてコードも書けるので、あとはとにかく僕らが英語をがんばろうという気持ちです
  • Hさん
    • Hさんも最近ジョインした English-speaker のエンジニア
    • 技術レベルが高く、機械学習をやったり不正利用検知などを担当していただいている
    • ヴィーガンのため日本のレストランで食事をするのに苦労している
  • Yさん
    • バックエンド全般を担当
    • 最も複雑な支払い登録周りの処理を素早くキャッチアップしていただいている
    • 支払い.comのエンジニアチームの中では数少ない英語がちゃんと話せる日本人エンジニア(余談ですが、僕の英語レベルはだいたい3歳児並みで、週に2〜3回 English-speaker なエンジニアとミーティングをしています)
    • 来年ごろカナダに移住する予定
    • カナダはビザを取るのがそんなに難しくないらしい
    • カナダには雄大な自然がある

あとがき

エンジニアが働きやすい環境を作るためには、チームみんなの協力が不可欠なのですが、いろいろな改善案などを提案してくれたり、みんなに協力してもらってめちゃくちゃ助かっています。

まだまだ改善中でカオスなエンジニア組織ですが、みんなが働きやすい環境になるように、これからも継続して改善していきたいと思っています!

最後までお読みくださり、ありがとうございました。UPSIDER はエンジニアやプロジェクトマネージャー、プロダクトマネージャーなど幅広く募集中ですので、もしご興味がありましたら、ぜひ一度カジュアルにお話しましょう!

career.up-sider.com

herp.careers

決済チームがテストコードを書く際に気を付けていること

こんにちは。決済チームでエンジニアとして働いている芦川です。

UPSIDER Tech blog 第2弾として「決済チームがテストコードを書く際に気をつけていること」を紹介しようと思います。

TL;DR

  • 100%のテストカバレッジを目指す
  • テストはブラックボックスを優先して記述、どうしても到達できない場合はホワイトボックス
  • 最初のテストケースは、テスト対象が動作する最も一般的なケースであるべき

私たちは日々大量のコードを書いており、そのシチュエーションは多岐にわたります。

そういった環境において、動作確認からのコード改修のコストを考えた場合、自動テストの有無によって生産性に大きく差が出ることは容易に想像ができます。また、既存のサービスに改修を加えるために、そのサービスの概要を把握したい場合、良いテストコードはドキュメントとして役立ちます。

以前、私はテストコードを一切書かないプロダクトの開発に従事していたこともありますが、今となってはどういう理屈でプログラムが正常に動作していたのか不思議でなりません。おそらく超自然的な何らかの力が作用していたものと考えられますが、その点についての言及は題とずれますので今回は控えさせていただきます。

というわけで、今回は私たちがテストコードを書く場合、指標として取り扱っている項目のうち、代表的なものを3つほど紹介しようと思います。


100%のテストカバレッジを目指す

これは私たちのチームが最も重要視している指標の一つです。

私たちが提供している決済サービスが正常に動作していない場合、お客様に与える影響は計り知れません。例えば、「お客様が本来拒否すべきである決済を誤って成立させてしまう」といったことも考えられます。

UPSIDERには、日次のリミットを設定できる機能があります。これは1日に利用できる金額に制限を設けるといったものです。上限を超えるような金額の決済が発生した場合に、それが正常に処理されなかったとすると、お客様からすると想定外の利用が発生してしまったことになります。

もしこういったことが頻繁に起こると、お客様に新しい機能を提供した際にも信用低下を原因に利用していただけず、より良い体験が提供できなくなってしまいます。そのようなことが起こらないよう、お客様に信用いただける品質の機能を提供するために、私たちは自動テストを重厚に書いています。

どうしても到達できないコード以外は絶対にテストケースを書きますし、ストアされているデータのパターンについても複数パターンを用意してテストします。

計測手法としてgo testカバレッジを利用するのは有用ですが、それだけを満たせば良いというわけではありません。決済システムにおいては、より網羅性の高いテストを目指すべきなので、go testで得られるようなC0のステートメントカバレッジを高い基準で満たすのは当然のこととして、C1やC2といったより複合的なカバレッジについても100%を満たすよう努力しています。


テストはブラックボックスを優先して記述し、どうしても到達できない場合にホワイトボックステストを書く

Go言語においてテストを書く場合のパッケージについて、大きく二つに分けることができます。packagename_testパッケージ名でファイルを作成する方法と、packagenameのパッケージ名でテストファイルを作成する方法であり、前者をブラックボックステスト、後者をホワイトボックステストと呼んでいます。

ホワイトボックステストの場合、そのパッケージ内のプライベートな関数や変数に直接アクセスすることができるため網羅性も高まりますし、基本的にテストコード自体の書きやすさも得られます。

しかし、ホワイトボックステストの採用には、以下のような欠点もあります。

  1. アクセシビリティを意識しないことから、必要以上の情報を公開してしまう恐れがある
  2. エッジケースの判断が難解になる
  3. 本来必要ない実装を見逃す恐れがある

1, 2は直感的なことだと思いますので、3つ目の観点を深掘りたいと思います。

例えば、以下のようなコードがあったとします。

package pkg

import (
    "errors"
)

func GetUserWithCurrentUsage(userID string) (*User, int, error) {
    u, err := getUser(userID)
    if err != nil {
        return nil, 0, err
    }
    cu, err := getCurrentUsage(userID)
    if err != nil {
        return nil, 0, err
    }
    return u, cu, err
}

func getUser(userID string) (*User, error) {
    if userID == "" {
        return nil, errors.New("userID is empty")
    }
    req := &GetUserRequest{
        UserID: userID,
    }
    return callUserService(req)
}

func getCurrentUsage(userID string) (int, error) {
    if userID == "" {
        return 0, errors.New("userID is empty")
    }
    req := &GetCurrentUsageRequest{
        UserID: userID,
    }
    return callUsageService(req)
}

これはユーザのデータと現在の使用量を取得する関数群です。

ホワイトボックスで全てのテストを記述した場合、容易に100%のカバレッジを得ることができるでしょう。しかし、ブラックボックステストのみを記述した場合、それは不可能です。

よく見てみるとgetCurrentUsageでのuserIDのバリデーションは、すでにgetUserで同様のバリデーションが行われていることがわかります。そのため、公開されている関数であるGetUserWithCurrentUsageのみをテストするとその条件分岐が不要であることがわかります。

この結果を経てリファクタリングすると、以下のようなコードになるでしょう。

package pkg

import (
    "errors"
)

func GetUserWithCurrentUsage(userID string) (*User, int, error) {
    // add
    if userID == "" {
        return nil, errors.New("userID is empty")
    }
    u, err := getUser(userID)
    if err != nil {
        return nil, 0, err
    }
    cu, err := getCurrentUsage(userID)
    if err != nil {
        return nil, 0, err
    }
    return u, cu, err
}

func getUser(userID string) (*User, error) {
    // remove
    // if userID == "" {
    //     return 0, errors.New("userID is empty")
    // }
    req := &GetUserRequest{
        UserID: userID,
    }
    return callUserService(req)
}

func getCurrentUsage(userID string) (int, error) {
    // remove
    // if userID == "" {
    //     return 0, errors.New("userID is empty")
    // }
    req := &GetCurrentUsageRequest{
        UserID: userID,
    }
    return callUsageService(req)
}

これによりブラックボックステストのみ記述することで、100%のカバレッジを達成することができます。こういった背景から、私たちは基本的にブラックボックスの形でテストコードを記述しています。

その上で、到達できないエッジケースのテストをホワイトボックスに閉じ込めることで、ブラックボックステストのコードを陳腐化させず、かつドキュメンテーションの役割を持たせることも意識しています。


最初のテストケースは、テスト対象が動作する最も一般的なケースであるべき

前提として、私たちはテストケースをテーブルドリブンで記述しています。

テーブルドリブンテストはGo言語のテストコードで広く採用されている手法で、以下のようにケース単位でテスト内容を記述する方法です。

テーブルドリブンテストをmapではなくsliceで定義する方法もありますが、テストケースにタイトルは必須だと考えているため、弊社ではmapで表現しています。

func TestSum(t *testing.T) {
    cases := map[string]struct {
        num1 int
        num2 int

        want    int
        wantErr error
    }{
        "Success case: 1 + 2": {
            num1: 1,
            num2: 2,
            want: 3,
        },
        "Success case: 106 + 24": {
            num1: 106,
            num2: 24,
            want: 130,
        },
        "Fail case: Max value that can be represented by an int is exceeded": {
            num1   : 9223372036854775807,
            num2   : 1,
            wantErr: invalidLengthErr,
        },
    }

    for name, tc := range cases {
        t.Run(name, func(t *testing.T) {
            res, err := mypackage.Sum(tc.num1, tc.num2)

            // 実際のアサーションにはgo-cmpを利用しています
            if res != tc.want {
                t.Errorf("received data didn't match: want %#v, got %#v", tc.want, res)
            }
            if !errors.Is(err, tc.wantErr) {
                t.Errorf("error didn't match: want %#v, got %#v", tc.wantErr, err)
            }
        })
    }
}

テーブルドリブンテストの効力として、うまく活用すればテストケースがそのパッケージのドキュメンテーションになりうるというものがあります。上述の例でも何に利用するかが明確で、かつどう言った場合にエラーとなりうるかを一覧化することができています。

そのため、パッケージの利用者はテストコードを閲覧することで、利用方法と利用に際して気を付けるべきことを簡潔に理解することができます。

さて、この利点を最大限に活かすために気をつけるべきことが、タイトルの通り「最初のテストケースは、テスト対象が動作する最も一般的なケースであるべき」というものです。

以下の例をご覧ください

func TestSetTransactionUpperLimit(t *testing.T) {
    cases := map[string]struct {
        userID string
    limit  int

        want    int
        wantErr error
    }{
        "Success case: Simple case": {
            userID: "some-user-id",
            limit : 1,

            want: 1,
        },
        "Fail case: Specify a negative value": {
            userID: "some-user-id",
            limit : -10000,

            wantErr: errNegativeValue,
        },
    }

    for name, tc := range cases {
        t.Run(name, func(t *testing.T) {
            res, err := userpackage.SetTransactionUpperLimit(tc.userID, tc.limit)

            if res != tc.want {
                t.Errorf("received data didn't match: want %#v, got %#v", tc.want, res)
            }
            if !errors.Is(err, tc.wantErr) {
                t.Errorf("error didn't match: want %#v, got %#v", tc.wantErr, err)
            }
        })
    }
}

これは、決済あたりの取引額を制限する機能のセットアップ部分のテストコードを極度に簡略化したものです。このテストコードは実際に動作するでしょうし、カバレッジは満たしています。

しかし、ドキュメンテーションとしての能力は低いと言わざるを得ません。パッケージの利用者は、このテストケースを見ただけでは、関数の利用シーンについて混乱してしまうでしょう。

お分かりかと思いますが、その理由は上限の設定にあります。

実際にこの機能の利用シーンを想定した場合、ユーザは1円を決済あたりの取引額の上限に設定するでしょうか?もしかしたらそういった場合もあるかもしれませんが、基本的にそれはエッジケースでしょう。

このテストの最初のテストケースは、より高額な値であるべきです。利用シーンは複数想定されるため、絶対的に正しい値というのはありませんが、より実務に沿った値とすることが重要です。一例として、私たちの実際のテストコードの最初のケースでは300000を設定しています。

このように、テストを書く際に実際のユースケースを想定して記述することで、テストコードに単なる単体テストで終わらせる以上の価値を付与することを重要視しています。


今回は以上となります、お読みいただきありがとうございました。今後も共有の価値があるインサイトがあれば、また記事にしようと思っていますので、その際はまたご覧いただけると嬉しいです。

宣伝

UPSIDERでは、成長企業のための法人カード「UPSIDER」と、すべてのBtoB取引でクレジットカードを利用できるビジネスあと払いサービス「支払い.com」を提供しております。

まだまだプロダクトで実現したいことがたくさんあり、プロダクトの急激な成長に伴う課題も増えている中で、一緒に前に進めてくれるエンジニアを絶賛募集中です。

カジュアル面談もやっておりますので、少しでもご興味のある方は、ぜひご連絡ください!!!

career.up-sider.com

herp.careers

「Tech Meetup 〜Goで作る決済サービス〜」にUPSIDERのメンバーが登壇しました

2022年8月4日(木)にオンラインで開催された株式会社KanmuさまとBASE株式会社さまとの合同Meetup 「Tech Meetup 〜Goで作る決済サービス〜」にUPDIERから2名登壇しました!

upsider.connpass.com www.youtube.com

イベント内容

今回のイベントは「Go×決済サービス」というテーマに沿って、カード決済事業を行う3社がそれぞれLT及びパネルディスカッションするものでした。

このイベントでのTwitterハッシュタグ、 #go_tech_meetup もとっても盛り上がりましたので、ぜひ当日のツイートも見てみてください!なかなかニッチな決済の話や決済システムあるあるの呟きが出てきて面白いです。

ご一緒させていただいたKanmuさま、BASEさま改めてありがとうございました!

セッション

UPSIDERのセッションはMiki(@m_miki0108)から、決済金額のリミット機能のうち、月間と日次の取引金額リミットを取り上げてどんな実装をしているのか、実際のGoのコードを紹介しながらお話しさせていただきました。

www.slideshare.net

金額に関わるコアな部分を触るうえで、何を考慮してこのような設計にしたのか、ケースの実例を交えながら決済システムにあまり関わったことがない人でもとっつきやすいようにまとめてみました。Go言語で書いてよかったことに関しては、他のプログラミング言語の経験から比較的最近Go言語を使い始めた私の所感も多めに語らせていただきました。

そして決済金額の日次のリミット機能については、なんとイベントの当日にリリースした機能です!とてもホットな内容をお届けすることができて良かったです。

これをきっかけにGoや決済システム、そしてUPSIDERにも興味を持ってもらえる人が増えるといいなと思ってます。

パネルディスカッション

三社で「Go × 決済」というテーマでパネルディスカッションをしました。

UPSIDERからは、Ryoya(@sekino_pii)がパネラーとして登壇しました。

以下の4つのテーマで、各社のアプローチや経験談を紹介しながら、ディスカッションを行いました。

三社の間で面白い共通点や相違点を見出すことができて、非常に盛り上がりました。

その中でも、決済の”理不尽な仕様”への想いは全員同じで、盛り上がりすぎて止まらない勢いでした。

パネラーのRyoyaは、決済の理不尽な仕様と三年間格闘してきた中で、BASEさま、Kanmuさまからもわかりみのあり過ぎる話が出てきたので(あと登壇中に飲んでいた檸檬堂が拍車をかけて)、テンションがあがって話しすぎてしまいました。

ちなみに、パネルディスカッションで冒頭で紹介した、UPSIDERのカードのアーキテクチャ図はこちらになります。

今後も技術イベントをやっていきます

UPSIDERでは、今後も技術面での発信に力を入れていきます! まだまだ少人数のエンジニアチームではありますが、各メンバーが日々の開発を通して得られた知見を発信していく予定です。

今回のようなイベント形式のものもまた開催していきます。もし「こういうイベントをやってほしい」などあれば、ぜひご連絡ください! Gopherの原作者は Renee French さんです。

宣伝

UPSIDERでは、成長企業のための法人カード「UPSIDER」と、すべてのBtoB取引でクレジットカードを利用できる決済サービス「支払い.com」を提供しております。

まだまだプロダクトで実現したいことがたくさんあり、プロダクトの急激な成長に伴う課題も増えている中で、一緒に前に進めてくれるエンジニアを絶賛募集中です。

カジュアル面談もやっておりますので、少しでもご興味のある方は、ぜひご連絡ください!!!

career.up-sider.com

herp.careers

meety.net