JavaのMVCモデルとは?Servlet・Model・JSPに処理を分ける理由

「送料無料の条件を変えたいだけなのに、画面のHTMLまで読まないと直す場所が分からない」。入力の受け取り、料金計算、画面表示を一つのファイルに詰め込むと、こんな困りごとが起きます。
MVCは、入力を受け付ける処理・業務のルール・画面の表示を分け、変更したい仕事を追いやすくする考え方です。この記事では、送料の確認画面を実際に作りながら、Servlet・Javaクラス・JSPがどのように協力するかを見ていきます。
この記事はSchoo「Java入門 中級」第5回「サーバーサイドアプリケーションの実装基礎」に対応しています。未受講の方も、フォームの値の取得とJavaのメソッドが分かれば読み進められます。環境構築の手順は扱いません。
MVCに分けると、何がうれしい?
今回の仕様は「商品合計金額が5000円以上なら送料0円、それ未満なら500円」です。金額は0以上の整数とします。後から次のような依頼が来たと考えてください。
- 送料無料の条件を、5000円以上から8000円以上へ変えたい。
- 結果画面の「送料: 0円」を「送料無料」という表示にしたい。
- 同じ送料計算を、別の画面や単体テストからも使いたい。
全部をJSPに書いていると、計算を調べるときもHTMLとリクエスト処理が一緒に目に入ります。計算だけを実行したい場合も、画面やリクエストに依存したコードを切り離す必要が出てきます。
ShippingServlet.java
getParameter("subtotal")
Integer.parseInt(text)ShippingFeeCalculator.java
if (subtotal >= 5000) {
return 0;
}shipping-result.jsp
<p>送料:
<%= shippingFee %>円
</p>クラス名やファイル名ではなく、担当する仕事によってModel・View・Controllerを区別します。
分けておくと、送料の計算はJavaクラスのメソッドとして直接呼べます。画面の配置を変えても、その計算式を書き直す必要はありません。ファイル数を減らすためではなく、変更する理由が違う処理を分けるためにMVCを使います。
Model・View・Controllerは、3種類の仕事
| 役割 | 今回の担当 | すること/しないこと |
|---|---|---|
| Model | ShippingFeeCalculator | 送料を決める。HTTPの受信やHTMLの組み立てはしない。 |
| View | shipping-result.jsp | 受け取った結果を表示する。送料無料の基準は決めない。 |
| Controller | ShippingServlet | 入力を受け取り、Modelを呼び、表示先へ渡す。送料の計算式は持たない。 |
Modelは「データベース」や「値を入れるクラス」だけを意味しません。今回のような業務上の判断や計算(ビジネスロジック)もModel側の仕事です。アプリが大きくなると、データを持つクラス、業務を処理するService、DBにアクセスするDAOなど、複数のクラスで担うこともあります。
クラス名にModelを付ける決まりはありません。また、MVCだから必ず3ファイルという意味でもありません。この記事では、入力用HTMLも含めて4ファイルを使います。
入力から画面表示までの流れ

