ジャバの異常な愛情 またはSpringはいかにしてモダンであることを止めて時代遅れになったのか

Spring以前

業務で使うシステムはサーバー間で連携することが多い。2019年現在ではREST apiに対してjsonやprotocolbufferで呼び出す事が当たり前のように行われているが、まだjsonも発見されていない時代はもっと複雑な仕組みが取られていた1。異機種間でやりとりするためのCORBAや、機種に依存しないデータプロトコルのASN.1なども利用されていたが、仕様は複雑でそれぞれをハンドリングするライブラリは有償で売られ、ベンダーからサポートを受けながら使用するようなものだった。

RMI

Javaの世界ではJava同士でやりとりするためのRMIが定義され、比較的に楽にRPCできるようになった2。とはいえhttpでrestをコールすることに比べたらアホみたいな複雑さである。
https://docs.oracle.com/javase/jp/1.3/guide/rmi/getstart.doc.html

J2EE

そのRMIの使い方に一定のルールを設け、EJBを始め様々な指針を提示したのがJ2EEである。実装者がそれぞれに試行錯誤して苦労するような無駄は減ったが、代償として仕様はさらに複雑化した。

Spring

獲得できる機能に対して、設定や規約が悪夢のように複雑なJ2EEのアンチテーゼとして登場したのがSpringである。DIと組み合わせることでソースを綺麗に保つことができた。Clientクラスからサーバー上にあるServer#method()を呼び出すソースは以下だ。

class Client {
  private Server server;
  public void setServer(Server value) {
    server = value;
  }
  public void action(){
    server.method();
  }
}

もちろん適切な設定ファイルが必要になる3。が、開発時はローカル(というか同一JVM内)にあるserver.method()を直接呼び出して検証し、設定ファイルを差し替えるだけででserverの中身がテスト用のモックになったり本番用のリモート呼び出し版になったりするのは便利だった。素のRMIなら開発時もローカルにRMIサーバーを上げてrmi://localhostで接続し、テスト環境では設定ファイルでrmi://serverhostに変更するだけとか、RMIのオーバーヘッドが嫌ならデバッグフラグを立てておき呼び出し時にif文でローカル実行かRPCかを呼び分けるようにする程度だったから、結構な進化である。RMIのオーバーヘッドはhttpに較べればかなり小さいが、開発時にもいちいち上げなければいけないのは面倒だ。メインサービスのhttpは忘れないが、別途rmiserverだけ起動、とかいう手順が必要だと忘れがちになったり、逆に常に同時に立ち上げるスクリプトを使っていると、httpが生きているのにrmiにつながらない時に開発メンバーが「どうすればいいかわからない」状態になることがある。

SpringはDIコンテナではあるが、そのDIでリモート呼出を簡単にしてくれた事に大きな功績があった。このように実装を切り分ける方法にはDIの前にServiceLocatorがあった。先程のサンプルを書き換えるとこうなる。

class Client {
  private Server server = Locator.find(Service.class);
  public void action(){
    server.method();
  }
}

アプリの初期化時にFactoryに適切なServerクラスのインスタンスを登録しておく。もちろん設定ファイルを見てローカルかリモートか変更できるようになる。デメリットはServiceLocatorに対する依存が発生することだ。細かいことは割愛するがマーチン・ファウラーのブログの日本語訳のリンクを貼っておくので読んでほしい4
Inversion of Control コンテナと Dependency Injection パターン

歴史は繰り返す

メインフレームにダム端末を繋いでいた時代から、コンピュータの性能が向上したことでダウンサイジングがブームとなりUNIXワークステーションやPCで処理を行う流れとなり、管理の煩雑さやセキュリティの問題から巨大なサーバーにシンクライアントや仮想デスクトップで接続するという逆行が起きている。データ通信の世界ではシリアル通信から、同時接続で速度を向上させるためパラレルの通信技術が発展した。しかし技術革新でかなり高い周波数で通信できるようになると、今度は複数ある通信経路の同期をとるのが大変になりシリアル通信のSATAに揺り戻しが起きている。

