🐨

Spring MVC + jQuery でWeb三層アーキテクチャを理解する

に公開

はじめに

ReactやNext.jsを触っていると、「サーバーがHTMLを返して、jQueryで動きをつける」という従来型の構成がどういう設計思想なのか、いまいちピンとこなかったので、この記事では、Spring MVC + jQuery を使ったWeb三層アーキテクチャを、ユーザー登録フォームを題材に整理し、モダン構成との違いを比較しながら、それぞれの設計思想を理解することが目的です。

作るもの

シンプルなユーザー登録フォーム。

[入力画面] → [確認画面] → [完了画面]
 Scr001      Scr002     Scr003
画面 機能
入力画面(Scr001) ユーザー名・メールアドレスを入力
確認画面(Scr002) 入力内容を確認、修正 or 登録実行
完了画面(Scr003) 登録完了メッセージを表示

プロジェクト構成

src/
  ├─ common/
  │    ├─ CommonValidate.java
  │    └─ Constants.java
  │
  ├─ dao/
  │    ├─ UserDAO.java
  │    ├─ UserDAOImpl.java
  │    └─ UserEntity.java
  │
  ├─ service/
  │    ├─ UserRegistService.java
  │    ├─ UserRegistServiceImpl.java
  │    └─ UserRegistDTO.java
  │
  └─ web/
       ├─ Scr001InitController.java
       ├─ Scr001ExecuteController.java
       ├─ Scr001Form.java
       ├─ Scr001Validate.java
       ├─ Scr002InitController.java
       ├─ Scr002ExecuteController.java
       ├─ Scr002Form.java
       └─ Scr003InitController.java

webapp/
  ├─ js/
  │    ├─ common.js
  │    ├─ Scr001.js
  │    └─ Scr002.js
  │
  └─ WEB-INF/jsp/
       ├─ Scr001.jsp
       ├─ Scr002.jsp
       └─ Scr003.jsp

画面ごとにController、Form、JSP、jsがセットで存在する。この「画面単位」という考え方がこの構成の基本です。

サーバー側の実装

InitController と ExecuteController の分離

1つの画面に対してInitControllerとExecuteControllerが分かれている。最初は冗長に感じたが、「表示」と「処理」を分けることで責務が明確になる設計だった。

// 画面の初期表示を担当
@Controller
public class Scr001InitController {

    @RequestMapping("/scr001/init")
    public String init(Model model) {
        Scr001Form form = new Scr001Form();
        model.addAttribute("form", form);
        return "Scr001";
    }
}
// 「確認へ進む」ボタン押下時の処理を担当
@Controller
public class Scr001ExecuteController {

    @Autowired
    private Scr001Validate validate;

    @RequestMapping("/scr001/execute")
    public String execute(
            @ModelAttribute Scr001Form form,
            BindingResult result,
            HttpSession session) {
        
        validate.validate(form, result);
        
        if (result.hasErrors()) {
            return "Scr001";
        }
        
        session.setAttribute("userRegistForm", form);
        return "redirect:/scr002/init";
    }
}

InitControllerは「画面を出す」だけ。ExecuteControllerは「ボタンが押されたら動く」だけ。この単純さが良い。

Formオブジェクト

public class Scr001Form {
    private String userName;
    private String email;
    // getter / setter
}

画面の入力項目と1対1で対応。HTTPリクエストのパラメータが自動でバインドされる。

サーバー側バリデーション

@Component
public class Scr001Validate {

    public void validate(Scr001Form form, BindingResult result) {
        // トークンチェック(CSRF対策)
        if (!isValidToken(form.getToken())) {
            result.reject("error.invalid.token");
        }
        
        // DBを参照するチェック
        if (isEmailDuplicated(form.getEmail())) {
            result.rejectValue("email", "error.email.duplicated");
        }
    }
}

サーバー側バリデーションはクライアントでは検証できないものを担当する。トークンチェックやDB整合性チェックなど。

JSP

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
    <title>ユーザー登録</title>
    <script src="/js/common.js"></script>
    <script src="/js/Scr001.js"></script>
