i-whammy

プログラマーとして学んだこと、思考を整理したことを書く。はず

2022年をふりかえる

三が日のうちに書けば良かろうと思って、気付けばこんなタイミングになってしまった。

2022年を色々な方向からふりかえろうと思います。

体調の話

  • とにかく体調に左右された一年だった
    • 流行りの病には一度もかからずに済んだ
    • ただ一言で言うと「起きてみないとその日の状態が分からない」日が非常に多かった
    • ラッキーなことに、秋頃に良い病院、良い治療法と巡り会えて、今は安定し始めている

仕事の話

  • 仕事も体調に左右されることが多かった
    • 本当ならもっとリスクを取りたい場面で、体調を理由に二の足を踏んでしまったり、諦めたりしたことがたくさんあった
  • とはいえ悪いことばかりでもない
    • 今までにない挑戦をさせてもらえている
    • 僅かではあるが成果を評価してもらえたりしたのは嬉しかった

技術者としての話

  • 取り組める範囲で新しい技術に触れようとしてきた
    • 経験のない部分で言えばReact、手持ちのもので言えばKotlin、Clojureに触れる時間が長かった
  • 社内向けのイベントで数回登壇した
  • 社外勉強会で初めて登壇した
  • 翻訳者の一人として参加した本が出版に至ったりした

プライベートや趣味の話

その他

  • 2022年始に掲げた、やりたいこと100個は40個くらい達成した
    • 100個全て達成する必要はないと思ってるので、まあこのくらいかな

2023年の話

いよいよ30歳が目の前に見えてきた。

新卒で一社目に入った時、「30歳までにマネージャーになりたい」ということを漠然とした目標に掲げていた。その会社におけるマネージャーは、会社内のポジションとして一つの到達点であり、憧れであり、大きな裁量や技量を持っていた。

今働いている組織では、そういうものに相当するポジションがない。*1

そういう環境に4年以上身を置いてきたせいか、今はそうしたポジションへの執着も全くない。

それよりも、どれだけ個人として技術を身につけるか、どれだけチームの成果を増やせるか、そういうことを考える時間が増えた。

漠然とした表現ではあるが、一人の技術者として、技術は勿論のこと、事業を成功に向けて動かし続ける経験を積み重ねたい。

その為に自分の手持ちの武器を磨きつつ、体幹を鍛えていきたい。でもいのちだいじに。


2023年末か、2024年始にこの記事を読み返して、その方向に少しでも近づけていたら嬉しい。

*1:世間ではIC=Individual Contributorという言葉があるみたいだけど、それとは少し違っている。勿論コードを書く時間が大半だけど、PM、PdM的な役割も全員が担っているイメージと考えてもらうと分かりやすいと思う。

駆け出しエンジニアの強い味方『教養としてのコンピューターサイエンス講義第2版 今こそ知っておくべき「デジタル世界」の基礎知識』を読んだ

『教養としてのコンピューターサイエンス講義第2版 今こそ知っておくべき「デジタル世界」の基礎知識』を最近読み終わりました。

著者は『プログラミング言語C』『プログラミング言語Go』等で知られるBrian W. Kernighan氏。御年80歳。

この本はカーニハン氏がプリンストン大学で行ってきた一般人向けの講義「Computer in Our World」*1の内容をまとめたものらしいです。その講義名にふさわしく、私たちの世界を取り巻く「コンピューター」というものについて、歴史と成り立ち、これからを網羅的に語った一冊となっています。

以下、発行元の日経BPのウェブサイトから目次を引用します。

【目次】
第0章 はじめに

第1部 ハードウエア
 第1章 コンピューターとは何だろう
 第2章 ビット、バイト、そして情報の表現
 第3章 プロセッサーの内部
 ハードウェアのまとめ

第2部 ソフトウェア
 第4章 アルゴリズム
 第5章 プログラミングとプログラミング言語
 第6章 ソフトウェアシステム
 第7章 プログラミングを学ぶ
 ソフトウェアのまとめ

第3部 コミュニケーション
 第8章 ネットワーク
 第9章 インターネット
 第10章 ワールド・ワイド・ウェブ

第4部 データ
 第11章 データと情報
 第12章 人工知能と機械学習
 第13章 プライバシーとセキュリティ
 第14章 次に来るものは?

各章のタイトルから見てお分かりいただける通り、コンピューターサイエンスに入門した人、実務でソフトウェアエンジニアリングを行う中で基礎を固めたい人にとってはうってつけの内容になっていると思います。このブログ記事のタイトルにも触れましたが、まさに「駆け出しエンジニア」とされる人々にも是非オススメしたい本だと思っていますし、自分の心を駆け出しの頃に戻す上でも大きな役割を果たしてくれる一冊だと思います。

内容自体は基礎的なものが多いですが、これらの範囲を完全に理解できているソフトウェアエンジニアはそういないと思います。*2自分の自信がない部分をもう一度確認する、気になった時に再度読み返す、そうしたリファレンスのような使い方ができる本でもあると思います。

各章の概観

前書き・第0章 はじめに