プログラミング言語の世界も同じだ。C言語が世界を席巻していた頃、可読性の低いソースが氾濫していた。インターネットはまだ普及していないし、接続しても毒にも薬にもならない企業ページか、カウンターcgiの設置された個人ページばかり、最新の情報と言えば雑誌だ5。オンラインのコミュティがないのだからオフラインミーティングもない(Niftyなどのパソコン通信ではオフ会もあった、というかオフ会の言葉の発祥がそのへんだと思う)。よく練られたベストプラクティスが無い中、平凡なプログラマが書くコードというのはそれはもう十人十色で、変数名が意味不明に略されていたり、一つのものに対してもいろんな名前がついていたり、読み解くのに一苦労だ。

ここにC++によるオブジェクト指向の波がやって来る。Cでしか書いたことない人達は「コメントに"//"が使えるC言語」6とか、「困ったらextern Cで書けばいいから」とか言っちゃうし、継承をよくわかってないまま使って単に可読性を悪化させただけだったりした。boostはまだ来ない。オペレーターオーバーロードで演算子の持つ意味が特定のクラスでは異なっていたり、ダイアモンド継承による問題もあった。技術者レベルはCの時代に比べミジンコほどしか上がってないのに、言語の機能と複雑度は爆発的に増えた。

省略名で苦労するなら長い名前でいいじゃん

c/c++で蔓延していたユニークな変数名・略名からの揺り戻しとして「変数名を省略するなブーム」が起きる。過去はみんな640x480のCRT(またはそれ以下の解像度のモノクロ液晶)で仕事をしていたので、変数名が長いとソースの右端が途切れて読みにくかったのである。マウスもなかったし。

Javaが出た1995年ころは、Windows3.1->Windows95,MacはSystem7-漢字Talk7.5くらいで、800x600や1024x7686でマルチウィンドウで任意のフォントサイズで表示ができるようになったこともあり、「変数名を省略しないブーム」が訪れた。これは最近の関数型言語の隆盛でまた揺り返しがきている。日本語->英語名での間違いとかもあり、大事なのはプロジェクトで用語辞書を定義して一貫性をもたせることであって名前を省略しなければいいというものでもない。google spreadsheetのようにフリーでwikiよりも気軽に編集できる辞書スプレッドシートが使えるようになった時代背景も大きい。

インターフェース、セッター、ゲッター...「すいませんこれ手で書くの?」

