Kotlin向けのWebフレームワーク、JoobyとKtorを比べてみる

Avatar photo
エンジニア 伊藤
2023年01月06日


弊社の案件のいくつかはバックエンドをKotlin+Joobyで開発しています。
Kotlin向けのWebフレームワークはいくつかありますが、Ktorの新しいバージョンが出ていたのでどんなものか比べてみました。

Joobyは軽量なマイクロフレームワークと言われており、旧来のフレームワークのように設定ファイル等を大量に用意しなくても使用することができます。
アプリケーションサーバを内蔵しているので別途用意しなくてもすぐに起動できます。

Joobyは以前からKotlinをサポートしておりちょっとしたシステムをkotlinで作りたいというのであればよい選択肢かと思います。

単純にテキストを返すだけならこんなコードで動作します。

fun main(args: Array) {
    runApp(args){
        get("/") {
            "Welcome to Jooby!"
        }
    }
}

いっぽう、Ktor(ケイターと読むらしいです)はKotlinの開発元であるJetBrains社が開発しており期待されていたのですが、開発が遅れておりなかなか普及しなかったようです。
しかし、Ktorも2022年4月頃にようやくバージョン2.0.0がリリースされており、今後は普及していくのではないかと思われます。

KtorはKotlinで開発されており、非同期処理をサポートしています。アクセス数の多いシステムに向いているかもしれません。

Ktorで単純にテキストを返すコードは以下のようになります。

fun main() {
    embeddedServer(Netty, port = 8080) {
        routing {
            get("/") {
                call.respondText("Hello World!")
            }
        }
    }.start(wait = true)
}

ここでは、JSON形式でデータを返すAPIを例にJoobyとKtorのコードを比べてみます。

Joobyでの実装例

コントローラーの実装例です。
Joobyもコントローラーを使うとSpring Frameworkと似たようなコードになります。
最初の例ではmain関数に処理を記述していましたが、機能が増えると管理が大変なのでコントローラークラスを作成して機能毎に分割するようにしています。

なお、このコードはopenapiで生成したコードを元に修正しています。

class UsersApi {

    private val userList = ArrayList()

    init {
        userList.add(User(id = 1, name = "hoge"))
        userList.add(User(id = 2, name = "piyo"))
    }

    @GET
    @Path("/")
    @Produces("application/json")
    fun usersGet(): Users {
        return Users(users = userList)
    }

    @GET
    @Path("{id}")
    @Produces("application/json")
    fun usersIdGet(@PathParam("id") id: kotlin.Int): User {
        return userList.firstOrNull { it.id == id } ?: throw Exception("Not found")
    }

    @POST
    @Path("{id}")
    @Consumes("application/json")
    @Produces("application/json")
    fun usersIdPost(@PathParam("id") id: kotlin.Int, user: User): User {
        userList.removeIf { it.id == id }
        userList.add(user)
        return user
    }
}

usersGetメソッドは単純にユーザーの一覧を返す処理です。
コントローラーのメソッドには@GETや@POSTのようなHTTPメソッドと、パスをアノテーションで指定します。
また、JSON形式でレスポンスを返すため@ProducesでContent-Typeを指定します。

    @GET
    @Path("/")
    @Produces("application/json")
    fun usersGet(): Users {
        //省略...
    }

usersIdGetメソッドはパスパラメータでidを取得し、該当するユーザー情報を返します。
パスパラメータは”{id}”のように波括弧で囲います。

    @Path("{id}")
    @Produces("application/json")
    fun usersIdGet(@PathParam("id") id: kotlin.Int): User {
        //省略...
    }

usersIdPostメソッドは送信されたユーザー情報を追加・更新します。
リクエストでJSONを送信するため、Content-Typeを@Consumesで指定しています。

    @POST
    @Path("{id}")
    @Consumes("application/json")
    @Produces("application/json")
    fun usersIdPost(@PathParam("id") id: kotlin.Int, user: User): User {
        //省略...
    }

ktorでの実装例

先ほどのJoobyのコントローラーと同様のものをktorで実装すると以下のようになります。

※ここではopenapiで生成したコードを流用しているのでLocation機能を使用していますが、新しいバージョンではResouceを使うことが推奨になっているようです。

fun Route.UsersApi() {
    val userList = ArrayList()

    userList.add(User(id = 1, name = "hoge"))
    userList.add(User(id = 2, name = "piyo"))

    get<Paths.usersGet> {
        val users = Users(
            users = userList
        )

        call.respond(users)
    }

    get<Paths.usersIdGet> {
        val user = userList.find { user -> user.id == it.id }
        if (user == null) {
            call.respondText("Not found")
        } else {
            call.respond(user)
        }
    }

    post<Paths.usersIdPost> {

        val req = call.receive()

        val user = req.user

        if (user == null) {
            call.respondText("Invalid parameter")
        } else {
            if (userList.any { p -> p.id == user.id }) {
                userList.removeIf { p -> p.id == user.id }
            }

            userList.add(user)
            call.respond(user)
        }
    }

}

Ktorではコントローラーの機能がないため、代わりにLocationという機能を使ってコントローラーっぽく書いています。

get<Paths.usersGet> ではユーザーの一覧を返します。
アノテーションの指定はコントローラーではなく別途指定するため、ここではgetメソッドであることを表す”get”を指定しています。

    get<Paths.usersGet> {
        //省略...
    }

では、パスはどこで指定するかというと、Paths.usersGetのほうで指定します。
@Locationアノテーションでパスを指定します。
注意点として、コントローラーで使用するクラスはシリアライズできる必要があるので、@Serializableを指定します。

object Paths {
    @Serializable
    @Location("/users")
    class usersGet

    //省略...
}

同様に、ユーザー詳細を返すget<Paths.usersIdGet>の場合はパスパラメータでidを取得しています。
idはit.idから取得できます。
この”it”という変数はkotlin特有の機能で、”get”または”post”メソッド内の括弧内では暗黙的に定義されています。

    get<Paths.usersIdGet> {
        val user = userList.find { user -> user.id == it.id }
        
        //省略...
    }

Paths.usersIdGetの@Locationでは”{id}”でパスパラメータを指定しています。

object Paths {
    //省略...

    @Serializable
    @Location("/users/{id}")
    class usersIdGet(val id: kotlin.Int)

    //省略...
}

ユーザー情報を追加・保存するpost<Paths.usersIdPost>では少々注意が必要です。
POSTメソッドを使用した場合、BODYデータはcall.receive()で取得する必要があります。

    post<Paths.usersIdPost> {

        val req = call.receive()

        //省略...
    }

比較

JoobyもKtorもWebアプリケーションとして基本的な機能には対応しているので、書き方の差はあってもだいたい同じように開発できそうです。

Ktorはコントローラーでのルーティングに対応していないので、openapiで生成したコードとはいささか相性が悪そうです。
openapiを使用するならテンプレートをカスタマイズしたほうがよいかもしれません。

サンプルコード

今回動作確認したJoobyとKtorのサンプルは以下にあります。

Jooby: https://github.com/ito-mmj/jooby-sample

Ktor: https://github.com/ito-mmj/ktor-sample

2023年01月06日

エンジニア 伊藤 |

«
このページのトップへ