カーニハン氏は本の冒頭で、コンピューターに関連する様々な事柄の中でも、「ハードウェア」「ソフトウェア」「コミュニケーション」「データ」という4つの中核的な技術領域があり、それらを知っておくべきであると述べています。実際にこれらの4項目は以降の書籍の構成にもつながっており、今後読み進める上でも助けとなる考えになってくれていると思います。

それぞれの項目について、カーニハン氏の言葉も借りて具体的に説明すると以下のとおりになります。

 ハードウェアは目に見える部分です。家庭やオフィスに置かれたり、携帯電話として持ち歩けたりする、見たり触れたりできるコンピューターです。

第1部では、ハードウェアの歴史、その中で情報がどのような形式でやり取りされているか(ビットとバイト)、ビットとバイトを用いて文書、画像、音楽、動画をどのように表現するかが説明されています。

 ハードウェアとは対象的に、コンピューターに何をすべきかを指示するソフトウェアは、ほとんど目に見えません。

第2部では、ハードウェアを動かすソフトウェアについて、どのように動いているのか、どのように動かしているのかをアルゴリズム、OS、メモリ、CPU、プログラミングといった切り口から解説しています。

 コミュニケーションは、コンピューターや電話といった機器が相互に通信して、私たちが対話できるようになることです。
 インターネット、ウェブ、電子メール、そしてソーシャルネットワークなどがコミュニケーションに使われます。

第3部では、コンピューター同士がどのようにコミュニケーションを取っているのか。ネットワーク、インターネット、ウェブと徐々に範囲を広げていきながら解説しています。

 データは、ハードウェアとソフトウェアが収集、保存、処理するすべての情報で、世界中の通信システムが送信しています。

第4部では、そうしたネットワークを通じてやり取りされたり、ハードウェア上で処理される情報について、どのようなリスクや活用方法があるのかを解説しています。

第1部 ハードウェア

ハードウェアのパートではコンピューターがどのような論理的構造から成り立っているか(=プロセッサー、メモリ、ストレージ等)、物理的構造はどのようになっており、進化してきたか(=ムーアの法則)から話が始まります。実際に目に見えやすい部分から説明が始まることで、読者としても理解しやすくなっているなと思います。

特に第3章の「プロセッサーの内部」では、コンピューターが電源が点いてからOSが起動するまでに起こっていること、プロセッサーが処理を実行する時の並行処理・パイプライン処理・キャッシングといった技術を取り扱っており、ハードウェアの基礎となる部分をしっかりおさえられるようになっています。

第2部 ソフトウェア

ソフトウェアのパートでは、線形アルゴリズム、二分探索といったアルゴリズムをテーマにすることから始まり、実際にソフトウェアを作るプログラミング、OSSの世界と歴史、OSが果たす役割(システムコールデバイスドライバー、ファイルシステムプロセッサー・メモリ・ストレージの管理等)といった内容に触れていきます。

この本の一番良かったと思える部分は、この第2部の幅広さです。アルゴリズムを取り扱う第4章では線形アルゴリズムから各ソートを経てNP問題まで、OSを取り扱う第6章ではプロセッサー、メモリ等との関わりから仮想OS、上位レイヤーのソフトウェアとの関連まで、とソフトウェアの基礎となる部分を網羅的におさえてくれています。自分が特に自信がない分野であり、Webエンジニアがあまり普段意識しない領域(かつ所謂「低レイヤー」として重要視されることもある分野)だと思うので、その部分の知識を固めたい人にとってはオススメのパートです。

第3部 コミュニケーション

コミュニケーションのパートでは、原始的に人間がどのような通信を行ってきたか(視覚信号、モールス信号、電気通信)から話が始まります。そこからやるのか?と思えるくらい歴史をさかのぼりますが、目に見えづらいネットワークの世界の導入としてはありがたかったです。

そこからローカルのネットワーク、イーサネット、無線通信、電話回線へと話を展開して、インターネットの話に移ります。インターネットを取り扱う第9章では、パケット、DNSTCP/IPTelnetSSHP2P通信といったネットワークの基礎となる知識を取り扱っています。

第4部 データ

最後のデータのパートでは、「データ」と関連するトピックを幅広く取り扱います。検索エンジンの仕組みから、トラッキング広告、サードパーティクッキー、クラウドコンピューティング機械学習、暗号アルゴリズムといったように、幅広さが見て取れると思います。

個人的には第10章のワールドワイドウェブから地続きに検索、クッキー周りの話を読み進めると、理解が深まりやすいのではと思いました。


「『低レイヤー』の知識をつけたいけど何から手を付けて良いのかよく分からない」「CSの知識や学歴がなく、網羅的な知識を身に着けたい」人にとってはオススメの一冊です。

範囲としては基本・応用情報技術者試験の午前試験の内容と重なる部分が多くあると思うので、試験勉強の副読本としても利用できるかも知れません。

*1:なおこの講義名自体は「ある日、5分以内にひねり出す必要があって付けた名称で、その後変更が難しくなってしまったもの」らしいです。

*2:もちろんですが、私も完全に理解できているとは思っていません。

End-to-endテストで日付のテストをする時に思い出してほしいこと

TL;DR

  • テストコードであっても腐るものは腐る

  • システムの表層にあたる画面の要素にとらわれず、業務のドメインを掘り下げて理解を深めるのが大事。

大体の話は

