プロジェクトの最初のころに発生する大変さ

最後のアジェンダです。プロジェクトの中で発生した大変さや知見、別言語に書き換えるメリットについて話していきます。先ほどのスケジュールの図をもう一度お見せします。これから時系列順に話していきます。

まず、プロジェクトの初期の大変さについて話します。そのあと、プロジェクトが中盤から後半になるにつれて明るみに出てきた反省点について話します。そしてリリースに関する大変さを話したあと、別言語での書き換えによって得たメリットについて話します。

ではまず、プロジェクトの最初のころに発生する大変さについて話します。

この話に入る前に、みなさんに質問したいことがあります。プロダクトの書き換えというのは、引っ越しのようなものです。まず引っ越しの箱詰め作業に入る時に、最初にみなさんは何をしますか? 今あるものをリストアップして、捨てるものを選びますよね。すべての荷物を梱包して引っ越す人はいないと思います。

ということで、今回についても、まずは捨てる機能の洗い出しをする大変さがありました。8年続くプロダクトには、実装されているがすでに使われていない複雑な機能がたくさんあります。これらが使われているのか、もういらないのか、調べて捨てることを決めて、新しい仕様に反映するというのが大変な作業でした。

また、使われていない機能だけではなく、現在も使っているが、当時の工数の関係で無理やり作った部分についても、今回どのように作るのか検討する必要がありました。

2つ目に、捨てた機能について、メンバー全員で認識を合わせるのが大変でした。実際何が起きたのかというと、誰かが調べて捨てることになった機能について、ほかのメンバーが実装してしまうことが起こりました。

なぜこのようなことが起きたのかというと、ドキュメントには、満たすべき仕様は書いていたものの、削除した機能の記述がなかったり不十分だったからです。そして、Perlのコードを読みながらKotlinに移行する時に誤って実装してしまいました。

そもそも、なぜPerlのコードを読む必要があったのかについては、次のページで説明します。削除した機能の共有についてどのように対策したのかというと、削除した機能についてもWikiなどに書くようにしたのと、Perlのコードにコメントを書くようにしました。

具体的には、Kotlinに移行する時のためのブランチをPerlに作って、ドラフトプルリクエストを作って、コメントを書くようにしました。これはかなりよい方法だったのでおすすめです。

KotlinエンジニアにとってのPerlの難しさ

ここまで、捨てる機能についての話をしてきました。次は、KotlinエンジニアにとってのPerlの難しさについて話します。

まず、なぜKotlinエンジニアがPerlのコードを読む必要があったのかというと、仕様がすべてドキュメント化されていないからです。なぜ書かれていないのかというと、機能がかなりのスピード感で追加や変更がされていた時期に、時間とヒューマンリソースがなかったためです。

もう1つは、スポット的に必要になった特殊な機能があるからです。これは、どのプロダクトについてもあるケースで、仕方のない側面もあるとは思いますが、時間をかけて取り組んでいく必要がある話です。そして、ポイントクラブでは、今回のKotlinへの書き換えを機に、ドキュメントの整理にも力を注ぎ始めました。

ということで、KotlinエンジニアがPerlを読む上での苦労話を少しだけ紹介します。重ねて言いますが、私はPerlは好きなので、Perlを悪く言っているわけではないです。ここは半分笑い話だと思って聞いてください。

まず、Difined-or演算子について話します。図のコードに「//」がありますよね。これ、コメントだと思いますか? 実はPerlでは、コメントではないです。Difined-or演算子といいます。

どういうものかというと、//の前にある$params{content_type}が定義されていればそれを使うし、なければ後ろの'tsv'の値を入れる、というような演算子になっています。これについては、Kotlinのエンジニアがみんなハマっていたので、ちょっとおもしろかったです。

次に、記号についてです。例えば右のコードなんですが、Perlを書いている人ならそんなに難しくない、何をやっているかなんとなく雰囲気はわかるようなコードです。ですが、Perlを書いていない人がこれを見ると、記号の量に圧倒されて、何をやっているかわかりづらいコードです。$や@や%がたくさん出てくるコードになっています。

