kotlin

はじめに

アメーバピグでサーバーサイドエンジニアをしている木村です。
アメーバピグはサービス開始してから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

メリット
関数型としての完成度が高い(Java8と比べて)
Scalaで書かれたタイプセーフなフレームワークも多数ある
デメリット
関数型の力量でコードに差がでやすい
学習コストが高い

Go

メリット
言語仕様がシンプルで学習コストが低い
プロセスの起動が早い
デメリット
JVM言語ではないので既存のJavaコードとは別プロセスになる
標準でmapやfilterなどの関数がない ※誤りがあったので修正しました

Kotlin

メリット
デフォルトでNotNull型になるのがよさそう
きれいなラムダと高階関数(Java8と比べて)
関数型に寄りすぎていない(文化的なもの?)
デメリット
(2016/12当時)Android開発の一部でしか使われてなかったためGoogleがAndroidで別の言語を押し出した場合に言語自体が一気にすたれる可能性がある

勉強会の実施(5ヶ月くらい)

下記のセットをScala, Go, Kotlinと順にやっていきました。

  • 基本文法の説明
    技術ブログや記事などでレベルにあったサイトを選択し、それを解説しながら進めた
  • 代表的なWebフレームワーク、ORMの説明
  • 実習としてWebの掲示板を作成
  • 言語のいいところ、悪いところなど振り返り

最後までいったらフレームワークや実習課題を変えてさらに何周かしようと思っていましたが実際は1周やったところで言語が決定しました。

以下、各言語の振り返りで挙がった意見、感想です。

Scala

IntelliJを使っているが、同じやり方で開発環境の構築しても上手くいかない人がいて危うさを感じた
ビルドは噂通り重い
Java8にくらべてラムダと高階関数が書きやすい
暗黙の型変換が黒魔術的

Go

ビルド、プロセス起動が噂通り早いのでローカルでのテストがしやすそう
パッケージ管理など意外と整備されてないと感じるところもあった
やはりmapやfilterつかいたい ※誤りがあったので修正しました

Kotlin

言語仕様は全体的に好印象
Scalaほど敷居が高くなくちょうどいい
勉強会期間中にGoogle I/Oでの発表があり当初考えていたデメリットはなくなった

新言語決定

勉強会を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の情報を発信していきたいと思います。

kimura
2014年中途入社のサーバーサイドエンジニアです。PvEのオンラインゲーム、コミュニティサービスなどネット上でのコミュニケーションに興味があります。