Twitterに書いておいたので先に読んでもらえると分かりやすいと思います。

対象読者

  • CypressとかSeleniumみたいなエンドツーエンドで画面の自動テストが実行できるフレームワークを使って自動テストを書いてる人

    • と思ったけど画面に限った話ではない気もするのでそれ以外の人も読んでもらえると何か学びがあるかもしれない

画面の自動テストを書く時に起こること

例えば画面内に、特定の日付が表れることをテストしたくなったとします。

その時、あなたはどんな表現でテストを書きますか?

どんなフレームワークでも、実装レベルでは「特定のCSSセレクタに該当するHTML要素の値が"2022年9月1日"になっていることをアサーションする」みたいな実装になると思います。

じゃあこれで終わりでいいのか、って言ったらそんなことはなくて、テストの保守性を高めるにはもう少しやれることがあるんじゃないかなーと思って筆を取りました。

以下サンプルコードはCypressとjavascriptを使って書きますが、言語やFWは本論じゃないので雰囲気で読み流して下さい。

愚直に実装する

「id="date"がついたdivタグの値が"2022/9/1"である」というテストを書きたいなーと思うと、Cypressだと以下のようなコードになります。

describe('テスト', () => {
  it('日付が2022年9月1日である', () => {
    cy.visit('/')

    // 中略

    cy.get('#date')
      .should('contain', '2022/9/1')
  })
})

今回話の対象にしたいのは、以上の例で言う「テスト」及び「日付が2022年9月1日である」の部分です。

これって何の日付なんだっけ?

「日付が2022年9月1日である」というアサーションの文言は、テストのアサーション表現としては必要最低限でしかありません。

まず「何の日付なのか?」という情報が欠けています。例えば受注データが記録された日なのか、荷物の発送予定日なのか、それともただの固定された文字列なのか*1、アプリケーションによってコンテキストは異なるはずです。

この「何の日付なんだっけ?」という問いへの答えをテストにも残しておくことで、後からテストを読んだ開発者がアプリケーションの仕様を理解しやすくなるのではないでしょうか。

以下の例は「受注日付が2022年9月1日であること」をわかりやすくしたコードのサンプルです。(あえてセレクタはそのままにしておきます)

describe('受注管理画面のテスト', () => {
  it('受注日付が2022年9月1日である', () => {
    cy.visit('/')

    // 中略

    cy.get('#date').should('contain', '2022/9/1')
  })
})

この日付っていつ変わるんだっけ?

次に、この日付がいつ変更される可能性があるのかという点も気にする必要があります。固定値であれば気にしなくても良いのですが、例えば月をまたいだり、年をまたいだりしたタイミングでこのセレクタで取得される日付が変わるとしたら?

もしこの自動テストがデプロイメントパイプライン上で定期的に実行されていたら、「コードを何も変更していないのにテストが失敗した!」という事態に陥ってしまうのではないでしょうか。*2

いつ日付が変わるのかに着目することで、より安定した、かつ仕様に忠実な表現に直すことができるのではないでしょうか。

以下の例は「id='date'に表示される日付が、アプリケーションを開いた月の初めの日であること」に読み替えたコードのサンプルです。

describe('受注管理画面のテスト', () => {
  it('表示される受注日付はその月の初めの日である', () => {
    cy.visit('/')

    // 中略
    const today = new Date()
    const firstDateOfMonth = [today.getFullYear(), today.getMonth()+1, 1].join('/')
    cy.get('#date').should('contain', firstDateOfMonth)
  })
})

何故こんな話がしたかったか

一見シンプルに見えるここまでの議論ですが、何故わざわざ取り上げようと思ったか。それはこのプロセスの中に、「システムの表層である画面」から「システムで取り扱うドメイン」への追究が発生していることに気がついたからです。

CypressやSeleniumのような自動テストフレームワークでテストを記述する時、開発者が一番注目するのは画面です。ともすれば、画面で起こっている表面的なことにばかり目が向いて、上で挙げたような愚直な実装に陥ってしまうこともあると思います。愚直な実装はその場では問題が無かったとしても、ドメイン知識を持つ開発者がチームを離れた時に仕様に対する知識が失われたり、突発的に失敗するテストに変貌するリスクを抱えています。

自動テストを書く時に、目線を画面からより深いドメインの部分に向ける、そしてそこで得た知識をコード上に反映させることで、テストコードの保守性をより高められるのではないでしょうか。

......というところで、先人(自分含む)が数年前に残したテストが突如失敗する様を目にした結果、勢いで書き始めた記事を締めようと思います。

*1:そんなアプリケーションがあるのかはわかりません

*2:私は最近なりました

Kotlin 1.2のアプリケーションを1.7までバージョンアップする

対象読者

  • 「このアプリケーション、Kotlinのバージョン古くなってきてるし、最新まで上げようぜ!」って仕事をやることになった、ちょっと前の私。
  • およびそのような人(いるのか)。

前提条件

  • 2017年頃ファーストリリース、現在も稼働中のバックエンドアプリケーション
    • ただしアクティブなメンテナとなるチームはおらず、今回たまたま私が改修するタイミングがあった
  • Java
    • 1.8系
  • Kotlin
    • 1.2.10
      • 2017年12月頃にリリースされた模様 github.com
  • フレームワーク
    • Spring Boot(1.5.8.RELEASE)*1
  • プロジェクト管理
  • その他
    • JUnit5
    • mockk
      • 1.7.15
    • mockito-kotlin
      • 1.6.0

