じぶん対策

日々学んだことをアウトプットして備忘録にしています。

OIDCを使用したクライアントシークレットなしでのソーシャルログイン実装について調査してみる

はじめに

最近OpenID Connect(OIDC)を使用したログインを実装するにあたって、implecit flowを用いる方法があることを教えてもらったので調査した結果をまとめてみます。

関連するRFCや信頼できそうな記事、理解しやすい記事を含めたいと思います。

参考: OIDCのImplicit FlowでClientSecretを使わずにID連携する

結論

  • OAuth2.0のimplicit flowはセキュリティリスクがある
  • OIDCのimplecit flowはOAuth2.0のimplicit flowと比較するとセキュリティリスクが少ない
  • ソーシャルログインを実現する場合、要件によってはOIDCのimplecit flowを使用することでクライアントシークレットの管理なしで実現できる
  • その場合はアクセストークンを発行してリソース取得エンドポイントを叩くのではなく、IDTokenに含まれる情報を使用する

OIDCとは

現代のウェブアプリケーションでは、ユーザーが他のサービスを通じて自身を認証する機能が実装されていることがあり、アカウント管理の煩雑さを解消し、開発者にとってはセキュリティのリスクや認証機能の実装の手間の削減を実現できます。このプロセスは「ソーシャルログイン」としてよく知られています。

OIDCはOAuth2.0という認可プロトコルを基盤とした認証の仕組みで、クライアントがエンドユーザーの同意を通じて、エンドユーザーの情報を安全に取得するための仕組みです。

OIDCのフロー

参考: OpenID Connect 全フロー解説

OIDCのフローは大きく以下のステップで構成されます。

  1. 認証認可リクエスト: クライアントはユーザーをOpenIDプロバイダー(例えばGoogle)の認可サーバーにリダイレクトします。この時、クライアントはリクエストにスコープopenidを含めます。

  2. ユーザー認証: ユーザーはOpenIDプロバイダーで自身のアイデンティティを認証します。成功すると、ユーザーはクライアントにリダイレクトされます。

  3. 認可レスポンス: リダイレクト時に、認可サーバーはcodeという認可コードをクライアントに渡します。

  4. トークンリクエスト: クライアントはこの認可コードを使い、認可サーバーにアクセストークンとIDトークンを要求します。

  5. トークンレスポンス: 認可サーバーはアクセストークンとIDトークンをクライアントに返します。

RFC6749に定義されているOAuth2.0の認可コードフローと同じフローになります。 このフローは、response_type=codeを指定した場合に実行されるフローとなります。 ただし、scopeopenidを含めた場合のみIDトークンが発行されます。

OIDCはIDトークンを発行するためのフローともいえると認識しているので基本的にはscopeopenidを含めることになると思います。

implecit flowについて

OAuth2.0にもimplecit flowが存在し、セキュリティ的なリスクがあって推奨されていないことは知っていたのですが、OIDCにもimplecit flowがあることを知らず、明確に違いを認識できていませんでした。 まずはOAuth2.0のimplecit flowについての復習から始めます。

OAuth2.0のimplecit flow

参考: OAuth 2.0 全フローの図解と動画

認可コードフローとは違い、認可エンドポイントにリクエストを投げ、応答として直接アクセストークンを受け取るフローです。

元々はJavaScriptを用いたブラウザベースのクライアント向けに設計されました。

フローの流れとしては以下のようになります。

  1. ユーザーがクライアントを認可すると、クライアントはリダイレクトURIにアクセストークンを含むリダイレクトレスポンスを受け取ります。
  2. クライアントはリダイレクトレスポンスからアクセストークンを抽出し、そのトークンを使用してリソースサーバーからリソースを取得します。

参考: OAuth 2.0 Implicit Grant Flow

ただ、このフローは設計上の脆弱性を持っています。 この脆弱性については以下の記事が詳しいです。

参考: OAuth 2.0 Implicit Flowをユーザー認証に利用する際のリスクと対策方法について #idcon

参考: 「単なるOAUTH 2.0を認証に使うと、車が通れるほどのどでかいセキュリティー・ホールができる」について

なお、RFCにおいても現在のセキュリティベストプラクティスをまとめた文書が発表されていて、原則認可コードフローの利用を推奨しています。また、PKCEを組み合わせて使用することが推奨されています。

OIDCのimplecit flow

OpenID Connectのimplecit flowはOAuth2.0のimplecit flowをベースにしています。そのため、同様のセキュリティリスクを抱えています。

ただし、OIDCのimplecit flowはOAuth2.0のimplecit flowとは異なり、IDトークンを発行するフローであり、このIDトークンにデジタル署名が含まれているため、クライアントはトークンが信頼できる発行者から発行され、改竄されていないことを確認できます。

ただし、OAuth 2.0のimplecit flowと同様に、IDトークンが直接ブラウザに渡されるため、トークンがブラウザの履歴やログ、HTTPリファラに記録される可能性は残っていますが、response_type=id_tokenを指定した場合はアクセストークンではなくID Tokenのみが記録されるため、OAuth 2.0のimplecit flowの問題に該当しないと思っています。

ただし、OIDCのセキュリティの利点を活用するためにはIDトークンの署名を検証することが大切です。

この署名の検討がOAuth2.0と大きく異なるセキュリティにおける重要なポイントです。

IDトークンについて

参考: IDトークンが分かれば OpenID Connect が分かる

参考: OpenID Connect Core 1.0 incorporating errata set 1

参考: JSON Web Token (JWT)

IDトークンはJWT形式で発行されます。このJWTには以下のようなクレーム(名前と値のペア)が含まれます。

クレーム 説明
iss トークンの発行者。これはトークンが誰から発行されたかを識別するためのもの。
sub 主題(Subject)。これはトークンが誰についてのものであるかを識別するためのもので、通常はユーザーの一意の識別子。
aud オーディエンス(Audience)。トークンの受け取り手を指定します。トークンはこのオーディエンスに対してのみ有効。
exp 有効期限(Expiration)。トークンの有効期限(UNIX時間)
iat 発行時刻(Issued At)。トークンが発行時刻(UNIX時間)
auth_time 認証時刻(Auth Time)。ユーザーが最後に認証された時刻(UNIX時間)
nonce リプレイ攻撃を防止するための文字列。リクエスト時にクライアントが送信し、IDトークンの発行時にそのまま返される。
acr 認証コンテクストクラス参照(Authentication Context Class Reference)。ユーザーの認証がどのレベルで行われたかを示すもの。
amr 認証方法参照(Authentication Methods References)。ユーザー認証に使用されたメソッド
azp 承認済みパーティ(Authorized party)。トークンが発行されたクライアント

これらはあくまでOIDCで定義された標準的なクレームなので、IDトークンはこれらに加えてカスタムクレームを持つこともできます。

ソーシャルログインの実装方針

今回の記事の目的であるクライアントシークレットの管理なしでログイン機能を実施するには、先述したIDトークンに含まれる情報を使用します。

IDトークンに含まれる情報は、ユーザーの識別子やメールアドレスなどの情報が含まれているため、これを使用してユーザーを識別することができます。

ログインのみの実装であれば発行者、ユーザーの識別子、クライアントの情報があれば実装できるため、クライアントシークレットの管理なしで実装できます。

まとめ

  • OAuth2.0のimplicit flowはセキュリティリスクがある
  • OIDCのimplecit flowはOAuth2.0のimplicit flowと比較するとアクセストークンではなくIDトークンが発行され、トークン自体の検証ができ、セキュリティリスクが少ない
  • ソーシャルログインを実現する場合、要件によってはOIDCのimplecit flowを使用することでクライアントシークレットの管理なしで実現できる
  • その場合はアクセストークンを発行してリソース取得エンドポイントを叩くのではなく、IDTokenに含まれる情報を使用する

所感

以前記事にしたりしてわかった気になっていた認証認可周りについて改めて調査しなおすいい機会になりました。 ただ闇雲に仕様を追うことだけでなく、要件を意識することの大切さを改めて感じた次第です。 こうした調査を通して、自分の知識の不足を感じることが多々ありますが、できる限り信頼のおける情報元を探し、こうしてアウトプットしていくことをこれからも継続していきたいと思います!

「勉強法の勉強会」参加レポと個人的な所感まとめ

はじめに