そして、オブジェクトの状態を隠蔽し振る舞いだけ公開しろということでprivateメンバを作成し、publicなgetter/setterを作成するのである。2019年にこれを手打ちしている猿はいないと思うが、Eclipseは随分前から自動生成をサポートし、JavaではLombokによりアノテーションだけで任意のアクセッサをプリコンパイルしてくれ、モダンな言語はproperty機能を備え(Delphiからあったし、なんならC#はDelphiだが)、Scalaではそもそもメンバ変数もpublicにすることが推奨され出した。これは「変数にしとこうと思ったけど、やっぱ関数にする」みたいな時にインターフェースを変えないまま差し込めるからだ。そもそも振る舞いだけ公開するならアクセッサを用意しては駄目で「Tell, Don't Ask」の原則にしたがって相手のオブジェクトに対してやって欲しい命令をだす方が望ましいのである。

XML地獄

さて話をSpringに戻そう。ServiceLocatorでは利用側のプログラムが任意のタイミングで取り出すので、実装を差し替えたいクラスだけ設定しておけばよかった。しかしDIコンテナではDIコンテナがインスタンスを生成し、その生成時にインスタンスは別のDIで差し替え可能なインスタンスを持つため、DIで差し替え可能なメンバーを持つクラスすべてをDIコンテナに登録しなくてはならなかった。そのため登録する項目数はかなり多く、spring起動時に依存関係を解決するため起動は遅かった。でもJ2EEのクソ設計に比べれば随分スマートだったので広く受け入れられた。当時は設定ファイルと言えばxmlで7、Springを始めとしたJava界隈はXML地獄の様相を呈していく。

設定より規約

ここは本題と外れるので余談となるが、このような状況で登場したRuby On Rails(以下RoR)は、設定を多数管理するよりも「規約をつくってそれにのっとっていればうまく動くよ」というスタンスで人口に膾炙し、後にRoRのフォロワーを多数輩出した。XML地獄から脱出する大きなムーブメントとなった功績は大きい。ただ「設定より規約」では規約をすべて頭に入れておかないと「これ、どこにも定義されてないんだけどどうやって動いてるの???」となり保守性が悪くなる。少ない規約で運用できるなら非常に有用だと思う。Spring DIに影響されRoRを後追いした日本ローカルなSeasar2では、どちらも劣化コピーだった上にアレがアレだったんで消えた。

アノテーションの是非

SpringはXMLからの脱却に、アノテーションを選択した。確かに設定ファイルは減った。意味不明な設定ファイルの項目があったとき、XMLなら項目名でgrepをかけてどういう処理をしているのか探し出すことができた。アノテーションの場合IDEからコントロールクリックで飛べるが、そこにはアノテーションとしての定義しかなく、実際はDIコンテナからインスタンスを生成する時に、対象となるクラスについているアノテーションを見て挙動を変えるわけで、ベテランじゃないと処理が負えなくなった。

そしてServiceLocatorの事を思い出してほしい。あれにデメリットがあるとされたのは、Locatorに対する依存があるからだ。設定ファイルからアノテーションのに移したということは、アノテーションをimportしアノテーションに依存する。ServiceLocatorに対する最大の優位点まで捨ててしまった。後にCDIとしてDI系のアノテーションが標準化されるが、springがやり始めた時は完全にspring依存で替えの効かないシステムになったのである。

アノテーションの例としてこれを見てほしい。

きしだのはてなより引用: https://nowokay.hatenablog.com/entry/20131108/1383882109

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;

@Named
@ApplicationScoped
@Path("/calc")
public class CalcService {

    @Inject
    CalcLogic logic;

    @Path("add")
    @GET
    public Result add(
            @QueryParam("left") int p1,
            @QueryParam("right") int p2)
    {
        int ans = logic.add(p1, p2);
        return new Result(p1, p2, ans);
    }
}

(筆者による改行変更あり)

ブログオーナーのきしださんは「便利」とおっしゃられているが、これはJavaにおけるWeb開発、DIの知識があるから「すげー少ない労力でかける」のであって、初見の人からしたら「このアノテーションは一体全部でいくつあって、そのうちどれとどれをマスターしておく必要があるのか、@GETに対しては@POST@PUTがあるだろうというのは想像できるが、じゃあ添付ファイルはどうやって取得するの?@Pathはアプリ起動時に自動的にこのクラスが読み込まれてservlet containerのルーティングに登録されるの?とか不安要素いっぱいすぎる。

ここで同じことをするsparkframework8+ServiceLocatorのサンプルを見てほしい。ServiceLocatorはサンプルとしてimport,他に依存するのはSparkframeworkだけである。

import static spark.Spark.*;
import com.example.ServiceLocator;

public class HelloWorld {
    public static void main(String[] args) {
        get("/hello", (req, res) -> {
            CalcLogic logic = ServiceLocator.get(CalcLogic.class);
            int p1 = intParam("left");
            int p2 = intParam("right");
            int ans = logic.add(p1, p2);
            return new Result(p1, p2, ans);
        }
    }
    private static int intParam(Request req, String key){
      return Integer.valueOf(req.params(key));
    }
}

アノテーションはひとつもなく同じことをしている。static import Spark.*してgetメソッドが使ってしまうあたりは依存関係が明確でないが、IDEでコントロールクリックすれば飛んで処理が追える。膨大なJ2EEやSpringのドキュメントにあらかじめ目を通しておかなくても、実際に書いてあるシンプルなコードから追えるのである。ちなみに上のソースは1行メソッドのためにわざわざintParam()を定義しているので、直に書けば3行短くなる。それより引数でもらってきたp1とp2を返す意味がわからん(だって呼び出し側が渡してきたパラメータなんだから呼び出し側は持ってるでしょ)ので、intParamは残しつつ無駄を省くとこう

import static spark.Spark.*;
import com.example.ServiceLocator;

public class HelloWorld {
    public static void main(String[] args) {
        get("/hello", (req, res) -> {
            CalcLogic logic = ServiceLocator.get(CalcLogic.class);
            return new Result(logic.add(intParam("left"), intParam("right")));
        }
    }
    private static int intParam(Request req, String key){
      return Integer.valueOf(req.params(key));
    }
}

おそらくSparkFrameworkの代わりにNinjaFrameworkを使っても似たようなものだろう。学習コストが違いすぎる。Springは当時眩しかったが、今では腐臭を放ち始めている。

Springの終焉

いつ終わっていたのか?Springの作者はRederick "Rod" Johnsonであるが、彼がScala言語を開発しているtypesafeにジョインした2012年、既に作者から見切りをつけられていたのではないだろうか。おそらく創始者としての責任感は感じていただろうから、実際にジョインした2012よりも前の段階から、すでにSpringは最先端の技術ではなく、より良い方法を追求しようとしていたのだと想像する。まあここはあくまで個人の想像だし、本人はまかりまちがっても「いやあ実はSpringなんてとっくに見切っててさあ」なんて思ってても公言できる立場ではないので、確認する術はない。

Micronauts

お、マイクロサービス特化のあたらしいフレームワーク?いい線いくのかと思ったけど思想が完全に「Springつらいから軽量Springを作る」になっていてアノテーション使いまくりでやばい。過去にLinusは「Subversionプロジェクトは無意味、CVSからほとんど進化してないのに多大なリソースをつぎ込んでいる」と批判してgitのベースを作ったが、ちょうどそんな漢字だと思う。MicronautsはSpringのつらさをちょっと軽減してくれるだけで、根本的な問題がなにも解決していない

今後

当然「じゃあ何を使うのか」という話になるわけだが、筆者は今の所Kotlin+SparkFramework+Expose(ORM)を使っているものの、ここがゴールだという気は全然しない。まずKotlinってJavaの知識が必要だし、gradleってmavenの知識が必要だし、すごく過渡期の中途半端なプロダクトという感じ。ExposeはマクロのないKotlinではボイラープレートが多すぎてまだまだつらい。kaptでどうにかなるのか?AndroidのおかげでKotlinは今後もシェアを伸ばすだろうけど、じゃあ全部Kotlinでいいかって言われるとうーん、となる。パターンマッチがないのも辛い、kotlin2.0で入って欲しい。あとJVMがでかくて、せっかくalpineでdocker imageつくってもJVMいれた時点でお腹痛くなってくる。busyboxの意味とは。

今はGoを試そうとしている。今度ジェネリクスも入るらしいからそれを待ちたい。まともなMaybe/Eitherが使えるようになったら良いかもしれないけど、Goの型システムでいけるのだろうか?無理そうな気がする。

言語機能的にはRustくらい欲しいが、Rustが10年後どれくらい使われているかを考えると、業務で使うのは怖い気がする(業種によるけど)。


  1. jsonは2001年頃Douglas Crockford氏によって"発見"された。 https://www.publickey1.jp/blog/17/jsonrfc_8259ecma-404_2nd_editonutf-8.html 

  2. RMIは当初CORBAに対応しておらずJava同士専用だった。 

  3. みんなが大嫌いなxml地獄の事。 

  4. 本当はServiceLocatorの方が良いと思ってるんだけど言葉を選んでDIの方が良いケースもあるよね、と一応言っておくか的な心情がアリアリとみえる。 

  5. イベントやリリースの情報ですら1-2ヶ月遅れで、まとまった有用な情報が本になるのは半年一年遅れ、ネットがないので口コミの伝播速度も遅く、通ってる本屋で偶然出会わなければその情報にアクセスすることもない。 

  6. 当時では広大なスペースだった、20"CRTで重さが30kgあり、「一人用の冷蔵庫より重てえええ」ってなった 

  7. 今でこそレガシー代表の冗長かつ無駄の多いフォーマットという扱いだが、前述のASN.1などに比べれば簡単だし、DTDを書くことでタグとして存在して良い名前は形式を定義しておくことができ、XSLTなどと合わせて「XMLを中心にした技術の波が来るぞ」という時代があった。jsonみたいに動的に任意で書けるものとは基本的なスタンスが違うのである。え?じゃあ今も使いたいかって?それとこれとは話が別。 

  8. ポストHadoopとして有名なApache Sparkではなくて軽量web frameworkの方。名前はこっちの方が先に使ってたのだが知名度では月とスッポンになってしまった。もちろんこっちがスッポン。Apacheも名前決める時にJVM界隈で被らないように気を使ってくれたっていいのに 

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away