結果

  • Kotlin
    • 1.2.10 -> 1.7.10
  • mockk
    • 1.7.15 -> 1.12.0
  • mockito-kotlin
    • 1.6.0 -> 2.2.0 *2
  • その他一部アプリケーションコードの修正(後述)

バージョンアップのモチベーション

Kotlin 1.2系が、最新のIntelliJ IDEAではサポートされていない為です。

執筆時点で最新バージョンの2022.2.1のIntelliJ IDEAでKotlin 1.2を利用しているアプリケーションを起動しようとしても、そもそも起動することができません。 (正確にはビルドすらできません。Kotlin: Language version 1.2 is no longer supported; please, use version 1.3 or greater.というメッセージの通り、最低でも1.3以上にする必要があります)

このことは開発生産性の観点で大きなマイナスなので、バージョンアップすることにしました。

方針

  • 時間をあまりかけたくないので、変更は必要最小限に
    • 極力バージョンアップ前に利用していたライブラリをそのまま利用する(ライブラリの追加、削除よりも、既存ライブラリの新しいバージョンを利用する)
    • 極力アプリケーションコードの修正を加えない

Kotlinのバージョンを上げる

最新のバージョンを確認する

  • GithubのReleasesページを確認 github.com
    • なおIntelliJ IDEAでは、より新しいバージョンのライブラリを利用できる場合、pom.xmlファイル上でガイドしてくれる機能がある

とりあえず上げてみる

  • アプリケーションコード上で修正が必要な箇所はビルド時点では特になし*3
  • Kotlinで書かれたコードをJava8上で動かす場合には、kotlin-stdlib-jre8 ではなく、kotlin-stdlib-jdk8を利用する必要がある点に注意が必要

単体テストを実行できるようにする

  • この時点では単体テストが実行できなくなっていた

    • mockkが依存するCoroutine周りのクラスパスが変わっていた
      • Kotlin 1.3以降でCoroutine周りの機能がデフォルトに変わったため
    • ということでmockkを最新までバージョンアップ github.com
  • 今度は一部のテストケースで「Caused by: java.lang.NoSuchFieldError: ASM_API」というエラーメッセージが出てテストに失敗するようになる

    • これはmockito-kotlinが依存しているbytebuddyのバージョンが古いことによって起こっているらしい
    • mockito-kotlinのバージョンも最新にすることで解決

アプリケーションが問題なく稼働することを確認する

  • JAX-RSのParamConverterを利用する処理の周りがうまく動かないことを確認
    • これはKotlinをバージョンアップしたことで、Null Safetyがより厳しく判定されるようになったため

例えば以下のようなParamConverterを実装したクラスがあるとします。(LocalDateTimeはJava標準のクラスとする)

class LocalDateConverter: ParamConverter<LocalDateTime> {
    override fun fromString(value: String): LocalDateTime {
        try {
            return LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"))
        } catch (e: Exception) {
            throw IllegalRequestException("Invalid date string. $value")
        }
    }

    override fun toString(value: LocalDateTime) =
            value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"))
}

ParamConverterはJAX-RSにおいて、String型のメッセージパラメータと特定の型Tで相互に変換する責務を担います。

参考:https://spring.pleiades.io/specifications/platform/8/apidocs/javax/ws/rs/ext/paramconverter

上記クラスで実装している fromString 関数は、文字列から特定の型Tへの変換処理を担っています。

Kotlin1.2においては、この fromString 関数の引数としてnullが渡ってきても問題なく動いていました。しかし、Kotlin 1.7までバージョンアップされる過程で、Null Safetyが厳しく検査されるようになり、nullが渡ってきた場合に想定通りに動かなくなってしまったようです。