</head>
<body>
    <h1>ユーザー登録</h1>
    
    <form:form modelAttribute="form" action="/scr001/execute" method="post">
        <div>
            <label>ユーザー名</label>
            <form:input path="userName" id="userName" />
            <form:errors path="userName" cssClass="error" />
        </div>
        
        <div>
            <label>メールアドレス</label>
            <form:input path="email" id="email" />
            <form:errors path="email" cssClass="error" />
        </div>
        
        <button type="submit" id="btnConfirm">確認へ進む</button>
    </form:form>
</body>
</html>

JSPはサーバー側で処理されて、完成したHTMLがブラウザに届く。この時点でデータは埋め込み済み。

フロント側の実装

jQueryの役割

jQueryがやることは限定的。

  • 送信前の入力チェック(必須、形式)
  • ボタン押下時のイベント制御
  • Enterキー制御

逆に言うと、画面の初期描画やデータ取得はjQueryの仕事ではない。サーバーがやる。

// Scr001.js
$(function() {
    $('#btnConfirm').on('click', function(e) {
        $('.client-error').remove();
        var hasError = false;
        
        if ($('#userName').val().trim() === '') {
            showError('#userName', 'ユーザー名を入力してください');
            hasError = true;
        }
        
        if ($('#email').val().trim() === '') {
            showError('#email', 'メールアドレスを入力してください');
            hasError = true;
        }
        
        var emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if ($('#email').val() && !emailPattern.test($('#email').val())) {
            showError('#email', 'メールアドレスの形式が正しくありません');
            hasError = true;
        }
        
        if (hasError) {
            e.preventDefault();
        }
    });
    
    function showError(selector, message) {
        $(selector).after('<span class="client-error">' + message + '</span>');
    }
});

クライアント側バリデーションはUXのため。すぐにフィードバックを返せる。ただし開発者ツールで回避できるので、サーバー側でも必ずチェックする。

データの流れ

入力画面の初期表示

[Browser]
    │ GET /scr001/init
    ▼
[Scr001InitController]
    │ Formを生成、Modelに詰める
    ▼
[Scr001.jsp]
    │ HTMLを生成
    ▼
[Browser]
    │ HTML受信、jQuery読み込み
    ▼
[jQuery] イベントリスナー設定完了、待機

確認画面への遷移

[Browser]
    │ ユーザーが入力して「確認へ進む」クリック
    ▼
[jQuery]
    │ 入力チェック(エラーなら送信を止める)
    │ POST /scr001/execute
    ▼
[Scr001ExecuteController]
    │ サーバー側バリデーション
    │ セッションに入力値を保持
    │ redirect:/scr002/init
    ▼
[Scr002InitController]
    │ セッションから入力値を取得
    ▼
[Scr002.jsp]
    │ HTMLを生成
    ▼
[Browser]

ポイントは、画面遷移のたびにサーバーを経由すること。クライアント側で完結しない。

モダン構成との比較

Spring MVC + jQuery と React/Next.js で何が違うのかを整理する。

コンポーネント再利用

Spring MVC + jQuery の場合
再利用の単位は「ファイル」。共通のヘッダーやフッターは別のJSPとしてincludeする。

<%@ include file="/WEB-INF/jsp/common/header.jsp" %>

<div class="content">
    <!-- 画面固有の内容 -->
</div>

<%@ include file="/WEB-INF/jsp/common/footer.jsp" %>

jQueryも同様で、共通処理はcommon.jsに切り出す。

// common.js
function showError(selector, message) {
    $(selector).after('<span class="client-error">' + message + '</span>');
}

function clearErrors() {
    $('.client-error').remove();
}

ただし、「入力フォーム付きのカード」のような、HTMLとJSとCSSがセットになったUIパーツを再利用するのは難しい。HTMLはJSP、JSは別ファイル、CSSも別ファイルに散らばっているので、1つのUIパーツとしてまとめられない。

React の場合
再利用の単位は「コンポーネント」。HTML(JSX)、JS、CSSが1つのファイルにまとまる。