久しぶりに社外の勉強会イベントに視聴者として参加してきました!
今回参加したのはゆめみ x とらラボ!主催の「勉強法の勉強会」です。
勉強会ってこんなにメモ取るのが忙しかったっけ?と感じるくらい自分に刺さる濃い勉強会だったのでその参加レポと個人的な所感や活用法をまとめてみたいと思います。

今回はLTの中でも特に印象に残っている以下の4つの発表についてまとめていきます。
他の発表も大変参考になったんですが、まとめる時間の関係上より自分の状況に当てはまるものを中心にまとめています。

  • ChatGPT時代の勉強法
  • 内需ドリブン勉強法
  • 技術書を集中して読むために新たに始めた方法が自分にクリティカルヒットした話
  • AWSの勉強法、3つの鍵

ChatGPT時代の勉強法

前提

  • ChatGPTはデータベースであり、データベースは結論を出せない
  • 責任も取れない

今後取り組んでいくべき勉強の内容について

  • 丸暗記の勉強はますます役に立たない
  • 判断、行動することがより重要視され、それらのために勉強が必要
  • 生涯勉強していく必要があり、勉強のサイクルはChatGPTによって加速する
  • ChatGPTによって学習のパラダイムが変わる
  • 今後はより考える力を養うのが重要
  • ChatGPTによってより自分に最適化された勉強ができる

Tips

  • 説明の途中でわからない単語が出てきたらそれを説明してもらう
  • 理解したかどうか不安なときは、自分の理解を述べて正しいかどうか聞いてみる
  • 特定のフォーマットで回答してもらう(表とかJSONとか)
  • 勉強した内容についてクイズを出してもらう

所感

ChatGPTについては言わずもがなの内容ではあるが、今後より価値のある勉強の内容は同意できた。
また、「無知の知」を知ることに活用できるというのも重要そうだなとおもった。
個人的には検索エンジンの台頭のときと同じ流れなのでは?と感じていて、暗記能力ではなく、判断力、行動力や回答を精査する力をつけるために勉強が大切になるというのは完全同意。
上記に加えて出力された情報が正しいかどうかを判断できる人がより学習サイクルを加速できるので、正しさを判断する方法をきちんと用意しておくべきではあると思う。例えば必ず公式ドキュメントと照らし合わせるとかを実施しないと、実際に試すしか正しさを検証できず、ここが個人的にChatGPTを勉強に使う上でハードルが高い。正しさを判断できる人はすでにその分野について詳しい気がする。
コード自体を書いてもらうほかにもコードレビューをしてもらったりできる(業務のコードでは避けたほうがいいと思うけど)

内需ドリブン勉強法

勉強には何より動機が大事

動機には外需と内需の二種類がある

外需

  • 仕事
  • 体系的かつ締め切りがある
  • 誰かが問いを与える

内需

  • 興味を満たしたい
  • 体系的でなくても文句は言われない
  • 締め切りがない
  • 問いは自分の中になる

強い興味、何かを作りたい欲の奴隷になる(素直に従う)ことが大切。
その動機に従って脱線することも新しい学びが得られる。
過程で今メジャーでない技術を選択したりすることが新しい引き出しに繋がる

質疑応答で気になったところ

  1. ついつい脱線し続けてしまう場合はどうするのか?
  2. 脱線した時のマイルストーンをアウトプットするのがいいのでは?

という回答をされていて、これはなるほどと思った。

所感

なにか作りたいものがあるエンジニアは強いみたいな話をよく聞いていたがより詳細に言語化されていて頷ける部分が多かった。
内発的な動機を持っている人とそうでない人は自分の周りを見ても分かれているので、自分の中に存在する内発的な動機を高めて逃さないように勉強することに取りくむとともに、内発的な動機を持っていない人はどうすればいいんだろう?という疑問も生じた。
「脱線のマイルストーンとしてアウトプットしていく」というのが個人的にかなりしっくりきていて、脱線を正当化できるし、後から役にたつことが多そうだなと感じた。
他の発表では外発的動機に着目した発表もあったので、対比が面白く、自分がどちらのタイプなのか考えてみるいい機会になった。

技術書を集中して読むために新たに始めた方法が自分にクリティカルヒットした話

課題

技術書を最後まで読めなくなった

解決法 ツイートしながら読む

  • 読後報告だけでなく実況中継的にツイートする
  • 頭の中で考えていることをアウトプットしながら読む
  • 気になったことや難しいキーワードを適当にツイートし垂れ流す
  • 後から見返せる
  • 普通に読むより集中して読める
  • 一日30分という時間設定をすることで集中力が高い状態で読める
  • 目で読んで、頭で考えてそれを書くことで深く読むことができる
  • 学校教育の授業も似たようなプロセスなので身体が最適化されているかもしれない

注意点

  • クローズドコミュニティの方がいいかもしれない
  • 少人数でやった方がいいかも
  • 共通体験になることで他の人がやっていることに触発されたり、再開するモチベになる
  • ハッシュタグはつけている

読書グループを作ることで読書習慣に間隔があっても再開しやすい

所感

今回の勉強会で一番試したい!と思ったのがこのLT。
技術書自体を読もうと買うことは多いが、考えながら読むことが増えたりして、読むのに時間がかかってしまうことも多く、習慣づけていくのが難しいと感じていた。
ツイートしながらの読書は考えていることを含めてアウトプットできるので後から見返したり、有識者からコメントをもらうこともできるのは結構いい点かも。

自分ならどうやって実現するかを考えてみると以下のようになった。

  • 社内slackのtimesチャンネルを使用する

これはクローズドなコミュニティで顔見知りが多い状態で行うためにハードルが低い方法。リアクションをもらいやすかったりもあるかも

  • 技術書ごとにスレッドにまとめていく

後から見返す時に検索しやすく工夫したいなと思っていて、本のタイトルごとにスレッドに書いていくのが良さそう。他の話題と混ざることも防げる。

AWSの勉強法、3つの鍵

AWSのような技術を勉強する場合には目的や目標を明確にするが大切

AWS学習の鍵

  1. 情報収集
  2. 検証実施
  3. 情報交換

情報収集

  • 情報を元に知識を得る
  • 知識がないと他の鍵も使えない
  • まずは一次情報を確認する
  • RSSの更新は毎朝確認してSlackなどに飛ばす(Whats new やAWS News Blogなど)
  • 公式動画コンテンツやEラーニングコンテンツもおすすめだが、有料のものも多い
  • 市販の書籍も利用できる(鮮度は落ちやすいが日本語で体系化された情報は 貴重)

検証実施

  • 自分の手元で実際に環境を用意して実際に動かしてみることができるのはクラウドの利点
  • わかりにくいところやつまづきやすそうなところこそが重要
  • 自分なりにユースケースを考えることが大事

  • 公式トレーニングやハンズオンのシナリオに従ってみる

  • 単発の機能確認ではなくやりたいことの流れが掴める
  • 手順通りにやるだけだと学びはすくないので操作している内容を意識することが大事

情報交換

各種ドキュメントでエラー情報や実際に動かしてみて得られた内容を発信したり意見交換、発信する

  • 社内wikiやteams/slackでメモでも検証結果でも発信していく
  • まずは社内でやってみる
  • いきなり大風呂敷を広げない
  • ほかのひとが書いたらいいねやスタンプ、コメントで賞賛を
  • ブログで対外的に発信する
  • みてくれる人が格段に多くなるので正確さを意識することになり、新たな情報や気づきが得られることもある
  • コミュニティ内で登壇などを通じて情報共有する
  • 敷居が高ければ同じ会社、同じ部署の同僚間での社内勉強会を開催する
  • インプットとアウトプットを意識することで学習効果は高まる
  • ブログにまとめるだけで未来の自分が助かるということもある

所感

AWSは個人的にも勉強したい分野ではあったが、個人でどのように勉強していけば掴めた気がする。
AWSを勉強する」のようなと漠然と学習を進めていくにはあまりにもAWSのサービスの種類は多いので、まずは興味のある分野に関連するサービスから勉強を進めていく必要がある。個人的にはコンテナ技術周りから手をつけていきたい。

まとめ

