BASE開発チームブログ

Eコマースプラットフォーム「BASE」( https://thebase.in )の開発チームによるブログです。開発メンバー積極募集中! https://www.wantedly.com/companies/base/projects

エンジニアとしてワクワクし続けるためのエンジニアリングマネージャという役割分担

f:id:f-shin:20181130212802p:plain

これは BASE アドベントカレンダー 1日目の記事です。

devblog.thebase.in

CTOの藤川です。ネットではえふしんと名乗っていて、会社でもえふしんさんと呼ぶ人が大多数です。

今年はテックリードの働きかけをきっかけとして、BASE社でもアドベントカレンダーを書いてみよう!という話になりました。皆様よろしくお願いいたします。

最近、エンジニアリングマネージャという役割が急速に普及し始めているので、今回はマネジメントの話について書いてみたいと思います。

IT業界にはエンジニア35歳定年説という都市伝説がまかり通っています。開発技術が進化してコードを書く労力が劇的に下がったにも関わらず、このことに恐怖してる人は少なくないようですし、何よりエンジニアがコードを書かなくなるような状態は望ましい状態ではないとされています。

一方で、成長するビジネスにおいては組織を作っていかないといけないのですが、エンジニアは、エンジニア出身のリーダーに評価してもらいたいという部分と、コードを書かないとエンジニアとして死ぬというイメージに矛盾があって、35歳定年説の不安と相まって、エンジニアのマネジメントのキャリアパスというのはどこか淀んでいる印象すらあります。

要は、マネージャの存在を絶対的に必要としているのに、自分がやるのは不安だという矛盾をはらんでいます。

これまでの転職志望者でも、現職で管理職になってしまったが、まだ技術の最先端に携わりたいと言う理由で面接にお越しいただく方もいて、転職後にマッチングに失敗するかもしれないリスクを取ってでも、転職せざるを得ない状況に追い込まれるというのは、エンジニアである僕らのキャリアにとっては辛い状況とも言えなくもないです。

この状況をポジティブに解決するのがエンジニアリングマネージャという役割だなと思い始めています。

エンジニアリングマネージャとは?

エンジニアリングマネージャとは、ピープルマネジメントの一形態です。エンジニアリングマネージャの仕事を一言で言うと、

事業、プロダクトに貢献しながら、チームのエンジニアの活躍にコミットすることで、メンバーの評価を上げる仕事

だと考えています。チームのメンバーに活躍してもらって、高い評価を得られることが、マネージャ自身も評価され、それができなかったらエンジニアリングマネージャとしては評価が下がる仕事として考えられます。

チームを持ってる以上、事業の推進やプロダクトの開発責任も担うわけですが、自分がコードを書いて問題を解決してもエンジニアリングマネージャとしては評価されないわけです。

会社によっては、そもそもエンジニアリングマネージャは組織図上の上長ではない立ち位置であったりもするようです。メンターみたいな立ち位置でしょうか。

当社においては、組織図上の上司にあたるピープルマネージャがその任を担います。いずれにせよ、ネット系企業におけるピープルマネージャの役割は、サーバント・リーダーシップにのっとりメンバーを下支えすることで、メンバーの活躍をメイクできなければ、すぐにでも他社に転職されてしまう昨今の状況において、チームを支える、メンバーの活躍を導くという役割を担っていることを自覚しているかどうかこそが重要です。

メンバーの給料は成果に応じて昇給する会社がほとんどだと思います。その前提において、エンジニアリングマネージャはメンバーの状況を常にトラッキングし、1on1などを通じてメンタリングしながら、活躍をメイクし、どういう成果が評価可能なのか?を明確にし、今後はどういう期待をかけていくべきか?を共有し、メンバー自身がそれを実現していきます。

昇給というものは、結果そのものに支払われるのではなく、結果によってもたらされる次なる期待に対し人的投資として支払われるものですので、そのお膳立てをする役割だと考えます。評価権限を持っていることで、メンバーと踏み込んだ話をすることができるし、それだけの責任を負うことになります。

エンジニアリングマネージャは常に挙動をメンバーから評価されている

エンジニアリングマネージャがメンバーの評価権限を持つ以上は、メンバーからすると、評価する人に値するか否かというのは常に評価されることになります。

その基準は、

  • 評価する人としての人間性は期待できるか
  • 自分のスキルを判断できる技術力、技術判断力を持っているか
  • 日常の活躍をしっかり観察してくれている人か
  • しっかり自分を高めてくれる指導をしてくれるか

など、緩すぎても厳しすぎても成立しませんし、日常の言葉一つ一つにおいて常にチェックされていると言っても過言ではありません。

コードを書くような知的労働においては、ポジティブに脳内物質の分泌が行われるか否かが生産性の大多数を支配します。目的意識を持っていないとか、楽しくないという状況で良いアウトプットができるわけはありません。それが一文字直すだけの簡単な仕事でも、アーキテクチャから作り変えるような難しい仕事でも、やる意義を引っ張ったり、勇気を振り絞って困難に立ち向かうように問題をほぐしていくのはエンジニアリングマネージャの重要な仕事です。

このマネージャの下にいると自分は評価されないと思った時に取りうる行動は、他部署に異動するか、会社を辞めるかのどちらかであることが多く、マネージャと戦い上司を教育してまで状況をよくしようとしてくれる人は稀であることを考えると、マネージャは常に緊張感を持ってメンバーから信頼に値する評価を得られているのか?について考えていなければいけません。

よく言われる、マネージャになるとコードを書く書かない問題は、チームメンバーの成長にコミットする役割において、メンバーや会社から自分が評価されるために、目の前のコードを自分が書くことのトレードオフを考えて、状況状況でマネージャが最適な手立てを判断してもらえば良いと思います。どうしても危機的状況や問題がある時、または背中を見せる時に率先してコードを書くシーンもあるでしょうから、采配として手段を使い分けるのはエンジニアのリーダーだからこそできることです。

また、技術はどんどん変わってもいくので、メンバーから評価されるためのキャッチアップが必要と考えると、むしろメンバーのプルリクエストを通じて最新技術やエンジニアリングを学ぶ姿勢が必要であるとも考えます。そのためにもプロダクトや事業と連動した技術マネジメントを行う必要も出てくるハズで、基本的にエンジニアとして前向きな役割でしかないと僕は考えますが、皆さんはいかがでしょうか?

エンジニアリングマネージャは「偉い」立場ではない

ここまで書けば、エンジニアリングマネージャは、シニアなエンジニアみたいな役割に近く、旧来型の組織である「上司・部下」「偉い人」というイメージではないことに気がついてもらえるのではないでしょうか?

むしろプロジェクトマネージャやディレクターに近い役割で、とても現場感が求められる仕事で、エンジニアリングにより踏み込んだ形でエンジニアの成長にコミットする役割であると考えます。

この役割になれるスキルはエンジニアとしてのスキルや事業に対する姿勢が信頼されていなければ、エンジニアであるメンバーから評価されることはないわけで、エンジニアのキャリアパスの一つであると考えられます。

そう書くと、この役割にはエンジニアのスキルがものすごく必要なのでは?と思われがちですが、ミニCTOとして技術で引っ張るテックリードとは違います。しかし、テックリードは卓越した技術力でチームへの影響を与え、如何に高いアウトプットを出すか?と言う部分にコミットする役割であると考えると、人を導くという点においてはエンジニアリングマネージャと向いているベクトルには大差はなく、何を「とくいわざ」とするかだけの話で、それぞれがチームで働くエンジニアに求められる職能として考えることが可能です。

そして、ここからが大事なことですが、必ずしもエンジニアリングマネージャは永遠にその立場でいる必要もなく、一定期間で別の人に入れ替わっても良いと考えています。期待するのは大人としてチームの活躍を支えるチームディレクションとしての役割です。ディレクションかプレーヤーかというのは、その都度、役割を入れ替えることはあってもよいと思います。

なによりそうすることで、たくさんのエンジニアが人の成長を支える仕事の難しさを知ることができるし、一度、エンジニアリングマネージャを経験した人は、より広い視野での仕事を期待できるわけなので、改めて現場でコードを書く役割にコミットしながら、開発プロジェクトマネジメントやメンバーの育成においてもエンジニアリングマネジメント力を発揮することが期待できます。

そうこうしてるうちに新しい事業アイディアが出てきた時には、そういう人が率先してエンジニアリングマネージャとしてチームを作れるようになることで組織のスケーラビリティは向上します。

このようにエンジニア組織全体としては、立場を入れ替え可能であることと、マネージャという役割をエンジニアとしてのキャリアの幅や柔軟性を高める役割として定義することで、プログラマ35歳定年説に代表されるような、コードを書かなくなって、エンジニアとしての死を迎えるみたいな不安な世界を終わらせることができるのではないかと考えています。

なお、これは、今自分が携わっているCTOにおいても全く同じです。あくまでも役割分担の一つとして柔軟に人を入れ替え、抜擢可能なチャレンジ文化を作ることは組織の柔軟性とスケーラビリティに影響が出るし、新たな役割への期待を常に作り続けることで、ビジネスが成長することを大前提とした組織運営の考え方として捉えています。

以上、えふしんでした!

明日は id:yaakaito です!お楽しみに!

「BASE Advent Calendar」の記事一覧はこちら。 devblog.thebase.in

CakeFest 2019 にシルバースポンサー・ランヤードスポンサーとして協賛いたします。

こんにちは、BASEでサーバーサイドエンジニアをやっている、東口(@higasgt)です。つい最近、Goを運用アプリケーションに導入する際のレイヤ構造模索の旅路 | Go Conference 2018 Autumn 発表レポート - BASE開発チームブログというGoに関するエントリを書きましたが、PHPもしっかり書いてます。
今回は、特に弊社のメインプロダクトであるネットショップ作成サービス「BASE」のフレームワークの大部分を占めるCakePHPの国際カンファレンスCakeFestの協賛についてです。

さて、この度、BASE株式会社は、CakeFest 2019に、シルバースポンサー・ランヤードスポンサーとして協賛いたします。

cakefest.org

CakeFest 2019 とは、PHP製フレームワークの一つであるCakePHPの国際カンファレンスです。年に一回開催されており、今回は初めて日本での開催です。

冒頭でも述べましたが、弊社はCakePHPをメインプロダクトのフレームワークとして採用し、日々CakePHPをベースとしたプロダクト開発を行っています。そのため、CakePHPコミュニティに貢献したいという想いから、初の日本開催となるCakeFest 2019に協賛することを決定しました。

また、CakeFestについて、日本で開催されると決定される前から、弊社の@tenkomaが、PHPカンファレンス関西2018にてCakeFestを紹介しておりました。
私も日本開催を待望しており、今回協賛という形でCakeFestに参加できることをとても嬉しく思います。

speakerdeck.com

CakeFestに参加しましょう!

肝心な開催時期ですが、@CakeDC の発表では、2019年11月の開催を予定しているとのことです。

国際カンファレンスが日本開催される機会はそこまで多くないと思いますので、普段CakePHPを触っていない方もこれを機会に参加してみると新たな発見があるかと思います。

トーク応募始まってます!

また、現在トーク応募が開始されています。

普段足が運びづらい国際カンファレンスが日本で開催されるということで、海外で英語で発表したいといった目標のある方はとても良い機会になるのではないでしょうか!

「BASE Advent Calendar 2018」をはじめます🎅🎄

こんにちは。BASEで採用広報を担当している米田(@aiyoneda)です。

今年も残り一か月となりました。今年は、BASEとして初のAdvent Calendarを実施する運びとなりました。こちらの「BASE開発チームブログ」にて、12/1(土)から12/25(火)まで毎日記事を公開していく予定です!

記事一覧はこちらです。随時更新していきます。

devblog.thebase.in

初となる「BASE Advent Calendar」では、技術的な内容だけにとどまらず、BASEのプロダクトや開発方針、開発組織の運営に関わる内容を中心にカジュアルな内容も含めて幅広くお届けしようと思っています。

これをきっかけにBASEという会社やメンバーの雰囲気、またBASEの開発部門での取り組みについて知っていただき、興味を持っていただければ幸いです。

記念すべき初日は、取締役CTOの藤川(@fshin2000)に筆を執ってもらいます!お楽しみに!

BASEのメインDBをAurora(MySQL)に移行しました

こんにちは、BASEランニング部で10kmマラソンなどに参加し、3kgほど体重が落ちたSRE Groupに所属しているデータベースエンジニアの植木です。おかげで甘いものが美味しいです。ちなみに次はハーフマラソンに挑戦です!

今回は会社のブログなどを書いてみます。弊社では、ネットショップ作成サービス「BASE」およびショッピングアプリ「BASE」を運営していますが、11月にメインDBをRDS for MySQL5.6からAurora(MySQL5.6コンパチブル)に変更しましたので、そちらの話を書かせていただきます。

何故Aurora?

まず、弊社でAWSをメインに使っていたという背景があります。入社した際にはRDS for MySQLを使用しており、CTOの藤川も「AWSを使うならAuroraにしたい」という要望を持っていました。私自身、AWSをメインに使い続けるつもりであればAuroraは良い選択だと思っていました。

MySQLを使う場合、オンプレミスやMySQL on EC2の方がスピードや機能面でメリットがありますが一定以上のスキルのエンジニアを複数人確保し続ける必要があり、運用にそれなりの稼働が取られます。RDSを選択して運用負荷を下げる方針であるならば、いっそRDSだからこそ使えるAuroraのメリットを受ける方が良いと考えています。下記2点がOKならAuroraの選択を候補に入れて良いかと思います。

・AWSにベンダーロックインしても大丈夫か
・MySQLの最新バージョンの機能よりもRDBの基本処理のスループットを重視するか

弊社運用を考えて、特にメリットと思った点

  • レプリカラグが発生しにくい
    • ストレージの使い方が大きく変わっていますのでレプリケーションラグが発生しにくいです。
  • クラスターエンドポイントとカスタムエンドポイント
    • HAproxyを使っての複数台レプリカのロードバランスを検討していたため、カスタムエンドポイントが移行直前にリリースされた事は助かりました。リーダーエンドポイントは弊社のように分析用レプリカを作っている場合にはそちらも参照先に入ってしまいます。書き込みはクラスタエンドポイントを指定して、読み込みはサービス用インスタンスだけを指定したカスタムエンドポイントを作成して使用しましょう。
  • クエリキャッシュ
    • クエリキャッシュの作りがMySQLとは全く違うためかなり有用になっています。
  • 無停止パッチ
    • 稀にAWS側要件のパッチが発生するので助かります。
  • スループットの向上
    • MySQLの5倍程度との事。そこまでのスループットはまだ必要ではないですが、後述のログや監視機能を有効にするためにスループットが向上されている事は良いです。
  • Performance Insights
    • MySQL Performance Schema設定を有効にする必要があるので、スループットに15%程度の影響が出るとの事ですが、専門じゃなくても負荷原因特定などが簡単にできるようになっていてとても便利です。MySQLでも使えるのですがスループットへの影響があるので躊躇っていたものをAurora移行を機に利用開始しました。
  • 監査ログ
    • ECというサービスを考えると監査は必要です。スループットに影響するのですがMySQLの監査設定と比べるとAuroraでは格段に影響が少なくなっているため利用する決断がしやすくなりました。ただし、ユーザー単位での記録くらいしかログ対象の選択設定ができないためCloudWatch Logが増える事は認識しておきましょう。
  • 費用面
    • 一台ずつの料金はMySQLの方が安いですが、AuroraのマルチAZは参照可能になっています。MySQL利用時にはサービス用のマスターレプリカ以外に分析・集計用DBを用意していましたが、AuroraではマルチAZ用インスタンスをそちらに割り振ることができます。そのため4台分必要だった費用が3台で良くなり費用削減ができます。
  • 高速クローン
    • 本番相当のDBをQA用に用意する事を進めているため、そちらで高速クローンが利用できると考えています。

Aurora注意点

続いて、Auroraの注意点です。基本的にMySQL互換ではありますが、少しAuroraで利用できない操作や違いがあります。

  • テーブルの圧縮機能が無い
    • 明確に圧縮非サポートと書いてあるドキュメントは無かったのですが、非サポートです。移行時のスナップショットからのリストア時に非サポートと書いてあるドキュメントはあります。 圧縮コマンドは通るのですが、実際には圧縮されず気づきにくいのでご注意ください。
  • Parameter Groupが複数存在する
    • インスタンス用とクラスタ用とに分かれます。Auroraはインスタンスとストレージが分かれているのでクラスタ用=ストレージ用と考えるとわかりやすいです。
  • Aurora Serverlessはどうなのか?
    • テスト環境としてAurora Serverlessを利用できないか考えましたが、仕組み的にクラスタ用パラメータグループしか存在せずインスタンス用パラメータグループにあるinnodb_file_formatでbarracuda指定ができないのが痛いです。おそらく、アクセス時にdefaultパラメータグループを利用してインスタンスが立ち上がってくるためAWS側でdefaultの設定を変えてくれるか、barracudaがデフォルトになっている5.7互換のAuroraでServerlessを出してくれるかに今後期待です。

Aurora MySQL5.7互換はどうなのか?

Aurora最新は5.7互換ですがPerformance Insightsなど新しい機能リリースが5.7にはまだされていません。また、移行パスがスナップショットからのリストアしかないため後述のレプリケーションを利用した移行ができず、移行時間が長くかかります。そのような状況ですので、今回は5.6互換のAuroraが最適だと判断しました。

事前作業

作業は大きく事前作業と当日作業とに分けられます。比率としては事前作業が8、9割で移行当日はそれほど作業がありません。重要なのは事前作業です。

テストに関して

テストに関しては、開発環境・Staging環境を事前に移行してそこを使用して開発をやってもらう事で網羅テストとしました。開発環境を移行後に最低限の確認を各開発チームでしてもらい、その後二ヶ月ほど開発環境として使ってもらいました。

また、開発・Staging環境については後述のような移行構成は作らずに空いている時間にデータベースの変更処理でアップグレードするのが簡単で良いです。

本番環境事前構成の作成

オンプレミスでMySQLをある程度扱っていた方ならわかると思いますが、レプリケーションを利用した移行の事前構成です。下記のような構成を作ります。RDS for MySQLでは子レプリカまでしか作成できませんが、Auroraでは孫以下のレプリカ作成が可能になっています。そのためこのような構成を作る事でサービスに利用しているMySQLには影響を与えずに事前にAuroraの構成を作っておく事が可能です。

f:id:kingyokkun:20181127194402p:plain

この際にほんのちょっと工夫するところとして、テンポラリの書き込み可能MySQLレプリカを間に入れる事をお勧めします。サービスに影響しない書き込み可能レプリカを入れることで、「mysql.rds_stop_replication」と「mysql.rds_start_replication」コマンドにてレプリケーションを操作し、レプリケーションを止めている間にテンポラリに対して巨大テーブルへのALTER文等を実施して事前にAuroraのテーブル定義をカスタマイズできます。弊社で言えば大きめのテーブルへのインデックス追加はこのやり方で実施しておきました。row_formatをDynamicに書き換えておきたい場合などはこの方法で実施すれば夜間メンテなどで長い時間サービスを停止する必要なく実施できます。

当日作業

当日の流れ

他社事例を見ると、ノンストップでの移行なども見受けられましたが、ネットショップ作成サービス「BASE」は60万店舗のショップに使っていただいているサービスのため、一時的に参照だけ可能になる事などが許される状況ではありません。そのため各ショップに連絡し、夜間メンテナンスタイムを入れさせてもらいました。ざっと下記のようなスケジュールです。

2:00:メンテイン作業開始
2:30:移行処理およびその他の追加作業開始
4:00:メンテ明け作業開始
4:30:社内限定アクセステスト
5:00:メンテ明け&最終テスト
5:15:終了

当日の移行作業

AuroraのMasterインスタンスを昇格します。その後、CNAME内のエンドポイント情報を全てAurora側に向ければ完了です。また、旧MySQLインスタンスは万一の場合にすぐに戻しができる必要があるため当日の削除はしません。どこからもアクセスできないSecurity Groupを作って入れ替えるかインスタンス名自体を変更してエンドポイント情報が変わるようにしてあげると良いです。弊社では後者を実施しました。インスタンス名変更は短時間でできますし、AWSコンソール上から見て名前が変わっている事が一目でわかるところが良いです。こちらで無事5時過ぎには作業完了しました。

f:id:kingyokkun:20181127194427p:plain

使用感

マスターはCPU利用率が高くなります。CPUを使い切る最近の流れですので、そこはあまり気にせず。レプリカのCPUはそんなに変わりませんが、タイムラグは本当に減りました。レプリケーションの仕組みが以前と違うのでわかっていたとは言え、ここはとても嬉しいところです。マスターインスタンスのCPU利用率が高めになったため、監視システムのワーニング値はレプリカと分けるなど見直しをした方がいいですが、全体に急激なコネクション増加なども減り、良い結果に繋がっております。

下記が移行前後のCPU利用率の変化です。

  • 緑:旧マスター
  • 赤:旧レプリカ
  • 青:新マスター
  • 黄:新レプリカ

f:id:kingyokkun:20181130150027p:plain

また、下記がレプリカのラグです。単位がミリセコンドなので、非常に安定しています。

f:id:kingyokkun:20181130150112p:plain

最後に

入社後、二ヶ月ほどでRDS for MySQL5.5からRDS for MySQL5.6への移行を実施し、それから1年経ってAuroraにたどり着きました。ようやく、一つ重めの荷物を下ろす事ができるような気がしています。まあ、そう言いながら来年はAurora MySQL 5.7互換やってたりしそうな気もしますが・・・。データベースとの付き合いはコードとの付き合いより長いと言います。今後もうまく回していきたいと思います。

BASEのSREチームではお客様に安心して買い物をしていただくために、サービスの安定性に責任を持ち、インフラ・アーキテクチャの改善に日々取り組み続けております。 ご興味を持たれた方いらっしゃいましたら是非ご連絡いただければ幸いです。

jobs.binc.jp

Goを運用アプリケーションに導入する際のレイヤ構造模索の旅路 | Go Conference 2018 Autumn 発表レポート

お久しぶりです、BASEでサーバーサイドエンジニアをやっている、東口(@higasgt)です。BASE BANKというBASEの子会社にて金融事業の立ち上げを行っています。今回は、BASE BANKで行っているGo言語でのチーム開発について書こうと思います。

なお、このエントリーの内容について、2018年11月25日に開催されたGo Conference 2018 AutumnにてLT登壇してきました。

f:id:khigashigashi:20181126081744p:plain

発表資料については次のURLで公開しています。

Server Side Architecture Strategy for Beginners

今回、LTをCFPで2つ提出して、偶然2つ採択いただいたので、併せてDocker開発環境の整備についても次のスライドで発表させていただきました。

Docker Go development environment starting with realize

Go製のApplicationをDocker上で開発する際に環境整備が喫緊の課題としてある方がいらっしゃればこちらの資料も併せてご覧いただくと参考にしていただけるかもしれません。

社内のGo言語の使用状況

今年の5月23日、「Go言語勉強会を始めたよ」といったエントリーを投稿していました。

devblog.thebase.in

その際には、基本的にBASEのプロダクトはほとんどがPHPで構成されており、各開発者が一時的に使用するスクリプトをGoで書くケースがあるよといった使用状況でした。
現在は、新規機能をGo製APIとして実装・運用するプロダクト機能が出てきており、実際に運用開始しているものもあります。今回紹介する事例についても運用開始に向けて進めているものになります。

レイヤ構造模索の旅路

Go言語で運用を前提としたサービスを実装する場合に最初に頭を悩ませる、レイヤ構造実現のためのプロセスについてです。世の中には、Clean Architectureなどレイヤ構造についての解説記事が多数ありますが、結局は自分たちの要件にあったものを実現していく必要に迫られます。それをどう探っていったかという少し泥臭い話をしようかと思います。
なお、注記しておきますが、今回紹介する構成が「本当に正しいLayered Architectureである」などといった概念を正確に実現できているかという点に関しては保証できませんので予めご了承ください。

なぜレイヤ構造が必要だったか

様々必要・不要意見があるかと思いますが、筆者の環境では次のような理由にて必要だと感じておりました。

  • チームの開発成果物に統一的な構造をもたせて、どこで何が書いてあるかわかりやすくしたい
  • 運用後のプロダクト改善を見越して、ビジネスロジックを入出力から分離した変更容易な設計としたい

運用を前提としない単純なスクリプトなどであれば、コードベースにレイヤ構造をもたせるといった検討は不要かもしれません。
しかし、今回は実際に運用しながらプロダクトとしてブラッシュアップしていく必要があるものであったため、上記の理由からレイヤ構造を考えていきました。

どのように進めたか

構造の概念図を仮定義

まず、最初に概念的な構成を図として定義するところからはじめました。実際に次のような簡易的な概念図を作成しています。 f:id:khigashigashi:20181126084405p:plain

Layered Architectureを参考にしつつ、単一な依存方向となる構成とし、HTTPなどアプリケーションの外側の処理とビジネスロジックを分離する当初の目論見を形にできそうな構成を作成しています。

作っては壊す

上記の図を書いたからと言って実際に実装に落とし込み、かつ機能をそこに乗せてみないと本当に自分たちにとって必要なものだったのかわかりません。そのため、概念図をチームに説明した上で、作っては壊していくことを宣言していました。

スキルを上げていく

チームが扱えるレベルを上げていかないことには、変更容易な設計となる成果物は難しいと考えます。そのため、その時点のチームで、ちょっとずつ背伸びしていく方針としました。そのため、後述しますが最初はシンプルな構成から仮定義した地点に近づけていく作業をしています。

実装を進めるにあたって

ユニットテストに対する意味付け

今回、進めるにあたってユニットテストに対して次の意味付けをしていました。

設計に対するフィードバック材料

ユニットテストを書くことによって、「疎結合な実装になっているか」といったコード設計に対するフィードバックがある程度得られると考えます。そのため、「実装の肌感を増やすためのユニットテスト」という意味合いをもたせました。

大胆なリファクタリング

「作っては壊す」と上で書いたとおり、都度都度大胆なリファクタリングが発生しうります。大胆にコードベースをいじるためにユニットテストを書いています。(しかし、これは同時に筆者に大きく反省を残す瞬間が後ほど現れます。)

パッケージの使用

Go言語を実際に扱うに当たり、標準パッケージでどれだけやるのかという議論はよく耳にするかと思います。筆者のケースでは、Go言語自体を覚えれば書ける状態にしたかったため、なるべく標準パッケージを用いる方針にしています。
後続のエンジニアに対する配慮という意味合いもありますが、同時に自分たちが、インターフェースの使い方など「Goらしく」実装するために必要な技法をプロジェクトを通じてある程度体得している状態としたかったためです。
そのため、最初は愚直に書いていき、理解度に合わせて順次便利になるライブラリ・機構を導入していっています。

実際のレイヤ構造

初版:Simple Model-Controller

一番最初の構造です。非常にシンプルなcontrollerとmodelしかない構造です。

├── model
└── controller

この際は、GoのAPI開発にまずチーム全体で慣れるというところから始めたい意図があり、最小限の構成となっています。 model内には、データベースに対する技術的実装なども含まれており、インターフェースの使い方はまだこれからです。

第二版:Model-Controller + DIP

次に移動した構造です。repositoryとdatastoreという2つのパッケージが増えています。

├── domain
│   ├── model
│   └── repository // +
├── infrastructure
│   └── datastore // +
└── controller

ここで、model内にべた書きされていた、データベースに技術的実装をdatastoreパッケージとよんでいる箇所に移動し、repositoryという抽象レイヤを挟む構成となりました。実装イメージとしては次のようになります。

package repository

type UserRepository interface {
    Save(userID int) error
}
package datastore

type UserStore struct {
    DB *sql.DB
}

func (s *UserStore) Save(userID int) error {
    // 処理
}

ここにて、インターフェースの扱い方について習熟するため、ユニットテストでも自前でモック実装を作成してもらい実装の流れを掴むということをしていました。

第三版:Layered Architecture + DIP

├── config
├── domain
│   ├── model
│   └── repository
├── infrastructure
│   ├── datastore
│   └── router
├── interface
│   ├── controller
│   └── middleware
└── service // + layered architectureのapplication layerに該当

ここで、最初の概念図の構成要素がほとんど用意された状態になります。単一な依存方向となるようなLayered Architectureの構成とし、レイヤ間をより疎結合にするためドメインレイヤにインフラストラクチャが依存する抽象を設置しています。
また、処理の複雑化に伴いアプリケーションレイヤに相当する(※1)serviceパッケージを作成しています。これはトランザクションを扱うことが喫緊の課題として取り組みましたが後に記述する反省点の一つとなりました。

※1 MVC + Serviceと捉えた場合はServiceレイヤと同等とも言えるかも知れません。

現在:Layered Architecture + DIP

2018年11月26日現在の構成です。

├── common // + 全レイヤー共通で利用する機能群
│   ├── uuidgen
│   └── validation
├── config
├── domain
│   ├── model
│   └── repository
├── infrastructure
│   ├── clock
│   ├── datastore
│   ├── logger
│   └── router
├── interface
│   ├── controller
│   └── middleware
├── service
└── testutil // + テストヘルパー群

全レイヤ共通で利用するUUID生成などをcommonディレクトリとして集約し、テストヘルパー群をtestutil packageに集約しています。

実践した上での反省点

トランザクションの扱い

トランザクションをどう扱うかを最初から考えておいたほうが良かったと反省しております。トランザクションをどう扱うによって大きくインターフェースが変わってくるかと思います。筆者の例の場合は次のような扱い方にしています。

package service

type SimpleServiceImpl struct {
    DB         repository.DBConnector
    User       repository.UserRepository
}

func (s *SimpleServiceImpl) Run(userID int) error {
    tx, err := s.DB.Begin()
    if err != nil {
        return errors.Wrap(err, "failed to begin transaction")
    }
    if err = s.User.Save(tx, userID); err != nil { // 引数に*Txを渡す
        wrapRollback(tx)
        return errors.Wrap(err, "failed to save user")
    }
    if err = wrapCommit(tx); err != nil {
        wrapRollback(tx)
        return errors.Wrap(err, "failed to commit transaction")
    }
    return nil
}

トランザクションはservice packageで扱います。その際、service package内で生成された*sql.Tx構造体をそれぞれのrepositoryの引数に渡す構造としています。

package repository

type DBHandler interface {
    Execer
    Querier
    Preparer
}

type UserRepository interface {
    Save(db DBHandler, userID int) error
}

その上で、repositoryでは、*sql.DB/Txをwrapした抽象型を作成し利用しています。なお、この構成については、@codehexさんのもう一度テストパターンを整理しよう(WebApp編) - Speaker Deckという資料を大変参考にさせていただいています。
そして、この実装構成にするために、本当に大胆なリファクタリングが必要になり、チーム全員でそれぞれ自身の実装した機能をリファクタリングするという工数が発生してしまいました。
世の中で公開されているレイヤ構造のサンプルコードでなかなかトランザクションを扱っているものは少ない気がしていますが、実際にやるのであれば、最初にトランザクションを扱うコードをサンプルとして作ってみることはおすすめしておきます。

追記:Contextの扱いについて

頂いたリアクションの中で、トランザクションに合わせてcontextの扱いについての話が出ておりましたので追記しておきます。
こちらもトランザクションと同様にメソッドの第一引数にcontext.Contextを渡す実装にしたほうがよいかと思います。
筆者が実際にcontextパッケージを用いた実装を行う際には、@deeeetさんの次の記事を非常に参考にさせていただきました。

テストヘルパーを先回りして用意しておく

ユニットテストを愚直に書いていくという方針は当初理解度を深める上ではよかったのですが、進んでいく中で共通で毎回やるアサーションなどがコピペ作業になっている状態が発生し、「テストを書くのが億劫になる」といった状態が生まれていました。
先回りして共通的なものをテストヘルパーとして提供して開発体験を損ねないということが重要だったなと思います。
実際例として、ResponseHeaderのアサーションなどのヘルパーなどを随時用意していっています。

// Example assertion response header
func AssertResponseHeader(t *testing.T, res *http.Response, code int) {
    t.Helper()
    if code != res.StatusCode {
        t.Errorf("expected status is '%#v',\n but actual is '%#v'", code, res.StatusCode)
    }
    if expected := "application/json; charset=utf-8"; res.Header.Get("Content-Type") != expected {
        t.Errorf("expected Content-Type is '%#v',\n but given '%#v'",
            expected, res.Header.Get("Content-Type"))
    }
}

また、テストに関するTipsは今回のGo Conference 2018 Autumnにて、@timakinさんが発表されていた、Golang API Testing the HARD way - Speaker Deckという資料が大変勉強になるかと思います。

まとめ

今回のエントリでは、最初に構想を決め、実践しながら拡張して構想に近づけていく事例を紹介させていただきました。
すでにGo言語をバリバリ使用している現場の方であれば過程の振り返りに、これから使用を検討されている方にとっては一歩踏み出す参考となれば幸いです。