その為、ワークアラウンドとして以下のように、nullの値が渡ってきても問題ないようにアプリケーションコードを修正しました。

    override fun fromString(value: String?): LocalDateTime? {
        try {
            return value
                ?.takeIf { value.isNotEmpty() }
                ?.let { LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")) }
        } catch (e: Exception) {
            throw IllegalRequestException("Invalid date string. $value")
        }
    }

ということで

こまめなバージョンアップって大事ですね。

*1:こいつもついでにバージョンアップしたかったが今回は時間の都合で割愛

*2:mockito-kotlinは2系にバージョンアップされる過程でGroup IDが微妙に変わっているので注意が必要です。com.nhaarman -> com.nhaarman.mockitokotlin2

*3:Deprecatedな関数はいくつかあります。String.toUpperCase等。https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/to-upper-case.html

マイクロサービスとは何であり何でないか?を知る、実践的な教科書:「モノリスからマイクロサービスへ ―モノリスを進化させる実践移行ガイド」を読んだ

はじめに

オライリー・ジャパンから出版されている、「モノリスからマイクロサービスへ ―モノリスを進化させる実践移行ガイド」を読みました。

最近で読んだ技術書の中でもなるほど、と思う部分が多く、概要に触れながら感想を書き残していこうと思います。

概要

この本の著者Sam Newmanは以前にBuilding Microservices(邦題:「マイクロサービスアーキテクチャ」)を著し、本書のことを冒頭でも「既存のシステムをマイクロサービスアーキテクチャに分解することをどう考え、実行するかを深く掘り下げた内容になっている」と紹介しています。

その紹介の通り、この書籍は確かに既存のシステムからマイクロサービスへの段階的な移行について、アプリケーションやデータベースの分割に当たって具体的な方法を示しています。

一方で、その移行前後の存在である「既存のシステム(しばしばモノリスとして形容される)」と「マイクロサービス」が何であり、何でないか、を詳細に紹介してくれています。

その為、マイクロサービスへの移行を志すエンジニアは勿論ですが、マイクロサービスを採用している現場で働くエンジニアにとっては何故今のようなシステムアーキテクチャが採用されているかの理解を助ける一冊となりますし、今後自分がシステムを設計していく際の引き出しを増やしてくれるような存在にもなっていると思います。

以下、オライリー・ジャパンの公式サイトから本書の内容の紹介を引用します。

本書は、モノリスからマイクロサービスアーキテクチャへと移行するための実践的なガイドです。マイクロサービスが自分たちのシステムに適しているかを判断するところから、ビジネスを維持しながらモノリシックなシステムを少しずつマイクロサービスに切り替えていく方法、さらには、マイクロサービスアーキテクチャが成長するにつれて起こる課題への対処の仕方まで、豊富な例やシナリオを用いて解説します。また、モノリスやデータベースを分解していくのに役立つ様々なパターンやテクニックも扱います。 システムのアーキテクチャ移行について具体的な方法を解説する本書は、エンジニア必携の一冊です。

O'Reilly Japan - モノリスからマイクロサービスへ

各章のまとめ

以下では、印象に残った箇所を引用しつつ、各章の内容の一部を簡単に紹介します。

1章 必要十分なマイクロサービス

この章では、以降で触れていくマイクロサービス、及びそれの対になるモノリスについて紹介しています。

まずマイクロサービスですが、本書内では以下のような言葉で説明されています。

マイクロサービスとは、ビジネスドメインに基づいてモデル化された、独立してデプロイ可能なサービスだ。サービス同士はネットワークを介して相互に通信してシステムを形成する。(p.1)

技術的な観点から見ると、マイクロサービスとは、カプセル化されたビジネス機能を1つ以上のネットワークエンドポイントを介して公開するものだ。それらはネットワークを介して相互に通信しあい、分散システムを形成する。(p.1)

ここにマイクロサービスを構成する要素である、以下のような3つの要素が見られます。

  • ビジネスドメインに基づいてモデル化・カプセル化されたサービスである
  • 独立してデプロイ可能なサービスである
  • ネットワークを介して相互に通信することでシステムを形成するサービスである

システムを実際に設計する上では、特に最初の2点が重要になります。1点目は、「どのような境界で、どのような方法でサービスを分割するか」「どのような情報を公開・隠蔽するか」という指針に、2点目は「独立してデプロイ可能にするために、どのような方法でサービスを分割するか」「分割されたサービスを独立してデプロイできるパイプラインをどのように設計するか」という指針に繋がります。

一方で筆者は、本書のテーマはモノリスからマイクロサービスへの移行である、と掲げています。その為この章の中で、モノリスがどのようなものであるかについても説明しています。本書の中では単一プロセスとしてデプロイされているシステムを一般的にモノリスとして紹介していますが、この章では、単一プロセスを複数モジュールで構成するモジュラーモノリス、複数サービスで高壊死されているもののシステム全体の独立デプロイ性を欠いている分散モノリスサードパーティ製のブラックボックスシステムを他の類型として紹介しています。

筆者はモノリスそれ自体にも利点があることを認めた上で、具体的に移行を行うにはどのようにしていくかを後の章で紹介しています。

2章 移行を計画する

この章では、実際に移行を計画する上で留意する点を説明しています。

章の冒頭で筆者は以下のように切り出しています。

マイクロサービスは目的ではない。マイクロサービスを手にすることが「勝利」ではないのだ。マイクロサービスアーキテクチャを採用することは、合理的な意思決定に基づいた意識的な判断であるべきだ。マイクロサービスアーキテクチャへの移行は、既存のシステムアーキテクチャのままでは達成できないことを実現するために考えるべきだ。(p.37)

この文章が本書の本質だと思います。執筆当初、マイクロサービス、が一種のバズワードと化していたように思います(今でもそうかもしれません)が、マイクロサービスはあくまでシステムを設計する上での合理的な手段の一つであり、目的とはならないということを留意するべきだと思います。

筆者はさらに、「3つの重要な質問(p.37)」として、マイクロサービスアーキテクチャ化を検討すべきか組織が見極める際、筆者が実際に行っている質問を紹介しています。

  • 達成したいことはなにか?

  • マイクロサービスの他に代替案はなかったか?

  • どうすれば移行がうまくいってるか分かるだろうか?

以降で筆者は、マイクロサービスを採用する際によく挙げられる理由(利益)を例として取り上げつつ、その利益がマイクロサービス以外でも享受できることを説明しています。例えば「チームの自律性向上」に対しては、コードベースの所有権の変更による自律性の付与、「リリースまでのリードタイム短縮」に対しては、デリバリーに関わるステップの可視化、ボトルネックの特定、といったように。

自分は特に2つ目の「リードタイム短縮」についての記述が参考になりました。一般的にアプリケーションのサイズ(コードの記述量、関連するドメインの規模、機能数)が小さくなるほど、リリースまでの時間は短縮される傾向にあると思います。一方でそれだけに着目して「マイクロサービスアーキテクチャを採用して、リードタイムを短縮するぞ!」と意気込んでも、実際にはサービス間のオーケストレーションであったり、分割に伴う組織設計であったり、それに付随するコストを見落としてしまい、結果的にリードタイムの短縮に繋がらない、という結果にもなり得るかもしれないなと思いました。

そのような場面でこそ、一見華やかな解決策に飛びつくのではなく、愚直に自分達のプロセスを可視化し、ボトルネックを特定、対処した方が、問題の解決に繋がるのではないかと感じました。

3章 モノリスを分割する

4章 データベースを分割する

この2章では、実際のモノリスとなっているアプリケーションや、共有してしまっているデータベースを分割するプラクティスを紹介しています。

詳細な説明は実際に本書を読んでいただくとして、自分が参考になった部分をいくつか抜粋します。

ストラングラーパターン(3.3)

既存のアプリケーションから新しいマイクロサービスへと機能を移行する際のパターンの一つです。このパターンでは、以下のようなステップで移行を進めます。

  1. 既存のシステム内で移行対象と、どのような呼び出しが行われるかを特定する
  2. 既存のシステムへの呼び出しを傍受する
  3. 移行対象の機能を新しいマイクロサービスへ移行する
  4. 移行が完了したタイミングで、2.で傍受していた呼び出しを新しいマイクロサービスへ切り替える

このパターンの長所として、呼び出しを傍受していることから、機能のリリース/ロールバックの切り替えを容易に行えることが挙げられます。既存のシステムを活かしつつ開発を進めた上で、最後に呼び出しを切り替えることで簡単にリリースができますし、万が一問題があったときも元の呼び出しにロールバックすれば良いだけです。

具体的な実装例としては、Nginxのようなリバースプロキシを前段に用意する、等が挙げられています。初めは新しいサービスへの呼び出しは 501 Not Implementedのレスポンスを返すようにして、最後に呼び出しを有効にするよう設定を行うことで簡単にストラングラーパターンを実現できます。

機能を移行しながら振る舞いを変える(3.4)

これは読んで字のごとく、機能を移行するタイミングと同時に、既存のシステムから振る舞いを変えてしまう、というものです。これはパターンと言うよりも、移行の開発中に既存システムに手を加える必要が出てきた場合にどうするか?という問に対する答えのようなものだと解釈しています。

筆者はこの場合に、以下のように述べています。

この問題を解決するのは簡単ではない。移行中の機能を途中で変更することを許容する場合には、ロールバックが困難になることを受け入れなければいけない。(p.99)

例えば既存システムに対してバグが見つかった場合、既存システムと新しいサービスの両方に不具合修正を施さなければ、ロールバックした際に振る舞いが変わってしまうリスクがあります。このようなリスクを避けるため、既存システムへは緊急のものを除いて手を加えない、逆に割り切って必ずどちらのバージョンにも不具合修正を加える、という意思決定をする必要が出てきます。

筆者はこの他にも変更データキャプチャパターン(3.9), サーガパターン(4.16)等の様々なパターンを紹介していますが、最後に3章の冒頭に載っている以下の文章を紹介します。

それぞれのパターンの長所と短所を必ず理解すること。これらは一概に「正しい」方法とは言えない。(p.80)

あくまで本書で紹介されている「パターン」は長所と短所を伴った解決策の一つであり、絶対的に正しい解決策ではない、ということを著者は最初に触れています。方法論が多く記述されているからこそ、それをそのまま模倣するのではなく、どのような問題意識に対して、どのように短所を割り切った上で、長所をもって問題を解決しようとしたかを検討するべき、という初心に戻らせてくれます。

5章 成長の痛み

筆者は本書の最後の章で、マイクロサービスへの移行に際して直面しうる問題について更に掘り下げて述べています。いつ、どのような問題が発生しやすいか、いつこうした問題に対処すべきなのか、どのように対処すべきか、という点を紹介してくれているこの章からも印象に残った箇所を抜粋します。

全体最適と局所最適の問題は)ランチの時に偶然耳にした一言がきっかけで気付くことが多い。コミュニティオブプラクティス(実践コミュニティ)のような、ある種のチーム横断的な技術グループがあれば、これらの問題をはるかに早く発見できる。(p.231)