どの発表もかなり学びの多い発表でした。
発表を聞いて自分の中に落とし込む前にどんどん進んでいってしまうのでメモだけ取って後からまとめる形での記事となりました。
勉強する際の動機づけに関する発表から具体的なChatGPT、AWS関連の勉強法まで幅広く、かつ対象となるエンジニアの多そうなLT会でした。
特に自分は技術書駆動な部分はあるので、アウトプットを垂れ流しながら読むというのは実践してみたいと思います。

たまには社外のLT会に参加するのもいい刺激になりましたし、今後は社内の勉強会も盛り上げていきたいなと改めて感じました。

エラーハンドリングをクリーンアーキテクチャで書く場合に意識すること

はじめに

業務においてクリーンアーキテクチャを意識してコードを日々書いていますが、「これってクリーンアーキテクチャの考えに沿うためにはどう書けば良いんだろう」と悩むことがあります。
今回はエラーハンドリングについて、クリーンアーキテクチャに則って考えてみたいと思います。
あくまで筆者一個人のいわゆる「ぼくのかんがえたさいきょうのエラーハンドリング」なのでエラーハンドリング時の一意見として参考になれば幸いです。

astroで作成した個人ブログも公開したのでこちらにも同様の記事を投稿しています。

www.lyricrime.com

想定読者

  • Webのバックエンド開発においてクリーンアーキテクチャを意識してコードを書いているがエラーハンドリングに悩んでいる人
  • エラーハンドリングをするメリットを知りたい人

まとめ

  • 基本的には型で例外をハンドリングするべきなので自作型を作ろう
  • ドメイン層にHttpExceptionを書くな
  • ドメイン層はプレゼンテーション層を意識しないため、エラーメッセージをAction層でコントロールするといいかも

前提 クリーンアーキテクチャについて

詳細は以前の記事でも紹介したのでそちらを御覧ください。

今の私の理解だと、クリーンアーキテクチャとは、「ソフトウェアにおいてもっとも重要なドメインルールを中心に考え、ドメインルールに依存するように依存の方向性を制限した上で適切に責務を分割し、テスタブルで交換可能なソフトウェア構成を実現するための設計手法」という捉え方をしています。

今回はこの前提に沿ってエラーハンドリングについて考えていきます。

今回は例題として、ユーザーのCRUDについて考えていきます。

Entity(Entities)

まずはコアとなるドメインオブジェクト、Entityについてです。

今回はユーザーを例にするのでコアとなるエンティティはユーザーを表現します。

class User
{
    private UserId $userId;
    private UserName $userName;
    private UserEmail $userEmail;

    public function __construct(
        $userId,
        $userName,
        $userEmail
    )
    {
        $this->userId = $userId;
        $this->userName = $userName;
        $this->userEmail = $userEmail;
    }

    public function userId(): UserId
    {
        return $this->userId;
    }

    public function userName(): UserName
    {
        return $this->userName;
    }

    public function userEmail(): UserEmail
    {
        return $this->userEmail;
    }

    public function toArray(): array
    {
        return [
            'userId' => (string)$this->userId,
            'userName' => (string)$this->userName,
            'userEmail' => (string)$this->userEmail
        ];
    }
}

現状はエンティティの振る舞いとして特段思いつくエラーがないので次に進みます。

ValueObject(Entities)

次に表現するのはValueObjectです。

エンティティの例で出てきたUserIdUserNameなどのクラスを指します。

たとえば、UserNameの場合は以下のようなクラス定義になります。

class UserName
{
    public const MAX_LENGTH = 20;

    private string $value;

    public function __construct(string $value)
    {
        $this->validate($value);
        $this->value = $value;
    }

    public function validate(string $value): void
    {
        if ($value === '') {
            throw new InvalidArgumentException('UserName is required.');
        }

        if (mb_strlen($value) > self::MAX_LENGTH) {
            throw new InvalidArgumentException('UserName must be less than ');
        }
    }
}

上記のように、UserNameクラスは以下の2つのバリデーションルールを持ちます。

  1. コンストラクタで渡された値が空文字ではないこと
  2. コンストラクタで渡された値の文字列長が最大文字数未満であること

これらのルールに違反した場合はどちらもLaravelの組み込み例外クラスであるInvalidArgumentExceptionを投げます。
ただし、エラーメッセージはそれぞれのバリデーションルールに沿ったものを持ちます。

Service(UseCases)

次に考えるのはServiceです。

アプリケーションにおけるユースケースを表現するのがこのServiceになります。
コード例では基本的にDIしてプロパティを保持します。

DDDにおけるアプリケーションサービスと似たような責務を持ちます。

DDDにおけるアプリケーションサービスの例ではよくCRUDのメソッド等がここに記載されていたりしますが、単一責任の原則に従うためにすべて別クラスに分割すべきだと思っています。

今回はアプリケーションサービスとは違い、1ユースケースのみを表現したクラスを用意します。

このクラスでは使用しているEntity,ValueObjectで発生した例外をハンドリングする必要が出てきます。

ここでの解決策としてはServiceクラス特有の例外クラスを作成します。
自作例外クラスではなく共通で使用する例外クラスに引数にメッセージ等を渡して例外を生成してもいいと思いますが、個別にクラスを作ってしまうことでより表現力が増すことになります。

class CreateUser implements CreateUserInterface
{
    private UserRepositoryInterface $repository;
    private UserFactoryInterface $factory;

    public function __construct(
        UserRepositoryInterface $repository,
        UserFactoryInterface $factory
    )
    {
        $this->repository = $repository;
        $this->factory = $factory;
    }

    public function process(
        CreateUserInputPort $input,
        CreateUserOutputPort $output
    ): CreateUserOutputPort
    {
        try {
        $user = $this->factory(
            $input->name(),
            $input->email()
        );
            $repository->save($user);
        } catch(Throwable $e){
            throw new CreateUserFailedSaveException($e);
        }
        return $output->output($user);
    }
}

自作例外クラスでは、以下のようにエラーメッセージの生成や例外に関する情報を隠蔽します。

use RuntimeException;

class CreateUserFailedSaveException extends RuntimeException
{
    public function __construct(
        string $message = 'Failed to save when creating user.',
        int $code = 500,
        Throwable $previous = null
    ) {
        parent::__construct($message, $code, $previous);
    }
}

例外クラスを作成すればこのように、例外固有のメッセージやステータスコード等を持つことができます。

ここで、エラーメッセージはかっこよく英語で書いていますね(あとで困ることになりますのでActionの説明まで覚えておいてください)

CreateUserでは、InputPortとOutputPortは単純なDTOとして使用しています。

クリーンアーキテクチャにおいて必ずしもDTOであるとは限らないと考えているんですが、Web APIを返すバックエンドの実装においてはJSONを返すのでPresenterというよりはControllerが最終的に出力の役割を果たします。
本来の文脈ではController => ユーザーの入力をUseCases層に渡すPresenter => UseCases層からの出力を表示に適した形に加工するという役割だと思います。

InputPort,OutputPortはUseCases層なのでHTTP通信であることやJSONといったWeb特有の文脈に依存させないために、ただ入力と出力のデータを受け渡すためのDTOとして実装すべきだと考えています。

Web以外のアーキテクチャにおいてはOutputPortを用いてPresenterが出力の責務(および出力のためのデータ加工)を全うすることになるのかなと思います(このパターンの実装経験が無いので想像になりますが、例えばコンソールアプリケーションのように標準出力を用いる場合はWebアプリケーションと異なる形になると思います)。

Factoryについては下記の様な実装になるかと思います。

use Symfony\Component\Uid\Ulid;

/**
 * Factoryパターンの実装です。
 *
 * エンティティを生成する流れを隠蔽します。
 * 今回の場合はIDの採番に関するロジックを隠蔽します。
 */
class UserFactory implements UserFactoryInterface
{
    public function createUser(
        UserName $userName,
        UserEmail $userEmail
    ): User
    {
        return new User(
            Ulid::generate(), // ここではSymfonyを用いてULIDを生成することを想定してます。
            $userName,
            $userEmail
        );
    }
}

Repositoryは永続化を隠蔽しますが今回は省略します。

Action(Controllers) ~ json形式でレスポンスを返すまで