そして最後は、hashrefの中身がわかりづらいということです。これはPerlに限らず、動的型付け言語ゆえの難しさです。hashrefというのはハッシュのリファレンスで、ここでは簡単に辞書型だと思っていただければ大丈夫です。

一番下のsub another_logicと書いてあるところを見てください。subというのは、サブルーチンといって、別言語の関数にあたるものなのですが、$dataの中身は何でしょうか?

これを知るためには、another_logicの呼び出し元のコードを追っていく必要があります。図だと、真ん中のlogicの中を見るとcreate_dataで使われていることがわかるので、さらにそちらを読みにいく、というようにたくさん読む必要があります。ここが大変でした。

プロジェクトの初期に起きる大変さの最後に、PerlエンジニアにとってのSpring Boot(Kotlin)の難しさを話します。Kotlinというよりも、Spring Bootに慣れるのに苦労した気がしました。Kotlin自体は、Perlエンジニアも比較的すぐ慣れた気がします。

よく引っ掛かった事例として、NoClassDefFoundErrorに出くわしました。Perlだったら、「import something」と書けばどこでもインポートできたのですが、Spring Bootでは、「import something」と書くだけだと、NoClassDefFoundErrorになってしまう場合があります。

これについては、BeanやDI、@ComponentScanなど、Spring Bootを学んで適切にインポートする必要があって、引っ掛かりました。

プロジェクト後半になるつれわかってきた反省点

プロジェクト初期の大変さの話は、ここまでです。プロジェクト後半になるにつれわかってきた反省点について話します。

まず1つ目が、nullableを多用し過ぎたということです。背景として、Perlの変数は、Kotlinでいうところのnullableのようなものです。Perlの変数は、どこでもundefになり得ます。undefというのは、Kotlinのnull相当のものです。

どこでもundefになり得るので、常にdefinedチェックをする必要がありました。今回のプロジェクトでは、基本的にロジックをそろえてKotlinに移植を進めていたので、PerlをKotlinに書き換える上で、変数をnullableとして定義しがちでした。

nullableとnon-nullについて知らない人がいると思うので、軽く説明します。non-nullは、次の図の上にあるように、nullが入らない変数です。nullを入れようとするとコンパイルエラーになります。一方でnullableは、nullを許容する変数です。String?のようにデータ型の末尾に「?」をつけるとnullableを宣言したことになります。nullableは、nullをセットしてもエラーが出ないものです。

では、nullableをたくさん使うと何が問題かというと、nullableが多いということはnullチェックも必然的に多くなってしまうことが問題です。一度nullチェックしたのに、同じ変数を不必要にnullチェックするのは、ナンセンスだと思います。

2つ目の問題として、nullableをnon-nullに変換する方法について考える必要があります。例えば、not-null assertion operatorを使う時には注意が必要です。not-null assertion operatorは、nullだったらNullPointerExceptionが飛ぶ演算子で、ビックリマークを2つ並べて書きます。

not-null assertion operatorのあまりよくない例について話します。例えば、someMethodというメソッドがあって、validateDataを呼び出したあとに、doSomethingを呼び出します。validateDataの中でcampaign.idのnullチェックを行っています。

そして、doSomethingの中で、campaign.idに対して、not-null assertion operatorを使っています。これは、validateDataメソッドでnullチェックをしているから、campaign.idがnullにならない前提でdoSomethingメソッドを実装したコードになっています。

つまりnullチェックが、not-null assertion operatorと離れたところに書いてあるわけです。これの何がよくないかというと、想像に難くないと思うのですが、図のようなanotherMethodを追加することを考えましょう。doSomethingメソッドをcampaign.idのnullチェックをせずに呼び出してしまいました。