マイクロサービスへの移行は全体最適と局所最適のバランスが必要な局面を生み出します。マイクロサービスとして分割されたサービスにチームが責任を持つ場合、意思決定はそのサービスに対する局所化されたものになりやすく、全体最適的な目線が失われるおそれがあります。そのような兆候に気付くという点で、カジュアルな他チームとの横断的なやり取りが役に立つのではないか、と著者は述べています。

昨今のコロナ禍によって、リモートワークへの移行が加速していきました。この傾向は、こうしたカジュアルな他チームとのやり取りの機械の喪失に繋がると日々体感しています。ゆえに、意図的にチームを横断するようなコミュニケーションをどのように設計するか、という点は今後のチーム開発において今まで以上に重要になってくるのではないかと感じています。

自分たちの意思決定が不可逆的なものと可逆的なもののどちらに傾いているかを、チームの人々が認識できるようにすることが秘訣だ。意思決定が不可逆的な方向に傾いているほど、チームの境界外の人を意思決定に参加させることが重要になる。

上記のようなチームを横断する話と類似して、意思決定の影響範囲について著者が指針を出しています。なんとなくこの決定は重要そうだから他のチームも巻き込んでおこう、という曖昧な基準ではなく、「この決定は不可逆性が高いので、隣のチームも巻き込んだ上で意思決定しよう」と能動的に選択していくことが重要だと感じました。