Webのバックエンドにおいて、リクエストの内容をServiceに受け渡す役割を担うのがこのActionになります。
入力を受け取るので、HTTP通信であることはこの層で初めて意識することになると思います。
そのため、この層ではHTTPExceptionとして例外を扱い、最終的にHTTPエラーレスポンスとして返します。
レスポンスの形式についてはRFC7807で定義されているのでこれに沿う形のJSONを返してあげるのがベストプラクティスかなと思います。

実装例を書いてみます。

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Response;
use InvalidArgumentException;

class CreateUserAction
{
    private CreateUserInterface $createUser;

    public function __construct(
        CreateUserInterface $createUser
    ) {
        $this->createUser = $createUser;
    }

    public function __invoke(CreateUserRequest $request)
    {
        try {
            try {
                $input = new CreateUserInput(
                    new UserName($request->name()),
                    new UserEmail($request->email())
                );
            } catch (InvalidArgumentException $e){
                throw new BadRequestException($e->getMessage());
            }

            DB::transaction();

            try {
                $createdUser = $this->createAdminUser->process($input);
                DB::commit();
            } catch (CreateUserFailedSaveException){
                DB::rollback();
                throw new InternalServerErrorException($e->getMessage());
            }
        } catch (BadRequestException $e){
            // JSONレスポンスの作成
            return Response::json([
                'title' => $e->title(),
                'status' => $e->code()
                'detail' => $e->detail(),
                ]);
        } catch (InternalServerErrorException){
            // Laravelの場合は500系エラーをHandler等で共通して処理したり記録したりすることができる
            throw Response::json([
                'title' => $e->title(),
                'status' => $e->code()
                'detail' => $e->detail(),
            ]);
        }
        return Response::json($createdUser->toArray());
    }
}

Action層より内側で発生した例外はエラーメッセージを持った状態でcatchできるので例えば、下記のように記載することもできます。

try{
// 処理内容
} catch (CreateUserFailedSaveException){
    throw new InternalServerErrorException($e->getMessage());
}

ですが、この方法では内部で発生した例外メッセージ(今回は英語で記載)をそのままJSONとして返したくない場合に対応できません。
内部のエラーメッセージを変更することは、内部実装がプレゼンテーションのための知識を持つことになるため避けたい、でも例外によってエラーメッセージを分けて表示したいといったケースです。

最初の例のように、エラーメッセージの内容をActionで決めてしまうのが現状良いかなと考えています。

また、全く同じことが正常系の場合にも言えます。

return Response::json($createdUser->toArray());

この部分ではエンティティのtoArrayメソッドの実装によって出力の形式が変わってしまいます。もちろん内容が変更された場合は出力も変更されるべきなのですが、ドメインロジックの配列に格納される順番に出力が依存してしまっているため切り離したいところです。

これらの問題は、クリーンアーキテクチャにおいては依存の方向性が内側に向いているため明確な違反ではないと考えていますが、切り離したほうがより綺麗な気がします。

改めてクリーンアーキテクチャの図を見てみると、Presentersという層が存在します。

先程のコード例では、Webという仕組みを意識した上でActionControllers = 入力という役割とPresenters = 出力という役割を持っている状態になります。

より丁寧に書くのであれば、Presenterとなる層を用意するといいかと考えています。

HTTPのための例外であるBadRequestExceptionInternalServerErrorExceptionから生成していた処理や、エンティティが持っていたtoArray()といったメソッドをPresenterに実装する事ができます。

まずは例外処理の方から考えてみます。

throw Response::json([
    'title' => $e->title(),
    'status' => $e->status()
    'detail' => $e->getMessage(),
]);

だと例外のメッセージがそのまま外に漏洩するので

throw Response::json([
    'title' => $e->title(),
    'status' => $e->status()
    'detail' => '任意のメッセージ',
]);

のようになります。ここで、タイトルやステータスは例外クラスごとに決まるはずなのでHTTPExceptionのインスタンスを生成するタイミングでdetailを渡してしまえばいいと思います。

try {
    $input = new CreateUserInput(
        new UserName($request->name()),
        new UserEmail($request->email())
    );
} catch (InvalidArgumentException $e){
    throw new BadRequestException('不正なリクエストです.');
}

HTTP用の例外クラスの実装例

use RuntimeException;
use Throwable;

class BadRequestException extends RuntimeException implements HttpExceptionInterface
{
    private string $detail;
    private string $title;
    private int $status;
    private Throwable $previous;

    public function __construct(
        string $detail,
        string $title = 'Bad Request',
        int $status = 400,
        Throwable $previous
    ){
        parent::__construct($title, $status, $previous);
        $this->detail = $detail;
        $this->title = $title;
        $this->status = $status;
        $this->previous = $previous;
    }

    public function title(): string
    {
        return $this->title;
    }

    public function status(): int
    {
        return $this->status;
    }

    public function detail(): string
    {
        return $this->detail;
    }
}

例外クラスから最終的にjsonに変換する必要があるのでそのためにpresenterを使用します。もともとの例ではファサードを使用していて十分スッキリ書けてはいるので責務の分離が主目的になります。

use Illuminate\Support\Facades\Response;

class ExceptionPresenter
{
    public static function makeJsonResponse(HttpExceptionInterface $e): JsonResponse
    {
        return Response::json([
            'title' => $e->title(),
            'status' => $e->code()
            'detail' => $e->detail(),
        ]);
    }
}

エンティティのtoArray()がそのまま外に漏洩することを防ぐためにpresenterを使用する場合も同様です。

use Illuminate\Support\Facades\Response;

class UserPresenter
{
    public static function makeJsonResponse(User $user); JsonResponse
    {
        return Response::json([
            'userId' => (string)$user->userId,
            'userName' => (string)$user->userName,
            'userEmail' => (string)$user->userEmail
        ]);
    }
}

以上を踏まえて最終的なActionは以下のような形になります。
ここまでやればクリーンアーキテクチャのかなりの部分をコードで表現できたんじゃないでしょうか。

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Response;
use InvalidArgumentException;

class CreateUserAction
{
    private CreateUserInterface $createUser;

    public function __construct(
        CreateUserInterface $createUser
    ) {
        $this->createUser = $createUser;
    }

    public function __invoke(CreateUserRequest $request)
    {
        try {
            try {
                $input = new CreateUserInput(
                    new UserName($request->name()),
                    new UserEmail($request->email())
                );
            } catch (InvalidArgumentException $e){
                throw new BadRequestException('不正なリクエストです.');
            }

            DB::transaction();

            try {
                $createdUser = $this->createAdminUser->process($input);
                DB::commit();
            } catch (CreateUserFailedSaveException){
                DB::rollback();
                throw new InternalServerErrorException('サーバーエラーが発生しました.');
            }
        } catch (BadRequestException $e){
            // JSONレスポンスの作成
            return ExceptionPresenter::makeJsonResponse($e);
        } catch (InternalServerErrorException){
            // Laravelの場合は500系エラーをHandler等で共通して処理したり記録したりすることができる
            throw ExceptionPresenter::makeJsonResponse($e);
        }
        return UserPresenter::makeJsonResponse($createdUser);
    }
}

バリデーションについて

Webアプリケーションを構築する際に、フレームワークで投げられる例外から、クリーンアーキテクチャドメイン駆動設計に基づいて分割された様々なオブジェクトから例外がスローされます。

これについての私の意見は以下のブログの内容とほぼ同じであるため、説明を譲ります。

https://ikenox.info/blog/validation-in-clean-arch/

それぞれの層が、それぞれの関心事、責務に基づいた例外をスローし、最終的に今回記事にしたような形でレスポンスに変換できれば良いと思っています。

まとめ

  • 基本的には型で例外をハンドリングするべきなので自作型を作ろう

型にしてしまうことで例外特有の情報をカプセル化する事ができます。PHPにおいてはtry...catchの際に型を使用できるので更に扱いやすくなります。

ドメイン層で投げられる例外はHTTPのことを意識するべきではないので、HttpExceptionはドメイン層に書かないようにすべきです。きちんと区別して使い分けましょう

  • ドメイン層はプレゼンテーション層を意識しないため、エラーメッセージをAction層でコントロールするといいかも

いわゆるPresenterと呼ばれるクラスや処理を用意してAction層でそれらの処理を呼び出してコントロールするとドメイン層の情報をそのまま外に漏らすことがなくなります。 ただ、この点についてはドメイン層に依存してもクリーンアーキテクチャの本質である依存の方向性を内側に向けるというルールに反しているわけではないので責務の分離のための自己満足の面も多分に含んでいます