その結果、not-null assertion operatorの箇所で、想定外のNullPointerExceptionが起き得る状態になってしまいました。これは当然、開発者が気をつければよいという話なのですが、注意しないとこういう問題は起きます。

結論、どうするのがいいかというと、可能な限りnon-nullを使うべきです。具体的な例としては、Controller層でnullチェックをして、Service層にはnon-nullで渡すなどして、nullableを扱う範囲を狭めるのがよいです。

もう1つは、not-null assertion operatorを使う時には、将来的にNullPointerExceptionが飛ぶ可能性をちゃんと考慮して、実装する必要があります。

とは言えnullableがあったおかげで、PerlからKotlinにガシガシとコードを移行できた点で、Perlから移行する上ではやさしい言語だなと感じました。

似たような意味の変数の扱い

反省点の2つ目に、似たような意味の変数の扱いについて話します。私たちのプロダクトには、digestという複数の意味を持つ変数があります。digestは、ユーザーとキャンペーンの組み合わせごとに発行されるトークンのようなものです。これについては特に覚える必要はなくて、digestという名前でいろいろな種類があるんだな、とわかってもらればよいです。

例えば、Limited digest、Unlimited digest、LINE digest、External LINE digestのようなものがありました。覚えなくて大丈夫です。プロジェクトの初期、これらのdigestをすべてString型で扱っていました。その結果、「このコードのdigestってどれだろう?」と、開発者が混乱してしまいました。

この混乱に対処するために、私たちはtypealiasを使い始めました。typealiasというのは、次の図の上にあるように「typealias LINEDigestString = String」のように書きます。これはStringへのエイリアスであることを宣言しているわけです。

次の図の下を見てもらって共感してもらえるとうれしいのですが、typealiasを使う前の左の図と比べて、右の図はtypealiasを使うだけでだいぶわかりやすくなったと思います。しかしtypealiasはあくまでもエイリアスで、ここではString型のエイリアスなので、文字列ならどんな値でも入ってしまいます。

typealiasを使っていたのですが、本当にやりたかったこととしては、インラインクラスを使うことでした。インラインクラスとは、画面に映しているようにvalue class Nameなどと書いてその長さを制限したり、メソッドを生やすことができるやつで、今回は時間がなくて書き換え切れなかったのですが、今後インラインクラスに書き換えていこうと考えています。

ビジネスロジックが外の世界のことを知ってしまっている

ここまでが、Kotlinの書き方に関する反省点でした。3つ目は設計の話です。ビジネスロジックが外の世界のことを知ってしまっている、ということについて話します。

これは、クリーンアーキテクチャやヘキサゴナルアーキテクチャなどでも言われていて、知っている人もいるかもしれませんが、事例として紹介します。

私たちのプロジェクトでどんな状況だったのかというと、一部のビジネスロジックが、io.grpcパッケージにあるStatusRuntimeExceptionの例外を投げていました。言い換えると、ビジネスロジックが、外の世界とどのように通信しているのか知っている状況だったということです。

その結果出てきた問題について話します。あるロジックを別のアプリケーションでも使いたいという、よくあることが発生したとき、そのアプリケーションは、gRPCを使わないアプリケーションだったのです。gRPCを使わないそのアプリケーションでも、io.grpcパッケージを依存に追加すべきだったのでしょうか?

いえ、そうではありません。このStatusRuntimeExceptionのためだけにio.grpcを依存に追加すべきできなくて、共通化したいロジックでは、StatusRuntimeExceptionを使わないようにすべきでした。そしてその結果、今回は共通部分に切り出すのが難しい、という状況が発生しました。

ではなぜ、このアンチパターンにはまってしまったのかというと、プロジェクト初期は、とにかく正常系の動作が動くコードをガシガシと書いていました。

そのため、例外周りの挙動に十分注意が払えていなかったことと、gRPC依存のコードをビジネスロジックから取り除くことを意識できていなかったこと、そして将来的にロジックをgRPC以外のアプリケーションと共通化することを十分に考えられていなかった、という状況でした。