// InputCard.jsx
function InputCard({ label, value, onChange, error }) {
    return (
        <div className="input-card">
            <label>{label}</label>
            <input value={value} onChange={onChange} />
            {error && <span className="error">{error}</span>}
        </div>
    );
}

これを別の画面で使いたければ、importするだけ。

import InputCard from './InputCard';

function UserRegistForm() {
    return (
        <div>
            <InputCard label="ユーザー名" value={name} onChange={...} error={...} />
            <InputCard label="メールアドレス" value={email} onChange={...} error={...} />
        </div>
    );
}

UIのパーツ化と再利用は、Reactの方が圧倒的にやりやすい。Spring MVC + jQuery で同じことをやろうとすると、JSPのinclude、JSの関数、CSSのクラスを別々に管理することになり、どれがどれとセットなのかわかりにくくなる。

ルーティング

Spring MVC + jQuery の場合
ルーティングはサーバーが握っている。

GET  /scr001/init    → Scr001InitController
POST /scr001/execute → Scr001ExecuteController
GET  /scr002/init    → Scr002InitController

画面遷移は必ずサーバーを経由する。「確認へ進む」を押すと、POSTリクエストがサーバーに飛び、サーバーがリダイレクト指示を返し、ブラウザが次の画面をGETする。

[入力画面] --POST--> [サーバー] --redirect--> [確認画面をGET]

毎回フルページリロードが発生する。

Next.js の場合
クライアントサイドルーティングが基本。

import { useRouter } from 'next/router';

function InputPage() {
    const router = useRouter();
    
    const handleSubmit = () => {
        // バリデーション
        // ...
        
        // 画面遷移(サーバーを経由しない)
        router.push('/confirm');
    };
}

router.pushを呼ぶと、ブラウザのURLは変わるが、ページ全体のリロードは発生しない。必要なコンポーネントだけが差し替わる。

APIを叩く必要があるときだけサーバーと通信する。

const handleRegister = async () => {
    const res = await fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify({ name, email })
    });
    
    if (res.ok) {
        router.push('/complete');
    }
};

Spring MVC + jQuery は「画面遷移 = サーバーへのリクエスト」だが、Next.js は「画面遷移とAPIリクエストが分離」している。この違いが大きい。

開発体験の比較

デバッグのしやすさ

Spring MVC + jQuery
サーバー側の処理はログを出せば追える。Controllerでリクエストを受けて、Serviceを呼んで、DAOでDBアクセスして...という流れがシンプル。
問題はjQuery側。イベントの連鎖が複雑になると、どのタイミングでどの関数が呼ばれているかわかりにくい。console.logを大量に仕込むことになる。
また、サーバーとクライアントをまたぐ問題(「サーバーからは正しいデータが返っているのにjQueryで表示されない」など)は、両方を行き来しながらデバッグする必要があって面倒。

React / Next.js
React DevToolsでコンポーネントの状態を可視化できる。どのコンポーネントがどの状態を持っているか一目でわかる。
状態の変更が明示的(useStatesetState)なので、「なぜこの値になったか」を追いやすい。

テストの書きやすさ

Spring MVC + jQuery
サーバー側(Controller、Service、DAO)は単体テストが書ける。JUnitでしっかりテストできる。
問題はjQuery。DOM操作のテストは書きにくい。jsdomなどを使えば書けなくはないが、セットアップが面倒で、結局手動テストに頼りがち。

React / Next.js
コンポーネント単位でテストが書ける。Testing LibraryやJestを使えば、「このボタンを押したらこうなる」というテストを書きやすい。

test('必須エラーが表示される', () => {
    render(<InputCard label="名前" value="" error="必須です" />);
    expect(screen.getByText('必須です')).toBeInTheDocument();
});

UIのパーツがコンポーネントとして分離されているので、テストの境界が明確。

変更時の影響範囲