さいごに

自分は普段マイクロサービスアーキテクチャを採用している組織でソフトウェアエンジニアとして開発を行っています。そして自分がプライベートで開発を行う際も、規模の小さなアプリケーションをいくつか設計することが多いです。

そんな中本書を読んで、「マイクロサービスアーキテクチャを採用する」とは単に「Webとバックエンドを分割する」という部分にとどまらない意思決定を伴うものであること、そしてレガシーなアプリケーションからマイクロサービスに分割していく手法は、規模の大小を変えてアプリケーションの設計の様々な場面において役に立つことを学びました。

マイクロサービスという単語を知っている、普段マイクロサービスアーキテクチャを採用しているエンジニアにこそ、この本を読んでもらいたいなと感じました。

Jenkinsでジョブが終了しても生き残るプロセスを作る

はじめに

Jenkinsのジョブから、nohupコマンド等でジョブが終了しても生き残るようなプロセスを作りたくなる場面があるかと思います。(例えば特定のプログラムを監視する目的でサーバー上に常駐させたい場合など。)

業務の中で上記のようなケースで若干ハマってしまったため、備忘として残しておきます。

前提として、Jenkinsの中のProcess Tree Killerについて説明します。

Process Tree Killerについて

Jenkinsにはビルドの中で生成されたプロセスを列挙、削除するような機構としてProcess Tree Killerが存在します。

Jenkinsのジョブの中で生成されたプロセスは、ジョブの終了と同時に殺されてしまいます。

デフォルトでは有効に設定されているのですが、もし無効にしたい場合には以下のように起動時オプションとして hudson.util.ProcessTree.disable にtrueを渡してやることで実現可能です。

$ java -Dhudson.util.ProcessTree.disable=true -jar jenkins.war

詳細な説明は以下に譲ります。

ProcessTreeKiller - Jenkins - Jenkins Wiki

個々のビルドの中でProcess Tree Killerを無効にしたい

Process Tree Killerは不要なプロセスを削除する上で役に立つので、デフォルトで無効にすることは憚られるかと思います。

以下のように環境変数 BUILD_IDdontKillMe を渡してやることで、その環境変数が有効になっている限り一時的にProcess Tree Killerを無効にすることが出来ます。

$ BUILD_ID=dontKillMe nohup ping localhost &

因みにJenkins Jobの中で実行する場合には上記のようなやり方で実現できますが、Pipelineでの実行時は BUILD_ID の代わりに JENKINS_NODE_COOKIE を指定する必要があります。

$ JENKINS_NODE_COOKIE=dontKillMe nohup ping localhost &

アプリケーションドライバレイヤーを利用した自動受け入れテストを考える

この記事は

『継続的デリバリー』を引き続き読み進める中で、表題のアプリケーションドライバレイヤー(及びウィンドウドライバパターン)を利用して受け入れテストを実装することについての記述に行き当たりました。

www.amazon.co.jp

以前の記事はこちら。

i-whammy.hatenablog.com

私は業務の中でGaugeとSelenideを利用したUIに対する自動テストを書く事で受け入れテストを実装しているのですが、保守性の高いテストを考える上でヒントになりそうだと思い、読んだ内容を自分なりに解釈しつつ記事にすることにしました。

この記事では、Kotlinのサンプルコードを用いて実装イメージとともに、『継続的デリバリー』で紹介されているアプリケーションドライバレイヤーという考え方について、掘り下げていきたいと思います。

受け入れテストにおけるレイヤー

『継続的デリバリー』の中で、筆者は受け入れテストを以下の3つのレイヤーに分けて説明しています。

  • 実行可能な受け入れ基準
  • テスト実装
  • アプリケーションドライバ

それぞれのレイヤの簡単な説明は以下の通りです。

  • 実行可能な受け入れ基準
    • 名の通り、受け入れテストにおける受け入れの基準を定める
    • 実行可能であることがポイントであり、Gaugeはこの点で親和性が高い
  • テスト実装
    • 実際に処理の呼び出しを行いテストの一連の流れをコントロールする
  • アプリケーションドライバ
    • 受け入れテストの対象となるシステムと直接やり取りをする

私はこの中でもアプリケーションドライバのレイヤーが自動テストの保守性を高める上で大きな役割を果たすのではないかと思いました。

以下でこのレイヤーについて掘り下げてみます。

アプリケーションドライバレイヤーについて