所感

普段業務でクリーンアーキテクチャを意識しながらコードを書いていても実装するたびに新しい疑問が湧いてきてどうすれば良いんだろう?と手が止まってしまうことが多々あります。
今回はエラーハンドリングに関しての現時点の考えをまとめることができたので自身の考えの変化をwatchしてみたいと思います。
今後もフレームワークのキャッチアップや手を動かして何かを作っていく中で自分なりに考えがまとまったら定期的にアウトプットしたいと思います。

ToDoリスト作成の振りかえり

はじめに

WEBエンジニアになってすぐに作り始めたリポジトリがある程度まで動作するようになったので今回はそこから得られた学びをまとめておきたいと思います。
以前も少し触れましたが、このブログを書き始めて一年間がたちました。かなり気合を入れて毎週記事を書いてきたのですが、そろそろアウトプットを中心とした記事も混ぜていこうかなと思います。
例えば今回は、技術についてだけでなく、いろいろ自分で手を動かしてみて感じたことや反省点をまとめてみます。

作ったもの

LaravelとNuxt2を使用して簡単なToDoアプリを作成しました。 Trello風にカードの追加、更新、削除や移動ができるようになっています。

難しかったところ

作成するにあたってつまづいたところをあげてみます。

技術選定

そもそも作ろうと思った時点では入社してすぐだったため、完成イメージこそありましたが完成までの道筋は一切見えていませんでした。
知識ゼロからの取り組みのため、とりあえず業務で触る機会のあったLaravel + Vueの構成を選択しました。

環境構築

まずつまづいたのは環境構築でした。
LaravelのインストールとVueのインストールという最低限の構成で特にLinterや静的解析の類のツールは導入していませんでした.
開発環境自体は見よう見まねでDockerでの環境構築をしましたが度々ハマることがありました。
今思えばこの時はコンテナ自体への理解が明らかに足りておらずコンテナによるメリットも一切わからないままDocker環境で開発してました()。
最初にDockerのチュートリアル周りをしっかり読み込めていればなと今になって思いますが当時はクリーンアーキテクチャやDDDがどういうものか考えることに必死で手が回っていなかったと思います。
というかWEBエンジニアになってかなり早い段階でクリーンアーキテクチャを意識したのは結構頑張っていたような気もします。

デザイン

業務での開発と違って大きくつまづいたのはデザイン面でした。
業務ではデザイナーさんがある程度デザインを固めてくれますが個人開発ではそうはいかないので、いろんなことを試しました。
大きく分けるとUI/UX面と見た目に分かれます。

  • UI/UX面

ほかのひとが作った個人開発ToDoアプリを触ってみる
既存のSaasアプリを参考にする

  • 見た目

カラーピッカーで適当に色を選択して作ってみるも色がどぎつい感じがして微妙.
デザイン用のWEBサイトなどのカラーパレットから良さげなのを選んだ.

設計

作り始めた当初は設計が全然わからない状態でした。
業務で設計について学ぶにつれて都度書き直していました。
業務で学んだことの復習や予習の材料としてかなり役立ったと思います。
クリーンアーキテクチャ、DDDを自分なりに噛み砕いて好きにコードを書くことで自分の解釈がかなり明確に言語化できるようになりました。

反省点

  • バックエンドから書き始め、とりあえずでCRUDを作成したため、フロントが欲しいデータと一致しなかった
    WEBエンジニアになってまず担当したタスクがバックエンドのものが多く、まずは簡単なCRUD機能をクリーンアーキテクチャを意識しながら作ることから着手しました。
    なんとなく掴めてきた段階でフロントエンドに着手したのですが、バックエンドから返すデータの形とフロントが使いたいデータの形とのギャップで苦労することが多かったです。
    RESTfulなAPIをどこまで保ち、どういうふうにフロントに表示するかを初めから想像できていればもう少し早く作れたという学びを得られました。

  • 自身の成長速度に合わせて開発できなかった WEBエンジニアになって最初の一年ということもあって、書けば書くだけ成長できた気がしています。が、それに合わせてTODOリストを開発できなかったのでみるたびに直したくなる現象が発生してしまいました。
    数ヶ月かけてバックエンドのCRUDをひたすら直し続ける作業をしていたので何かを作るという観点では反省点かなと思いますが、勉強にはなりました。

  • 定期的な作業時間を確保できなかった 個人開発における一番の障害は時間の確保でした。
    週一ペースでのブログ記事の執筆に力を入れていたのもあって開発に割ける時間が取りにくかったです。
    週何時間のように定期的な時間の確保をすることと、何かを作りたい時は企画から開発終了までスピード感を持って取り組むとスイッチングコストやモチベーションといったコストをかなり抑えることができそうだなと思っています。

  • 開発環境の整備が微妙 フレームワークの導入自体はできていたのですが、静的解析やLinterといった自動ツールは早い段階で導入しておくとより開発効率を上げられたかなと思いました。
    また、先述したようにかなり時間をかけてしまったので、久々にIDEを開いたときに何をしようとしているのか忘れてしまうことも多々ありました。
    プロトタイプを一気に作り上げた上で、issue管理していくのが現状解決策かなと思っています。

これから

いくつか改善したい仕様を修正しつつ、勉強のための土台としてこのリポジトリを引き続きメンテナンスしていこうと思います。

  • Nuxt3へのアップデート
  • PHP8.2、およびLaravel10へのアップデート

所感

手を自分で動かすことで得られる知見は非常に大きかったです。特に業務においてはたいてい整備されていることの多い開発環境周りの知識や、技術選定、開発のためのツール類についてもこれからより考えてみたいと思います。

手を動かした系のブログどう書けばいいのかあまりわかってないんですが、コードこれからはインプットよりアウトプットを中心に学習していきたいと思います。

ブログを書き始めて一年が経った話

はじめに

今回は具体的な技術の内容ではなく、本ブログについて思うところをまとめたいと思います。
週に一本のペースで欠かすことなく、またそれなりの分量も記事を一年間継続して書けたかなと思います。
継続のために弊社スマレジのブログ手当の存在は結構大きかったです。

まとめ

  • とりあえずアウトプットしよう
  • 質はアウトプットしてから考えよう

技術発信のジレンマ

はじめに伝えたいのはとにかくアウトプット量を増やしたほうがいいということです。
何かしら書かない言い訳をすることは簡単なんですがアウトプットして発信しないとスタート地点ですらないと思ってます。
まずは自分自身のためにアウトプットを継続し、リアクションに期待しないことが継続のコツだと思っています。(もちろんリアクションがあれば嬉しいですし励みになるのであるに越したことはないですが)
アウトプットをする際に考えられる心理的な障害を考えてみます。

当たり前のことや業界の常識をわざわざブログ記事にする必要がない

自分の記事を見返すと公式ドキュメントの要約になってしまってるなあと感じることが多々あります。
ですが価値のない技術記事はほとんど存在しないと思っています。書いた人の視点が入った記事ならどんな記事であっても誰かにとっては参考になる記事です。
また、大事なことは誰が何回書いてもいいと思っていますし、判断は書く側ではなく読む側がします。
アウトプットする前から悩むよりは世の中に出してしまって読み手に委ねるほうが世の中のためになります。
読み手は本当に信頼のおける情報が欲しければ公式ドキュメントを見ると思いますし、信頼性に欠けていても多角的な視点やtipsを得るために個人ブログを読むと思います。

そもそも正しい情報かどうかわからない

特に私のような歴の浅いエンジニアの場合はインプットの内容が正しいかどうか不安になることがあります。そこで対策しているのが以下の点です。

  • 局所的に書く
  • 三者に指摘をもらう
  • 信頼できる情報源を活用する

局所的に書く、というのは 私の環境の〇〇といったケースの場合、のように自身の環境や体験、詰まった問題をベースに書くことで事実を記載するようにする、という点です。

第三者に指摘をもらう、というのが個人だと難しいのですが同僚や友人に確認してもらって意見をもらうだけでかなり正確さが向上すると思います。

信頼できる情報源を活用するというのは原則公式ドキュメントを参照したり、信頼できる情報を参考にすることを意識しています。

どうすれば読まれる質のいい記事になるかわからない

