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

最終更新日

Schoo Java入門 中級 第5回 サーバーサイドアプリケーションの実装基礎

「送料無料の条件を変えたいだけなのに、画面のHTMLまで読まないと直す場所が分からない」。入力の受け取り、料金計算、画面表示を一つのファイルに詰め込むと、こんな困りごとが起きます。

MVCは、入力を受け付ける処理・業務のルール・画面の表示を分け、変更したい仕事を追いやすくする考え方です。この記事では、送料の確認画面を実際に作りながら、Servlet・Javaクラス・JSPがどのように協力するかを見ていきます。

この記事はSchoo「Java入門 中級」第5回「サーバーサイドアプリケーションの実装基礎」に対応しています。未受講の方も、フォームの値の取得とJavaのメソッドが分かれば読み進められます。環境構築の手順は扱いません。

MVCに分けると、何がうれしい?

今回の仕様は「商品合計金額が5000円以上なら送料0円、それ未満なら500円」です。金額は0以上の整数とします。後から次のような依頼が来たと考えてください。

  • 送料無料の条件を、5000円以上から8000円以上へ変えたい。
  • 結果画面の「送料: 0円」を「送料無料」という表示にしたい。
  • 同じ送料計算を、別の画面や単体テストからも使いたい。

全部をJSPに書いていると、計算を調べるときもHTMLとリクエスト処理が一緒に目に入ります。計算だけを実行したい場合も、画面やリクエストに依存したコードを切り離す必要が出てきます。

図解:何を変えたいかで、見るファイルが分かる
Controller:入力を受け付けるShippingServlet.java getParameter("subtotal") Integer.parseInt(text)
フォームの値を受け取り、計算を依頼し、結果画面へつなぐ。
Model:送料を決めるShippingFeeCalculator.java if (subtotal >= 5000) { return 0; }
送料無料の条件を変えるなら、この計算処理を確認する。
View:結果を表示するshipping-result.jsp <p>送料: <%= shippingFee %>円 </p>
文字・配置・見た目を変えるなら、こちらを確認する。

クラス名やファイル名ではなく、担当する仕事によってModel・View・Controllerを区別します。

分けておくと、送料の計算はJavaクラスのメソッドとして直接呼べます。画面の配置を変えても、その計算式を書き直す必要はありません。ファイル数を減らすためではなく、変更する理由が違う処理を分けるためにMVCを使います。

Model・View・Controllerは、3種類の仕事

役割今回の担当すること/しないこと
ModelShippingFeeCalculator送料を決める。HTTPの受信やHTMLの組み立てはしない。
Viewshipping-result.jsp受け取った結果を表示する。送料無料の基準は決めない。
ControllerShippingServlet入力を受け取り、Modelを呼び、表示先へ渡す。送料の計算式は持たない。

Modelは「データベース」や「値を入れるクラス」だけを意味しません。今回のような業務上の判断や計算(ビジネスロジック)もModel側の仕事です。アプリが大きくなると、データを持つクラス、業務を処理するService、DBにアクセスするDAOなど、複数のクラスで担うこともあります。

クラス名にModelを付ける決まりはありません。また、MVCだから必ず3ファイルという意味でもありません。この記事では、入力用HTMLも含めて4ファイルを使います。

入力から画面表示までの流れ

送料確認画面のMVC。1、ブラウザから金額4500円をServletへ送信。2、ServletがModelのcalculate(4500)を呼ぶ。3、Modelが送料500円をServletへ返す。4、Servletが結果をrequestに入れてJSPへ引き継ぐ。5、JSPが作ったHTMLをブラウザへ返し、送料500円を表示する。
MVCの役割と値の流れ。Modelから返った計算結果を、ServletがJSPへ渡します。

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は送料を自分で決めず、計算結果を受け取ります。入力が不足していたり、整数に変換できなかったりした場合は、結果の代わりに固定のエラーメッセージを渡します。

チェックこの例の担当理由
値が届いたか、整数にできるかServletHTTPから受け取った文字列をJavaの値にするため。
金額が0以上かModel送料計算が受け付ける金額の条件だから。
5000円以上なら無料かModel送料を決める業務ルールだから。

NumberFormatExceptionIllegalArgumentException の子クラスなので、catchはこの順番にします。空欄・整数以外・intの範囲外・負の金額を分けて扱っています。HTMLの requiredmin は入力を助けますが、サーバー側のチェックの代わりにはなりません。

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ではなく、入力フォームから確認してください。

変更するとき、どこを直す?

図解:計算の変更と、表示の変更を混ぜない
変更A:8000円以上を送料無料に
送料を決める条件を変更
Modelの変更if (subtotal >= 8000) { return 0; }
Servletへの戻り値は今までと同じint型の送料。
変更B:0円を「送料無料」と表示
結果の見せ方を変更
JSPの変更送料が0円なら 「送料無料」と表示 それ以外なら金額を表示
送料無料の判定条件5000円を、JSPにもう一度書かない。

変更AではModelの比較条件を変えます。引数と戻り値の形が同じなら、Servletの呼び出し方とJSPの金額表示はそのまま使えます。ただし、テストの期待値や、別の場所に書いた料金案内も新しい条件に合わせて確認します。「変更が必ず1ファイルで済む」という保証ではありません。

変更BではJSPが送料0円という結果を見て表示を切り替えます。これは表示のための分岐です。JSPにif文を書いてはいけないのではなく、送料の基準をJSPでもう一度計算しないことが大切です。

分けたつもりでも、役割が混ざる書き方

  • Servletに subtotal >= 5000 を書く:画面が増えると、各Servletに送料の条件が散らばりやすくなる。
  • JSPから計算クラスを呼んで送料を決める:表示と業務処理が結び付き、画面を読まないと処理の流れが追いにくくなる。
  • Modelで request.getParameter() を呼ぶ:通常のJavaコードや単体テストから計算だけを呼びにくくなる。

今回はMVCの分担を見るための送料確認サンプルです。実際の購入処理では、ブラウザから届いた金額をそのまま請求の根拠にせず、商品情報や数量を使ってサーバー側で金額を確定します。

確認問題

  1. 商品合計金額6000をフォームから送信しました。Servletが最初に取得する型、Modelに渡す値、Modelの戻り値、JSPに表示される送料を答えてください。
  2. 送料無料の条件を「8000円以上」に変更してください。どのファイルのどの条件を変えますか。7999円と8000円の送料も答えてください。
  3. 送料0円のときは「送料無料」、それ以外は今までどおり「送料: 金額円」と表示するJSPのコードを書いてください。本文の送料のp要素だけを置き換えるものとします。
  4. 「送料を決める処理」をServletやJSPではなくShippingFeeCalculatorに置くと、画面を使わないテストで何ができるようになりますか。4500円の例で説明してください。

演習後の確認:こちらのページを使って、自分で考えた内容を見直せます。

まとめ

  • Controllerは入力を受け取り、Modelの処理と表示先をつなぐ。
  • Modelは業務ルールを扱う。今回なら送料を決めるJavaクラス。
  • Viewは結果の見せ方を扱う。JSPがサーバーでHTMLを作る。
  • 分ける目的はファイルを増やすことではなく、ルール変更・表示変更・テストで扱う範囲を明確にすること。

関連する記事

シェアする