そして結局、いざそういったロジックを共通化したいとなった時、このへんをキレイにする時間がないという状況になりました。

どうすればよかったのかというと、今回のケースについては、StatusRuntimeExceptionを共通で使うロジックでは使わないこと。そして、外の世界とどのように通信しているか知っている層と知らない層をきちんと分けることを意識したほうがよかったです。

これを一般化すると、よく言われているようにビジネスロジックは、外の世界のことを知らない状態にするのがよいです、ということが再確認できた事例でした。

リリースに関する難しさ

リリースに関する難しさについて、2つだけ話をします。

現行のPerlシステムを考慮した「REAL QA」が大変でした。REAL QAというのは、プロダクション環境のQAのことで、我々のチームではREAL QAと呼んだりしています。REAL QAは、もしすべてのデータベースをリプレイスするなら、現行システムのことを考える必要はないので簡単だったかと思うのですが、今回はそうはいきませんでした。

現行システムからの移行方法を検討している中で、ベストな選択肢として現行システムのデータベースを使うことにしたことで起きた、大変さでした。もしすべてのデータベースをリプレイスする方法を採用していたら、また別の大変さが発生していたと思います。

今回、どのようなことに気をつけたのかというと、1つ目は、現行システムと非互換なデータが発生しないことを確認するために、BETA環境においてPerlとKotlinを長い期間、今回でいうと1ヶ月半くらい並行で動かしていました。この期間に多くの問題について気づきました。この期間は、十分に長めに取るのがよさそうだという知見も得られました。

そして、現行システムのほかに新しいアプリケーションを追加するわけなので、MySQLの接続数が問題ないかなどをていねいに確認しました。

リリースに関する2つ目の大変だったことは、Perlに残した部分の開発です。全体を1回のリリースで総入れ替えすればいいのですが、今回はそうはいきませんでした。もしすべてを総入れ替えするなら、あと3、4ヶ月必要でした。それまで、サービスを運営する上で、ほかの要望を止めるのは難しいです。

部分的にリリースして、ほかの要件にも対応していくのがサービスにとってもよいはずなので、そのために努力が必要でした。そして今回は、Perlに何を残してどう手を入れるのか、ということを考えることに注力しました。

別言語に書き換えたことによるメリット

最後に、別言語に書き換えたことによるメリットに触れます。4つ話をします。

1つ目に、使われていない機能などが全部1回リフレッシュできる点がよかったです。

2つ目に、このプロダクトの開発にそれまで携わっていなかったエンジニアがジョインすることで、ドキュメント化されていない仕様も洗い出されました。そして今回はドキュメントに落とすことはかなり意識したので、誰か1人しか知らないみたいな機能はなくなりました。

3つ目に、弊社の開発組織でよく使われているKotlinに書き換えたことで、知見の交換やレビューが活発にできるのと、エンジニアリソースの流動性も上がってすごくよかったと感じます。

4つ目に、今回は別言語に書き換えるタイミングで現代的なアーキテクチャ、技術セットを使ったので、エンジニアスキルとしてもかなりアップデートできてよかったです。

不要な機能の削除やコードレビュー、ドキュメントの充実化

まとめです。本日はLINEポイントクラブにおける8年続くPerlプロダクトをKotlinに書き換えた話をしました。長くサービスを運営していると、必ずシステムは複雑化していきます。これは誰かが悪いわけではなくて、その時その時で最善の選択をした結果、必然的にこうなります。そして、複雑化するとメンテナンスが大変になってきます。

別言語に書き換える上での大変さと解決方法を一部紹介しました。そして、別言語に書き換えたことによるメリットや知見が多く得られました。みなさんのプロダクトでも、近い未来だけではなく数年後まで見据えて、不要な機能の削除やコードレビュー、ドキュメントの充実化にぜひ取り組んでください。

以上で発表を終わります。ご清聴いただきありがとうございました。