個人的に歴が浅く、技術発信に慣れていない時期なので質より量にこだわった一年間でした。
ひたすら記事執筆の練習と思い分量を書くことと、記事執筆のためにいろんな情報源を読み込んでいくことで自身の技術力の向上を目的としていました。

この一年間の成長は自分としても満足できるもので、その基盤にはやはりこのブログの存在が大きかったと思います。

自分がブログ執筆によって得たかったメリット

自分自身のスキル向上

何より初めに得られるメリットは自分自身のスキルの向上です。
書いた瞬間から、いや、書こうとしたその時から得られるメリットだと思っています。
プログラミングの世界ではラバーダッキングと呼ばれるデバッグ手法が知られています。
言語化するとそれだけで理解が深まります。また、普段から細かく言語化する訓練を積んでおくことで、業務においても技術に対する解像度が上がり、正しい判断に繋がりやすくなります。
エンジニアは総じて人に何かを説明する機会の多い職種だと私は思っていて、言語化スキルが高ければコミュニケーションによるロスを低減し仕事がしやすくなると思っています。

より深いインプット

記事を書くには初めのうちは少なくともアウトプットより多くのインプットを行なってそこから情報の正確性に関する判断等を行なって取捨選択や要約をする必要があります。
初学者なのでまずは普遍的なソフトウェアエンジニアリングに関する知識を中心にインプットするようにしました。
具体的なフレームワーク等の技術スタック(LaravelやVue)に飛びつかず、基礎的な考え方や技術を勉強することを優先しました。
これは戦略的に行ったわけではなく、言語の入門本を買ってすぐに内容が簡単すぎて読まなくなり後悔した経験から得られた教訓なので技術書を買うことは内容以外でも学びがあるかなと思います。

このブログのこれから

振り返ってみるとスマレジに入社し、このブログを書き始めてからは業務終了後もかなりの時間デスクに向かうようになりました。
今となっては習慣になっていて苦ではないですがやっぱり慣れないうちは大変だった記憶があります。
ひたすら量を意識した一年間だったのでこれからは質も意識していきたいと思います。

下記に参考にあげている記事の中で、uhyoさんが技術記事を書くためのステップには下記のステップがあると書かれています。

  1. 練習期
  2. 信頼向上期
  3. 思想発信期

このブログにおいても一年間練習はできたと思うので、次は信頼の向上を目指したいと思います。
わかりやすさと正確性を意識したうえで、できる限り自分の考察や思想を盛り込めるような記事を書いたり、より細かな実践的なTips等も書いていこうと思いました。

公式ドキュメントを見ればわかる当たり前の内容だから書かない、単純なtipsすぎて文量が少ないから書かないといった事態に陥ることを避けて継続していこうと思います。

参考

https://techplay.jp/column/1609

余談

この一年間はブログのための時間以外にも環境作りのために結構投資した一年でした。

  • 椅子
  • マウス
  • キーボード
  • ウルトラワイドモニタ
  • 技術書15冊くらい
  • 時間は貴重だから自炊してる暇ないと自分に言い聞かせた外食費
  • 開発用のプライベートなPCのスペックが心許ないので購入したmac mini

ざっと目につくものでこれくらい買ってるんですが業務にもブログにも役に立っているので元は取れていると信じてます。

TypeScriptのジェネリクスについて

はじめに

今回はTypeScriptのジェネリクスについてです。JavaScriptがある程度かければTypeScriptでつまづく箇所は結構少ないとは思いますが、つまづいたのがジェネリクスという概念でした。
今回はジェネリクスの仕組みと何を解決できるのかについて調査してみます。

ジェネリクスとは

型の安全性とコードの共通化を両立するための仕組み

抽象的な型引数を使用して、実際に利用されるまで型が確定しないクラス関数インターフェースを実現するために使われる。

参考サイトの例をみてみます。

中身のロジックが同じで引数の型が異なる関数が以下のように三つあります。

function chooseRandomlyString(v1: string, v2: string): string {
  return Math.random() <= 0.5 ? v1 : v2;
}
function chooseRandomlyNumber(v1: number, v2: number): number {
  return Math.random() <= 0.5 ? v1 : v2;
}
function chooseRandomlyURL(v1: URL, v2: URL): URL {
  return Math.random() <= 0.5 ? v1 : v2;
}

これらを共通化するにはどうすればいいでしょうか。
解決策の1つはany型を使用して型チェックを放棄することですがせっかくTypeScriptを使う以上避けたいです。

ここで登場するのがジェネリクスで、下記のように書けます。

function chooseRandomly<T>(v1: T, v2: T): T {
  return Math.random() <= 0.5 ? v1 : v2;
}
chooseRandomly<string>("勝ち", "負け");
chooseRandomly<number>(1, 2);
chooseRandomly<URL>(urlA, urlB);

ここで、Tには任意の型変数名が入ります。つまり、なんでも構わない名前です。慣習的にTypeTが使用されます。

function chooseRandomly<T>(v1: T, v2: T): T {
  return Math.random() <= 0.5 ? v1 : v2;
}
let str = chooseRandomly<string>(0, 1);
// エラー
Argument of type 'number' is not assignable to parameter of type 'string'.
str = str.toLowerCase();

この例の場合、引数の型によって返り値の型が決まるため、上記のような型のエラーに気づくことができます。

このように型の安全性と汎用性を共存させることができます。

わからないこと

ジェネリクスがここまでの例で解決することを理解はできましたが、オーバーロードやユニオン型でも解決できそうな気がします。

例を再度考えてみます。

ジェネリクスを使うことで型の種類を問わず引数のように使用する側で渡すことができる点では汎用性は高そうです。

function chooseRandomly<T>(v1: T, v2: T): T {
  return Math.random() <= 0.5 ? v1 : v2;
}

ジェネリクス型推論

ジェネリクスを使用すると型推論されます。

function hoge<T>(x: T) {
    alert(x instanceof Date);
}
hoge<string>("new Date()"); // false
hoge<Date>(new Date()); // true

このように、ジェネリクスを使って型推論を利用することができ、さらに型の指定はコンパイラに推論可能であれば型名を省略できます。

function hoge<T>(x: T) {
    alert(x instanceof Date);
}
hoge("new Date()"); // false
hoge(new Date()); // true

問題点: 型のメソッドを使えない

ここまでの説明だと型自体を決定することはできるんですが、実際に渡ってくる引数の型は呼び出されるまで分かりません。
そのために型に属する機能をなにも呼ぶことができない状態になります。
この問題を解決するためにはジェネリクスを下記のように使用します。
そしてこの使い方こそがジェネリクスの真価を発揮する方法だと思っています。

解決策: ジェネリクスの制約

interface X {
  sayMyName();
}
 
class Y implements X {
  public sayMyName() {
    alert("I'm Big-Boy");
  }
}
 
function a<T>(t: T) {
  t.sayMyName();
}
 
a(new Y());

上記のコードはコンパイルできません。理由はTという型がコンパイル時には未知の型であるためです。
この問題を解決するためには型引数T自体に制約を追加します。

function a<T extends X>(t: T) {
    t.sayMyName();
}

<T extends X>で型XもしくはXを継承した型のみを型引数にとるということを表現します。

ちなみに、下記のように利用したいメンバーだけを強制することもできます。

function a<T extends { sayMyName(); }>(t: T) {
    t.sayMyName();
}

この使い方はTypeScriptのダックタイピング的な動作を利用しています。

ダックタイピング

個人的にこれまで静的型付け言語に触れることが多く、プロダクトで使用しているPHPも現在はほぼC#のような静的型付けに近い運用をしているためダックタイピングという考え方に馴染みがないのでまとめておきます。

コンパイル時に型の検査をするのではなく、実行時に実行するという方法です。
オブジェクトに何ができるかはクラスではなく実行時のオブジェクトそのものが決定するという考え方です。

"If it walks like a duck and quacks like a duck, it must be a duck" (もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない)

言い換えれば、同じ振る舞いを持つものは共通のインターフェースを持つとも言えます。
インターフェースの判定に継承は関係なく、必要なインターフェースを持っているかどうかのみに着目します。

