HRMOS事業部プロダクト開発部の清水(@kaonash_)です。
現在サーバーサイドKotlinを使って新規事業のプロダクト開発を行っています。
業務ではSpring Bootで開発をしていますが、個人的に活動をする中でJetBrains製のWebフレームワークであるKtorに触る機会があったため、その知見を少し共有させてもらおうと思います。
Ktorとは?
そもそもKtorをご存知でない方はこのページにアクセスされないような気もしますが笑、上述のとおり、JetBrains社のWebフレームワークです。
2018年11月に1.0がリリースされ、軽量・非同期を特徴としています。
KtorでのRoutingの定義方法
まず、前提としてRouting定義の基礎知識です。
Ktorでは、Routingを定義する方法が大きく2つあります。標準のRouting
のみを使う方法と、それにexperimentalな機能であるLocation
を組み合わせる方法です。
Routing単体で定義する場合
Routing
単体で定義する場合はこのような形になります。
|
|
Location + Routing で定義する場合
Routing
にLocation
を組み合わせて使うと、パスパラメータやクエリパラメータをタイプセーフに扱うことが可能になります。
|
|
ちなみにここで使っているget
メソッドは上のRouting単体の場合に使っているget
メソッドとは別のものになります。(前者はio.ktor.locations.get
、後者はio.ktor.routing.get
)
正直このあたりの名称が被っていたりするのがKtorのRoutingちょっとイケてないなと思うポイントの一つなのですが、それはまた別の話。(Location
はまだexperimentalなので、しばらくすればこのあたりも含めて解消されるんじゃないかと期待しています)
実際に使ってみた
さて、前置きが長くなりましたが、ここからが本題です。
実際に開発を進めるにあたって、Location
のリクエストパラメータをタイプセーフに扱える機能は魅力的です。
ので、今回はLocation
を活用した上でRouting定義を書いてみました。
実際出来上がったコードはこんな感じです。
|
|
・・・なんということでしょう。
なぜだかめちゃくちゃ分かりづらいコードが出来上がってしまいました。(実際はもっとたくさんのRouteが存在していてさらにカオス) 開発開始から1ヶ月にして立派な技術的負債の完成です。
ではこのコード、なにが問題なのか考えてみましょう。
1. どのURLがどの処理を呼ぶのか、直感的に理解できない
これが最大の問題点です。たとえば"/users/1/phone_number"というURLでputリクエストが飛んできたとして、どの処理が呼ばれるのか全然わかりません。
本来Routingをまとめて定義することで一覧性を高められるのがKtorの利点のはずなのに、まったく直感的でなくなってしまってます。
上側に定義されているroutingを見て"/users"の中に入りそうなところまではかろうじてわかるものの、Location
側にもroute定義がされており、もはやお手上げ状態。
route定義がrouting
とLocation
に分かれてしまっているが故に、どちらを見ればいいのかよくわからない。
さらには、Location
をネストさせてしまっていることも一層理解しづらさに拍車をかけてしまっています。
2. リクエストパラメータの取得方法に統一性がない
get<UserParam.ItemParam>
の箇所を見てください。
userIdもitemIdもどちらも同じパスパラメータなのに、値の取得方法が変わってしまっています。
|
|
これ、感覚的にはこう記述したいところ。
|
|
ですが、Location
をネストさせた場合、必ず内部のLocation
は外部のLocation
をプロパティに含める必要があるため、こういった書き方しかできません。
改善ポイント
以上の問題点を加味した上で、どのようにRouting定義をするのがよいのか、改めて考えてみました。
結果、現時点で至った結論は、下記の3つ。
- URL定義を
routing
とLocation
に分散させず、Location
に統一する Location
の定義は実際の処理を行う場所に一緒に記述するようにする- ネストは原則使用しない
一時はLocation
自体使わないようにしようかとも思いましたが、やはりタイプセーフにパラメータを扱えるメリットは残したく、上記のような結論になりました。
リファクタリングした結果
上記の内容を踏まえて、リファクタリングした結果がこちらです。(Location
のクラス名も一緒に変更しています)
|
|
Location
をネストさせず、あえて冗長にRoute定義を行うようにしました。
加えて、Location
の定義と実際の処理を同じ場所に記述するようにした結果、どのURLがどの処理に紐付いているのか、かなりわかりやすくなったのではないでしょうか。
問題点であげた、リクエストパラメータの取得方法の統一性に関しても、下記のように感覚的な取得が可能になっています。
|
|
KtorでのRoutingについてのFAQ
最後に、KtorでのRoutingについて多くの方が疑問/不安を持たれるであろうポイントについて自分なりの見解を述べたいと思います。
疑問点1. 規模が大きくなった場合、1つのファイルにRoutingをまとめる書き方ってスケールしなくない?
たしかにRouteが数十個を超えるようになった場合、1つのファイルにまとめて記述することには限界があります。が、これは簡単に解消できます。
|
|
このように、Routeクラスの拡張関数を作成する形で定義してあげれば、定義を細かく分割していくことができます。
分割した拡張関数ごとに別のファイルを用意するようにすれば、規模が大きくなってもファイルサイズが膨張することなくスケールさせることが可能です。
疑問点2. Locationをネストさせて階層構造にしておいたほうが、機能ごとにRouteをまとめられるし、不用意なRouteの衝突も防ぎやすいのでは?
これはまさにおっしゃるとおりで、だからこそ僕も最初は階層構造で定義したわけなんですが、上述した「リクエストパラメータの取得方法に統一性がなくなってしまう」問題点と天秤にかけた結果、「現時点では」Locationをネストさせないほうが読みやすい/書きやすいコードになるのでは、と判断しました。
疑問点3. ぶっちゃけ、Spring Bootと比べてどうなの?
フレームワークにKtorを採用したことについて現状概ね満足しているのですが、論点をRoutingに絞ると、「どっこいどっこい」もしくは「まだ若干Spring Bootのほうが使いやすいかも」というのが正直な感想です。
僕自身がSpring Bootを使っていて一番困るのは、「このRoute、どの処理を呼び出すの?」を探す時です。
Controllerごとに@RequestMapping
アノテーションでpathを指定していますが、正直これがかなり探しづらい。(※個人の感想です)
特に、@RequestMapping
で/users/{userId}/
を指定したControllerと/users/{userId}/items/{itemId}
を指定したControllerが別々にあったりするとなおさら迷路に迷い込むことになります。
これに対してKtorでは、Route定義をまとめることができるため、Spring Bootと比べて呼び出す処理が探しやすいと感じます。
一方で先に述べたように、Routeをネストさせつつ、うまくパスパラメータを取得できるという点ではSpring Bootに分があります。
加えて、Ktorだとrouting
とLocation
どちらでもRoute定義ができてしまうがゆえに、書き方を事前に意思統一しておかないとコードが汚れやすいという問題点もあります。
ただしこのあたりの問題点はversion upに伴って改善していってくれるのでは、と期待しています。(確証はない)
まとめ
Ktor自体はまだ1.0が出て間もなく、知見もなかなか集めづらい状況ではあります。
ですがKotlin製のWebフレームワークとしてはとても注目度も高く、これから実用事例も出てくるだろうと思いますので、もっといろんな人柱・・・もとい、様々なベストプラクティスが出てきてくれるといいなと思います。
Let’s enjoy Kotlin!!