ModelからJSPへ直接結果を送っているわけではありません。calculate() の戻り値をServletが受け取り、setAttribute() でrequestに入れてから、forward() でJSPへ処理を引き継ぎます。この間にブラウザへ別のリクエストを出させる必要はありません。
1.入力画面:金額を送る
src/main/webapp/shipping-form.html に置きます。以下の例はJava 17とTomcat 10.1の jakarta.servlet を使う構成です。すべてのファイルをUTF-8で保存します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>送料の確認</title>
</head>
<body>
<h1>送料の確認</h1>
<form action="shipping" method="post"
accept-charset="UTF-8">
<label for="subtotal">商品合計金額(円)</label>
<input id="subtotal" name="subtotal"
type="number" min="0" step="1" required>
<button type="submit">送料を確認する</button>
</form>
</body>
</html>
アプリのコンテキストパスが /lesson05 なら、/lesson05/shipping-form.html から開きます。送信先は /lesson05/shipping です。金額はリクエストパラメータなので、サーバーではまずStringとして受け取ります。
2.Model:送料のルールをJavaクラスに書く
src/main/java/example/ShippingFeeCalculator.java を作ります。
package example;
public class ShippingFeeCalculator {
public int calculate(int subtotal) {
if (subtotal < 0) {
throw new IllegalArgumentException(
"商品合計金額は0円以上にしてください。");
}
if (subtotal >= 5000) {
return 0;
}
return 500;
}
}
引数は商品合計金額、戻り値は送料です。フォームもJSPも知らないため、次のように普通のJavaコードから呼べます。
ShippingFeeCalculator calculator = new ShippingFeeCalculator();
int shippingFee = calculator.calculate(4500);
System.out.println(shippingFee); // 500
負の金額を拒否するのもこのクラスのルールです。別の画面から呼んだ場合でも、同じチェックが働きます。「ブラウザで入力できないから大丈夫」ではなく、処理する側でも不正な値を受け付けないようにします。
3.Controller:受け取り・呼び出し・引き継ぎを書く
src/main/java/example/ShippingServlet.java を作ります。
package example;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@WebServlet("/shipping")
public class ShippingServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
String text = request.getParameter("subtotal");
if (text == null || text.isBlank()) {
request.setAttribute("error", "商品合計金額を入力してください。");
} else {
try {
int subtotal = Integer.parseInt(text.trim());
ShippingFeeCalculator calculator =
new ShippingFeeCalculator();
int shippingFee = calculator.calculate(subtotal);
request.setAttribute("subtotal", subtotal);
request.setAttribute("shippingFee", shippingFee);
} catch (NumberFormatException e) {
request.setAttribute("error",
"商品合計金額は0〜2147483647の整数で入力してください。");
} catch (IllegalArgumentException e) {
request.setAttribute("error",
"商品合計金額は0円以上にしてください。");
}
}
request.getRequestDispatcher("/shipping-result.jsp")
.forward(request, response);
}
}
中心になるのは calculator.calculate(subtotal) の1行です。Servletは送料を自分で決めず、計算結果を受け取ります。入力が不足していたり、整数に変換できなかったりした場合は、結果の代わりに固定のエラーメッセージを渡します。
| チェック | この例の担当 | 理由 |
|---|---|---|
| 値が届いたか、整数にできるか | Servlet | HTTPから受け取った文字列をJavaの値にするため。 |
| 金額が0以上か | Model | 送料計算が受け付ける金額の条件だから。 |
| 5000円以上なら無料か | Model | 送料を決める業務ルールだから。 |
NumberFormatException は IllegalArgumentException の子クラスなので、catchはこの順番にします。空欄・整数以外・intの範囲外・負の金額を分けて扱っています。HTMLの required や min は入力を助けますが、サーバー側のチェックの代わりにはなりません。
getParameter() はブラウザが送った文字列を読むもの、setAttribute() と getAttribute() はサーバー内で処理結果を渡すものです。名前が同じsubtotalでも、受信したStringと計算に使う数値を区別します。
4.View:渡された値をJSPに表示する
src/main/webapp/shipping-result.jsp を作ります。ここではJSTLやELを使わず、<% ... %> と <%= ... %> で読み取る例にしています。
<%@ page pageEncoding="UTF-8"
contentType="text/html; charset=UTF-8" %>
<%
String error = (String) request.getAttribute("error");
Integer subtotal = (Integer) request.getAttribute("subtotal");
Integer shippingFee = (Integer) request.getAttribute("shippingFee");
%>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>送料の確認結果</title>
</head>
<body>
<h1>送料の確認結果</h1>
<% if (error != null) { %>
<p><%= error %></p>
<% } else if (subtotal != null && shippingFee != null) { %>
<p>商品合計金額: <%= subtotal %>円</p>
<p>送料: <%= shippingFee %>円</p>
<% } else { %>
<p>入力画面から送料を確認してください。</p>
<% } %>
<a href="shipping-form.html">入力画面に戻る</a>
</body>
</html>
getAttribute() の戻り値はObjectなので、StringやIntegerとして取り出しています。setAttribute() に渡したintはIntegerとして保持されます。結果画面を直接開いたときは属性がないため、入力画面を案内します。
この例でHTMLに埋め込むのは数値と、Servletで決めた固定メッセージだけです。入力文字列をそのまま表示していません。商品名など外部から届いた文字列を表示する場合は、HTML用のエスケープが別途必要です。
フォームから4500を送ると「送料: 500円」、5000を送ると「送料: 0円」になります。ServletはPOSTを処理するため、アドレスバーへ /shipping と直接入力するGETではなく、入力フォームから確認してください。
変更するとき、どこを直す?
if (subtotal >= 8000) {
return 0;
}送料が0円なら
「送料無料」と表示
それ以外なら金額を表示変更AではModelの比較条件を変えます。引数と戻り値の形が同じなら、Servletの呼び出し方とJSPの金額表示はそのまま使えます。ただし、テストの期待値や、別の場所に書いた料金案内も新しい条件に合わせて確認します。「変更が必ず1ファイルで済む」という保証ではありません。
変更BではJSPが送料0円という結果を見て表示を切り替えます。これは表示のための分岐です。JSPにif文を書いてはいけないのではなく、送料の基準をJSPでもう一度計算しないことが大切です。
分けたつもりでも、役割が混ざる書き方
- Servletに
subtotal >= 5000を書く:画面が増えると、各Servletに送料の条件が散らばりやすくなる。 - JSPから計算クラスを呼んで送料を決める:表示と業務処理が結び付き、画面を読まないと処理の流れが追いにくくなる。
- Modelで
request.getParameter()を呼ぶ:通常のJavaコードや単体テストから計算だけを呼びにくくなる。
今回はMVCの分担を見るための送料確認サンプルです。実際の購入処理では、ブラウザから届いた金額をそのまま請求の根拠にせず、商品情報や数量を使ってサーバー側で金額を確定します。
確認問題
- 商品合計金額6000をフォームから送信しました。Servletが最初に取得する型、Modelに渡す値、Modelの戻り値、JSPに表示される送料を答えてください。
- 送料無料の条件を「8000円以上」に変更してください。どのファイルのどの条件を変えますか。7999円と8000円の送料も答えてください。
- 送料0円のときは「送料無料」、それ以外は今までどおり「送料: 金額円」と表示するJSPのコードを書いてください。本文の送料のp要素だけを置き換えるものとします。
- 「送料を決める処理」をServletやJSPではなくShippingFeeCalculatorに置くと、画面を使わないテストで何ができるようになりますか。4500円の例で説明してください。
演習後の確認:こちらのページを使って、自分で考えた内容を見直せます。
まとめ
- Controllerは入力を受け取り、Modelの処理と表示先をつなぐ。
- Modelは業務ルールを扱う。今回なら送料を決めるJavaクラス。
- Viewは結果の見せ方を扱う。JSPがサーバーでHTMLを作る。
- 分ける目的はファイルを増やすことではなく、ルール変更・表示変更・テストで扱う範囲を明確にすること。