ここ最近Ruby on Railsに触れる機会があるのですが、Rubyのばあいは一般的なクラス継承もできるので継承によるポリモーフィズムも利用できます。が、ダックタイピングを使えば継承が不要であり型による制約に縛られることなく簡素なコードで実現ができます。
ただ、動的型付け言語でダックタイピングは乱用されることがあり、ここ最近では静的型付け言語に注目が集まっているのでメリットとデメリットを理解することが必要です。

まとめ

  • ジェネリクスを使用することで引数の型が決まっていない処理を共通化できる
  • コレクション等でメリットが大きい
  • ある程度の型推論にも使えるが過信はできない
  • 振る舞いを利用する場合は型引数自体にextendsを使って制限をかける必要がある

参考

https://qiita.com/k-penguin-sato/items/9baa959e8919157afcd4
https://typescriptbook.jp/reference/generics
https://www.buildinsider.net/language/tsgeneric/01
https://ja.wikipedia.org/wiki/%E3%83%80%E3%83%83%E3%82%AF%E3%83%BB%E3%82%BF%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0

「チームで働くエンジニアの心構え」について新人だった頃から意識してきたことを言語化してみる

はじめに

今回は書籍「Googleのソフトウェアエンジニアリング ―持続可能なプログラミングを支える技術、文化、プロセス」を読んでエンジニアになってチームで働くにあたって意識してきたことを自分なりに振り返ってみようと思います。
個人的にはエンジニア、特に初心者エンジニアの成長速度には技術的な部分より心構えや姿勢によるところが大きいと考えています。
書評ではなく一部引用しながら自分の考えをまとめますので書籍自体の意図とズレる場合がありますがご了承ください。
参考書籍は全25章からなりますが、今回は2章の「チームでうまく仕事をするには」を中心にまとめます。

想定読者

  • 会社員としてWEBエンジニアになりたての人
  • エンジニアとして周囲との実力差に悩んでいる人

筆者の略歴

組み込み派遣エンジニア約2年->(現在)WEBエンジニア1年

現職に採用された時点ではWEBの知識はゼロ。前職では主に単体テストフェーズを中心に担当しており開発経験自体も浅めという状態でした。
チームメンバーには同時期に入社したメンバーもいましたが前職もWEB系だったりして、周りとの実力差のギャップがある状態でした。

チームで仕事をするということ

参考書籍2章「チームでうまく仕事をするには」の内容です。

ソフトウェアエンジニアリングとはチームによる取り組みである

天才プログラマーが一人で何かを成し遂げるというのは幻想であり、どんなに著名なエンジニアでも一人きりで何かを成しているわけではなく、周囲の協力があって偉大な功績を残しているという内容です。

これこそがOSSという文化が存在し、エンジニアがやれスクラムアジャイルだと議論する根底にある考えだと思っています。

上記の考えに従うとソフトウェアエンジニアリングは基本的にチームによる生産活動です。加えてここ最近意識して行動しているのはエンジニアのチームプロスポーツチームだと思って行動しています。
どういうことかというと、自分たちのチームをサッカーでいうプレミアリーグのチームだと思って行動しています。(サッカー詳しくないのであんまりわかってないんですがバスケでいうとNBAで、特にサンアントニオスパーズのイメージです。伝わってくれ)

筆者としてはここ半年ほどチームリーダー的な役割を担当していますが、選手監督のような立ち位置だと思っています。

具体的にはなにか判断する際に一度立ち止まって「これ、一流スポーツチームでも同じことをするかな」と考えるようにします。

また、組織に対して貢献できているかどうか、を判断基準においています。

例えば以下のような人によって意見の違うことに対しての自分なりの答えを出すために用いています。

  • チーム内の実力差

大抵の場合チームには自分より実力のあるエンジニアがいます。
スポーツ選手がそうであるように、いかにお互いにシナジーを産んで成長し勝利(成果)に繋げるかが問題です。
新人側は自身の成長や成績のために質問し、また他の部分でチームに貢献することを意識します。
質問の仕方については他の良記事に譲り、チームへの貢献について考えてみます。

私の場合はチームにジョインした当初、自身がチームに貢献できていないことに対する焦りを感じていました。今考えてみれば、ほぼ新人のようなエンジニアが数ヶ月立ち上げに時間がかかるのは当然と言えば当然なのですが、新人側としては日々教育を受けるばかりで焦る気持ちだけが強くなります。
そこで私がしたのは些細なチームタスクを捌いたり、チームの問題を解決するちょっとしたツールを作成することです。

大抵の場合、チームには誰でもできるような些細なタスクがいくらか存在し、優先度が低いためにしばらく放置されたりします。この辺りを普段の業務に加えて積極的にピックするようにしました。
また、些細なチームの問題、例えば私のチームではslackで終礼(日毎の定例を朝会ではなく業務終了前に行なっています。)の際の司会をランダムに決めたかったが、slackで実現するのがめんどくさいといった本当に些細な課題がありました。このあたりをツールとして作ったりしていました。些細ですがチームの利益にはなっていますし、新人側の私にとっては精神衛生上よかった取り組みでした。

このようにチームとして教育を受ける一方でチームに貢献する意識はエンジニアとしても大切なものだと感じています。 いわゆるギブアンドテイクの精神です。

  • バス係数を増加させる(属人化を防ぐ)

バス係数というのは、チームの何人がバスに轢かれるとチームが機能しなくなるか、という値です。
例えばプロスポーツチームにエース選手がいるように、ソフトウェアエンジニアリングを行うチームにもキーマンが存在します。
それはドメイン知識であったり、技術的な知識の面かもしれませんが、キーマンの握る知識をできる限りチーム内で共有知になるようにします。
質問をテキストベースでまとめて質問し、回答をテキストでもらう、ペアプロ等で直接教えてもらったことをまとめてチームに共有するなどがあります。
これらは自分の理解度をチームに共有し、かつチームとしての知識共有に貢献できます。
エース選手(キーマン)が移籍した途端にチームが崩壊することを防ぐために貢献することも新人エンジニアがチームに対してできる貢献の仕方です。

  • 業務時間外の自己研鑽

ここ最近は働き方改革等で業務時間が短縮したり、残業時間が見直されたりしています。

もちろんこれはとても良い方針で、エンジニアリングの文脈からもエクストリームプログラミング持続可能なペースとして紹介されています。
概要としては、「開発者は週40時間を超えて働いてはならない」という考え方です。
人間は十分な休息をとっていれば最高のパフォーマンスを発揮し、最も創造的な活動を行うことができるという考えからきています。

話を戻して考えたいのは、「プロスポーツ選手は自己研鑽をするのか、しないのか」です。
もちろんチームから休暇中の過ごし方について細かく管理される場合は少ないとは思いますが、たいていの選手は、休暇中であっても体調に気を使い、次シーズンに向けて自己研鑽を積むことでしょう。
エンジニアにも同じことが言えると思っています。会社から指示されることがなくても、自己研鑽を積んでいる人がそうでない人より成果に貢献しやすく、評価されやすいです。
ここでの注意点は、あくまで評価されるのは成果であり自己研鑽そのものではないという点です。
プロスポーツ選手もエンジニアも求められるのは 成果(スポーツ選手の場合は勝利) であり、そこに近づくための工夫は個人に任されていると思います。
自己研鑽なしに成果を出し続けられるのであれば何の問題もないですが、現実的には自己研鑽を積んだほうが成果を出すのが容易になります。

隠蔽は有害と見なされる

参考書籍2.3項で紹介されている部分です。以下引用

単独作業に全ての時間を費やすことは不要な失敗をしたり成長の可能性を逃したりするリスクを増大させているに等しい。

特に新人エンジニアが一番悩む問題が、「わからない部分があるけれど、どこまで自分で調べてどういうふうに質問すればいいのかわからない」問題だと思います。
この点についていくつか個人的に考えている指針を紹介しようと思います。

まずは問題がドメイン知識に関する問題なのか、一般的な技術に関する問題なのかを判断します。

関連するドキュメントがすでに用意されていて場所を知っている場合はそちらを参照してからできるだけ早めに聞くようにします。
理由としては一般的な技術の問題と比較した場合に情報を検索してたどり着くことが難しいことが多く、下手すると情報自体、ドキュメントが存在しない場合があるからです。

  • 技術的質問は時間を決めて解決を試みる

ドメイン知識の問題とは違い、技術的な問題の場合はWeb検索等で解決できる場合があります。