Spring MVC + jQuery
画面単位で分かれているので、ある画面の修正が別の画面に影響することは少ない。Scr001を変えてもScr002には影響しない。
ただし、common.jsを変更すると全画面に影響する可能性がある。そして、どの画面がcommon.jsのどの関数を使っているかは、grepしないとわからない。
また、同じようなUIを複数画面で実装している場合、1箇所直したら全部直す必要がある。コピペが多いとしんどい。

React / Next.js
共通コンポーネントを直せば、それを使っている全画面に反映される。これはメリットでもあり、デメリットでもある。
変更の影響範囲は TypeScript の型チェックや IDE の参照検索で確認しやすい。

チーム開発での分担

Spring MVC + jQuery
「画面単位」で分担しやすい。「Scr001はAさん、Scr002はBさん」という分け方ができるが、共通部分(common.js、共通JSP)の修正は衝突しやすい。

React / Next.js
「コンポーネント単位」で分担できる。「InputCardはAさん、FormLayoutはBさん」という分け方。

実行時の比較

初期表示速度

Spring MVC + jQuery
サーバーでHTMLを生成して返すので、ブラウザはHTMLを受け取ったらすぐ表示できる。First FCP(Contentful Paint)は速い傾向。
ただし、毎回サーバーでHTMLを生成する処理が走る。

React(CSR)
最初にJSバンドルを読み込んで、クライアント側でHTMLを生成する。JSの読み込みと実行が終わるまで画面が表示されない。FCPは遅くなりがち。

Next.js(SSR/SSG)
サーバーでHTMLを生成して返すので、Spring MVCと同様にFCPは速い。加えて、その後のインタラクションはReactが担当するので、両方のいいとこ取りができる。

画面遷移のUX

Spring MVC + jQuery
画面遷移のたびにフルページリロード。白い画面が一瞬見えることがある。
ユーザーにとっては「ページが切り替わった」という感覚がはっきりある。良くも悪くも伝統的なWebの体験。

React / Next.js
SPAなので画面遷移がスムーズ。必要な部分だけ更新される。
アプリケーションのような体験が作れる。ただし、「今どのページにいるか」がURLだけではわかりにくい。

サーバー負荷

Spring MVC + jQuery
画面遷移のたびにサーバーがHTMLを生成する。ユーザーが増えるとサーバーの負荷が高くなりやすい。
セッションを使う場合、セッション管理のオーバーヘッドもある。

React(CSR)+ API
サーバーはAPIリクエストに対してJSONを返すだけ。HTMLの生成はクライアントが担当。サーバーの負荷は比較的軽い。
ただし、APIのエンドポイント設計をちゃんとやらないと、N+1的なリクエストが発生してかえって重くなることも。

まとめ

以上、Spring MVC + jQuery の構成を整理して、モダン構成と比較してみました。

Spring MVC + jQuery の特徴

  • サーバーが主導権を握る設計
  • 画面単位でController / Form / JSP / js がセットになる。
  • jQueryは「味付け」担当で、責務が限定的
  • 画面遷移 = サーバーへのリクエスト

モダン構成(React / Next.js)との違い

観点 Spring MVC + jQuery React / Next.js
再利用単位 ファイル(JSP, js, css別々) コンポーネント(まとまっている)
ルーティング サーバー主導 クライアント主導
デバッグ サーバーはログ、jsは追いにくい DevToolsで状態可視化
テスト サーバー側は書ける、jsは難しい コンポーネント単位で書ける
影響範囲 画面単位で閉じている コンポーネントの依存関係による
チーム分担 画面単位 コンポーネント単位
初期表示 速い(HTMLが来る) CSRは遅い、SSRなら速い
画面遷移UX フルリロード スムーズ
サーバー負荷 高め(HTML生成) 低め(JSON返すだけ)

どちらが良い・悪いではなく、チームのスキルセットやシステムの要件によって選ぶものでSpring MVC + jQuery は安定しているし、サーバーサイド中心のチームでは今でも有効な選択肢だと感じた。
ただ、UIの複雑さが増してくると、jQueryでの状態管理やコンポーネント再利用の難しさがボトルネックになってくる。そのあたりの限界を理解した上で取捨選択するのが大事だと思う。

Discussion