上記の通り、アプリケーションドライバレイヤーは受け入れテストの対象となるシステムと直接やり取りをするレイヤーであり、このレイヤーはどのようにテスト対象とやり取りをするかの詳細を知っていることになります。例えばUIに対するテストであればSelenium、Selenide等を利用してUIに対する操作を行います。

一方で他のレイヤーから見た際に、アプリケーションドライバレイヤーはドメイン言語で表現されたインターフェースを提供するという特徴を持ちます。

すなわち、テストの実装の側から見た場合には、テストの実装から、テストに必要な情報のやり取りについての詳細について知る事はなく、抽象度が高いAPIを通して詳細とやりとりをすることが出来るようになります。

因みにGUIに対するこのようなアプローチはウィンドウドライバパターンとして紹介されており、以下のファウラーの記事でも詳しく知ることができます。

martinfowler.com

なおアプリケーションドライバレイヤー自体はGUIに限らず、API等他のシステムをテスト対象とする場合でも利用できます。

アプリケーションドライバレイヤーのメリット

上記のような特徴を持つアプリケーションドライバレイヤーですが、以下のようなメリットがあると感じました。

保守性が高まる

『継続的デリバリー』内でも紹介されていますが、一番大きなメリットはテストが変更に対して強くなることだと思います。

特にUIに対するテストは、画面の細かな変更に対応するためにテストの修正を余儀なくされる場面が多くあるかと思います。

アプリケーションドライバレイヤーにUIとのやり取りの詳細を押し込めることで、変更に対して強いテストが書きやすくなります。

テストが失敗した際の対応が容易になる

『継続的デリバリー』の同じ章(8章 自動受け入れテスト)の他の節でも紹介されているのですが、適切なレイヤリングによってテストの問題の調査が容易になると考えます。 例えばSeleniumを用いた自動テストでは、テストが失敗する場合に「画面描画を待っていてタイムアウトしてしまった」「セレクタの指定が誤っていた」等の原因から、純粋なアサーションエラーまで幅広い原因が考えられます。

アプリケーションドライバレイヤーが適切に分離されている場合、どのような原因でテストが失敗したのか分かりやすくなり、また部分的にタイムアウトまでの時間を延長する際にも対応する箇所を局所化させることが出来るようになります。

他にもマルチデバイスへの対応がしやすくなる(デスクトップアプリ、Webアプリ、モバイル等)というメリットがあるかなと思いました。

実践

実際にアプリケーションドライバのレイヤーを利用して受け入れテストのリファクタリングを行っていく様子を見ていきたいと思います。

サンプルコードはKotlinとGauge、Selenideを利用して記載していますが、適宜コメントで補足を入れています。

テスト対象

簡単な銀行口座のアプリケーションを考えます。

ユーザーがアプリを立ち上げてログインした際に、現在の口座残高を知ることが出来たり、他の様々なメニューを開くことが出来るようなポータル画面が表示されるようなものをイメージしてください。

以下のようなコンポーネントがあり、そこにユーザーの現在の残高が表示されることをテストすることとします。

  <div>
    <p class="current-balance">{{currentBalance}}</p></div>

まずは愚直に

まずはシンプルに、現在の残高を確かめる実装を書いてみます。

// GaugeはMarkdown形式で書かれたSpecファイルとStepアノテーションが付けられた実装を紐付けることが出来ます
@Step("現在の残高が<balance>円である")
fun assertCurrentBalance(balance: Int) {
  // query selectorを利用して現在の残高を取得しアサーションする
  assertEquals(`$`(".current-balance"), balance)
}

簡潔な実装ですが、受け入れの基準を満たしているかどうかを検証するテストになっています。

一方で画面に対する変更があった場合、その影響を受けやすいコードです。

また「現在の残高(currentBalance)」ということは分かりますが、どのような場面、画面で利用されている残高なのかイメージがつきづらいです。

アプリケーションドライバを導入する

ではアプリケーションドライバレイヤーを導入してリファクタリングをしてみます。

@Step("現在の残高が<balance>円である")
fun assertCurrentBalance(balance: Int) {
  // UserPortalPageDriverというドライバクラスを用意する
  val currentBalance = userPortalPageDriver.getCurrentBalance()
  assertEquals(currentBalance, balance)
}

今回は UserPortalPageDriver というドライバクラスを設けて、ログイン時に初めに表示されるポータルページとの直接のやり取りを担う役割を持たせてみました。 これによって、ドライバクラス内にUIとのやり取りの詳細を隠蔽出来ました。

またクラス名からどのような場面で利用される残高なのかという点がはっきりし、ドメインとしての主張が強まったと思います。

『継続的デリバリー』内ではページ単位よりもさらに細かく、画面を構成するコンポーネント単位でドライバーを用意した例が掲載されていましたが、この辺りはアプリケーションの規模に応じて変えると良さそうだなと思いました。

まとめ

記事にしてみると、プロダクションコードを書く際には意識してきた抽象化、レイヤリングという要素をきちんとテストコードを書く際にも意識する事でより良いテストコードが書ける、という事を改めて実感できました。

『継続的デリバリー』はこの受け入れテストに関する8章でちょうど折り返しにあたるため、引き続き少しずつ読み進めていきたいと思います。