はじめに
アメーバピグでサーバーサイドエンジニアをしている木村です。
アメーバピグはサービス開始してから8年半となるサービスでサーバーサイドはJavaで開発されています。そこに先日新言語としてKotlinを導入したので経緯や導入までの道のりを紹介したいと思います。また、今回はJavaとKotlinの技術面の比較よりも導入までのプロセスに焦点を当てたいと思います。
導入の動機
Kotlinといえば、Google I/O 2017でAndroidの公式言語としてサポートされることが発表されましたが、実はその半年くらい前から私のプロジェクトではJava以外の言語の導入を検討していました。社内でのサーバーサイド言語のトレンドはJava→Node.js→Goと移ってきていてJavaはやや置いていかれてる印象です。
Javaはいろんな意味で安定した言語である反面コードが冗長で、デキるエンジニアほどJava以外の言語にも興味を持っている状況なので組織として技術的なモチベーションを上げる必要があると感じていました。
迷い〜周りを巻き込む
新言語の導入を考えた時点ではまだKotlinに決めたわけではありませんでした。候補としてはScala・Go・Kotlinを考えていて、この3つの言語の勉強会を順にすることでチームメンバーと議論しながら新言語の決定をすることにしました。
自分はチームのエンジニアリーダーという導入技術を決定できる立場でしたが、トップダウンで言語を決定して導入するよりも「迷い」も含めてメンバーと共有し議論して決定することで、目的であるモチベーションアップにも繋がったように感じています。もし上司を説得する必要がある場合でも、チームの機運を高めるという意味では勉強会から始めてみるというのはいい方法ではないでしょうか。
ちなみに検討時点ではそれぞれのメリット・デメリットを以下のように考えていました(印象も含みます)。
Scala |
メリット |
Go |
メリット |
Kotlin |
メリット |
勉強会の実施(5ヶ月くらい)
下記のセットをScala, Go, Kotlinと順にやっていきました。
- 基本文法の説明
技術ブログや記事などでレベルにあったサイトを選択し、それを解説しながら進めた - 代表的なWebフレームワーク、ORMの説明
- 実習としてWebの掲示板を作成
- 言語のいいところ、悪いところなど振り返り
最後までいったらフレームワークや実習課題を変えてさらに何周かしようと思っていましたが実際は1周やったところで言語が決定しました。
以下、各言語の振り返りで挙がった意見、感想です。
Scala |
IntelliJを使っているが、同じやり方で開発環境の構築しても上手くいかない人がいて危うさを感じた |
Go |
ビルド、プロセス起動が噂通り早いのでローカルでのテストがしやすそう |
Kotlin |
言語仕様は全体的に好印象 |
新言語決定
勉強会を1周終えたところでどの言語がいいか議論しました。メリット・デメリット挙げてみてKotlinがよさそうだったのと、Google I/Oの直後でKotlinきてるね!ということもあり全員納得でKotlinに決定しました。
JavaとKotlinの比較
コードにはあまりふれない予定でしたが軽くサンプルだけ紹介します。
サンプル①-1 Entity : Java
public class Friendship {
/** ユーザーID */
private int userId;
/** 相手のユーザーID */
private int targetId;
/** 親友フラグ */
private boolean goodFriend;
/** 友だちになった日時 */
private long time;
@SomeAnnotation
public int getUserId() {
return userId;
}
@SomeAnnotation
public int getTargetId() {
return targetId;
}
@SomeAnnotation
public boolean isGoodFriend() {
return goodFriend;
}
@SomeAnnotation
public long getTime() {
return time;
}
public Date getTimeAsDate(){
return new Date(time);
}
public void setGoodFriend(boolean goodFriend) {
this.goodFriend = goodFriend;
}
public void setTime(long time) {
this.time = time;
}
public void setTargetId(int targetId) {
this.targetId = targetId;
}
public void setUserId(int userId) {
this.userId = userId;
}
}
サンプル①-2 Entity : Kotlin
open class Friendship {
// ※ORMの都合もありNullableにしています。
/** ユーザのユーザーID */
@get: SomeAnnotation
var userId: Int? = null
/** 相手のユーザーID */
@get: SomeAnnotation
var targetId: Int? = null
/** 親友フラグ */
@get: SomeAnnotation
var goodFriend: Boolean? = null
/** 友だちになった日時 */
@get: SomeAnnotation
var time: Long? = null
fun getTimeAsDate(): Date {
return Date(time!!)
}
}
コードが短くなり本質の部分だけが見えやすくなりますね。
サンプル②-1 コレクションに対する処理 : Java
FriendshipのListから親友フラグがtrueのものだけを抜き出す処理です。
public List<Friendship> findGoodFriendship(int userId) {
List<Friendship> friendshipList = getAllFriendship(userId);
List<Friendship> goodFriendList = new ArrayList<>();
for (Friendship friendship : friendshipList) {
if (friendship.isGoodFriend()) {
goodFriendList.add(friendship);
}
}
return goodFriendList;
}
サンプル②-2 コレクションに対する処理 : Java8 Stream API
public List<Friendship> findGoodFriendship(int userId) {
List<Friendship> friendshipList = getAllFriendship(userId);
return friendshipList.stream().filter(f -> f.isStrong()).collect(Collectors.toList());
}
stream()とcollect()が冗長ですね。
サンプル②-3 コレクションの繰り返し : Kotlin
fun findGoodFriendship(userId: Int): List<Friendship> {
val friendshipList = getAllFriendship(userId)
return friendshipList.filter { it.goodFriend }
}
この例でもKotlinの処理が無駄なコードが少なく理解しやすいのではないかと思います。
導入にあたって決めた事
・サブシステムとして独立させない
ある程度独立性のある機能をサブシステムとして切り出して、その開発を新言語で、というのは考えがちですが、サブシステム化すると新言語をさわる人がどうしても限定されてしまって半年経ってもその言語で書いてない人ができきたり、たまにしか書かないから除々に敬遠されていったりということになりそうだったので、これから新規でつくるクラスはどの部分でもKotlinで書いていいということにしました。
・既存JavaコードをすべてKotlinに置き換えるということはしない
エンジニアとしてすべてを置き換えたくなる衝動に駆られますが、なにせ8年も運用していると既存のコードが膨大なためリグレッションテストを踏まえた工数とリスクを考えると、そのことに時間を使うよりは事業的にメリットのあること、ユーザー体験が向上するものに時間を使ったほうがよいと判断しました。もちろん置き換えるメリットのほうが大きければそのほうがよいと思います。
・Gradleの導入はしない
もともとMavenを使っていましたが、そのままいくことにしました。Gradleにすると依存ライブラリの優先順位が変わってしまうため、これも工数とリスクによるデメリットのほうが大きいと判断しました。
実際の導入(1ヶ月くらい)
・pom.xmlの編集
https://kotlinlang.org/docs/reference/using-maven.html
公式のこのページを参考にしましたが、既存のJavaプロジェクトでKotlinを動かすのは驚くほど簡単でした。
・開発環境の整備
IntelliJを使っているのでデフォルトで新規クラスを作成するときにKotlinも選べる状態で、特にやることありませんでした。その他ビルドやリリースのジョブなども変更が必要な箇所はありませんでした。
・既存機能のコピーをKotlinで実装
まずは既存のJavaクラスのコピーとしてKotlin版をつくることで既存機能に影響を与えずに本番に導入しました。またServiceだけをKotlinで追加しても動かないので社内管理画面やバッチの参照系の機能のコピーを作りました。
Java→KotlinのコンバートはIntelliJの変換機能を使い、後はNullableになってるけどNotNullにできるところなど気に入らないところを修正していく程度でした。
・対象のロールを除々に広げる
社内管理画面やバッチなどサービス影響の低いプロセスのロールへリリースし、問題ないことを確認して除々に重要度の高いロールへリリースしていきました。環境の差異など気にして慎重に進めました。
・JavaとKotlinの相互呼び出しの検証
ロールの検証と平行してJavaとの相互運用も考慮しController、Service、Daoの中のServiceだけKotlinにするなど何パターンか検証しました。Javaと100%互換が謳われていますが、ここもサービス影響が少なくなるように慎重に進めました。
ここまで終わったところで「正式導入」とし、今後新規のクラスはJavaかKotlinか担当者が好きに選べるようにしました。
その後
本番導入後2ヶ月ほど経ちましたが、特にトラブルもなく、新規のクラスはほぼすべてKotlinで書かれている状況で、新言語の導入としては成功といえると思います。勉強会のほうは継続していて、フィールドをどう宣言すればNotNullにできるか、など実用的なTipsを共有したりしています。
最後に
私自身すっかりKotlinが好きになったので、今後も機会があればKotlinの情報を発信していきたいと思います。