有名なGoogle15分ルールにもあるように、まずは時間を決めて解決を目指します。

解決できなかった場合は躊躇わずにチームメンバーに質問をしましょう。
Googleの場合は15分ですが、個人的にはある程度長くなることも考慮しています。
個人的な判断基準としては「自分がタスクを捌く単位時間 * 問題解決にかかる時間」が「相談する相手がタスクを捌く単位時間 * 問題解決にかかる時間」を上回りそうな場合に質問します。

この基準においては、自分では解決できなさそうだと感じる問題の場合、問題解決にかかる時間は無限大になるので15分を待たず即質問します。
反対に、少し悩めば解決できることの場合は相談する相手が同じ時間を問題に使うのに比べて自身で解決したほうがコストが低くなるので粘ってみます。(問題を相談する際にはコミュニケーションコストも発生します)

また、この考えに従うと聞いた場合に相手から即答で答えをもらえそうな場合に迷わず聞くことになりますが、チーム全体としての問題解決にかかるリソースが最小限になります。

メンバーに質問して問題に対する回答を得られた場合は問題の解決策に加えて以下のような内容を確認します。

  • 問題を解決するために調査した情報リソースはなにか
  • 例えばエラーメッセージをみて何を判断して解決したか
  • 問題を解決するために必要な前提知識は何か

同じ問題が再発した場合に自身で解決することはもちろん、根本原因を理解することで類似する問題にも対処することを目指します。

エンジニアにおいて質問するというのは必須のスキルであり、質問の仕方というのはエンジニアとして成長する上で欠かせない技術です。
参考書籍には

常に学び続けよ、常に質問し続けよ

とも書かれており、質問することを恐れてはいけません。(良い質問の尋ね方)
エンジニアはよくいい質問の仕方を記事にします。この記事の多さはそれだけソフトウェアエンジニアリングにおいて質問が重要であることの示唆だと思います。

個人的に技術面以外で業務で意識していること

ここからは書籍関係なく業務で意識しているポイントです。自分で意識していますし自分が「この人仕事できるな〜」と感じるのもこのあたりの要件を満たしている人です。
自分がどういう人を「仕事ができる」「一緒に働いて仕事がしやすい」と感じているかを言語化しておきます。

  • 他人の立場になって考える
  • 仕事の目的を理解する
  • 自分の頭で考える

他人の立場になって考える

この考え方から起きるアクションのメリットはコミュニケーション部分に現れることが多いです。
例えば質問をする場合や報告をする場合、自分の持ちうる情報の中で相手が欲しいものはどれかを考えて文章を構成します。
できる限りの情報を伝えることはもちろん必要ですが、相手の欲している情報をできるだけ最初に、できるだけ簡潔に伝えるようにします。

また、コードレビューをメンバー全員で行うような場合等自身がボトルネックになるタスクがある場合はそのタスクの優先度を上げます。
自身のタスクが他人に与える影響を考慮して優先度を決定します。

そもそもエンジニアはユーザーのために開発を行うので、日頃の業務から他人の立場に立つ練習をしておくとユーザーにとってより良いプロダクトを開発できると思っています。

仕事の目的を理解する

日々なにかしらのタスクをこなしていると思いますが、タスクを振られる際には必ず目的や背景を意識し、わからなければ聞くようにします。
このタスクをこなすとプロダクトがどう良くなり、ユーザーにどう影響するのかを確認します。

日々意識するようにしていると実は必要のないタスクだった、なんてパターンも考えられます。
「このタスク、〇〇をやることで必要なくなりませんか?」みたいな提案は新人でも行える上にチームのリソースを別のことに活かすことができます。
また、エンジニアは大抵テックリードやマネージャー候補です。将来的に自分で目的を考えて業務をこなす練習を新人の時から意識できるとチームを率いることができるまでの時間が短くなると思ってます。

自分の頭で考える

現在の弊チームには前リーダーから共有されて受け継がれている一つのツイートがあります。

https://twitter.com/ChoConejito/status/1495765992240205830

この自分で考えろ、だが勝手な判断をするなというのは必ずしも矛盾するわけではないということを理解する必要があります。

自分の意見を持つことと、自分の判断のみで行動することには大きな差があります。

この区別を明確にするために個人的に意識していることは、「判断の結果、個人、もしくはチームのリソースを一定以上必要とする場合は必ずチームに共有する」ことです。

そもそもこの考え方にはいくつかのステップがあります。

1.自分の意見を持つ

特に受託や派遣といったキャリアを持つ場合、言われた通りに実装することに慣れてしまっている場合があります。まずは自分がどうするべきと判断して、その理由を合わせて説明できるようになる必要があります。
これには前提となる技術的知識やドメイン知識が必要であったりしますが、常に自分の意見を持つことで失敗から得られる経験値が上がるという副次的なメリットもあります。

2.自分の意見をチームに共有する

ここで行うのは自分の意見を言語化し、整理してチームに共有することです。ソフトウェアエンジニアリングにおいて言語化能力と文章をまとめる力はコードの整然さと比例すると思っています。
また、整理した上でチーム内の心理的安全性の確保も必要です。
よく誤解されがちな心理的安全性という言葉ですが、チーム全体で仲良しこよししようというものではありません。
チームにとって利益になると自分が心から信じている提案や意見があり、チームの雰囲気を壊すとみなされて不当な評価を受けるといった理由で発言することを躊躇わない関係性の構築を指します。そういった意味ではある意味チーム全体で仲良しごっこをしてぬるま湯に浸かることの正反対にあると言えます。

普段から心理的安全性を確保した関係性の構築と自分自身の意見を用意することで自分の意見をチームに共有することができます。

3.判断の結果、個人、もしくはチームのリソースを一定以上必要とする場合は必ずチームに共有する

1、2のステップで自分の意見を共有する準備ができたのであとはタイミングの問題です。
私個人の判断基準は自身の判断の結果、個人、もしくはチームのリソースを一定以上必要とするかどうかです。
例えば判断の結果、行動するにあたって数分〜数十分程度で実行できる場合はチームに共有するより実行してしまって結果を共有するようにします。
反対に行動に数時間以上リソースを必要とする場合、必ず行動に移す前にチームに相談します。

自分のリソースはチームのリソースであることを忘れてはいけません。
相談の仕方としては、「〜〜のような問題があります。現時点での自分の意見としては〜〜を試したいと思っています。何か他に解決策やアイデアがあれば教えてください。」のように、

  • 現状の問題
  • 問題に対する自分の意見

を含めます。

この方法には複数意図があります。

1.文字通りチームメンバーに知見があればアドバイスをもらう

これは文字通り、チームメンバーに知見がある場合に助けを求める意図です。

2.現在の自分の困っている状況をオープンにする

相談した時点で自分が困っていることをチームメンバーに伝えます。
また、テキストでの相談の場合は回答が残り、過去の回答が検索等で閲覧可能であればそれはチームとして、もしくは組織としてのナレッジとなります。

3.相談することで行動に対する判断を個人のものではなくチームのものにする

個人的に新人が意識する意図はここにあります。
個人の判断で行動することなく、チームに共有した上で行動します。
仮にこの行動方針が間違っていたとしてもチームに方針を相談ができていれば、状況が共有でき、指摘やアドバイスを求めることで責任をチームで持つことができます。
個人で責任を背負いすぎないためにも負荷分散しましょう。

まとめ

私が新人WEBエンジニアとしてここ1年意識した点をまとめます。

  • ソフトウェアエンジニアリングに関わる以上、チームとして活動する他ない。
  • チームは個人に貢献し、個人はチームに貢献する姿勢を持つ
  • 自身の状況や課題を常にオープンにし、チームに共有する
  • 相手の立場に立って考えて行動する
  • 目の前の仕事だけではなくその背景を理解しようと努める
  • 自分の意見を持つようにするが、行動を伴う場合は必ずチームに共有する

所感

今回は業務を通じて意識してきたことを言語化しました。
半分ポエムですが今回挙げたポイントを満たしている方と仕事をするとストレスなく円滑に仕事ができているなと感じます。
今回参考にした書籍は2章というごく一部でさえエンジニアとしてチームで働くことの重要性とその方法についてまとまっていてとても参考になりました。
全600ページ超となかなかのボリュームですがようやく半分近く読み進めることができていますが学びの多い箇所が多いです。