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でコンポーネントの状態を可視化できる。どのコンポーネントがどの状態を持っているか一目でわかる。
状態の変更が明示的(useStateやsetState)なので、「なぜこの値になったか」を追いやすい。
テストの書きやすさ
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