JavaのDTOとは?複数のデータをまとめて受け渡すクラスの使い方

計算した「小計・送料・合計」を、まとめて画面へ渡したい。このように、ひとまとまりのデータを別の処理へ受け渡すときに使うクラスが、DTOです。DTOを使えば、受け取った側で「何番目の値だろう」と考えずに、getSubtotal() や getTotal() のような名前で必要な値を取り出せます。
DTOは、特別な命令ではありません。これまで学んだフィールド・コンストラクタ・getterを使ったJavaのクラスです。この記事では、見積もりを計算するModelから、Servletを経由してJSPへ、一つの計算結果を受け渡す例で使い方を確認します。
Schoo「Java入門 中級」第6回「Webアプリケーション構築演習(基礎編)」に対応する補足記事です。授業を受けていなくても読めるよう、この記事だけで例の仕様とコードを示します。WebのコードはJava 17・Tomcat 10.1の jakarta.servlet を前提にしています。
合計だけ返すと、画面に小計と送料を出せない
次のような見積もり画面を作るとします。DBへの保存や注文の確定は行わず、数量から金額を計算して表示するだけです。
- 商品の単価は500円。数量は1〜20の整数を受け付ける。
- 小計は「500円 × 数量」。
- 小計が3000円未満なら送料300円、3000円以上なら送料0円。
- 合計は「小計 + 送料」。画面には小計・送料・合計の3つを表示する。
| 数量 | 小計 | 送料 | 合計 |
|---|---|---|---|
| 5 | 2500円 | 300円 | 2800円 |
| 6 | 3000円 | 0円 | 3000円 |
メソッドから int の合計2800だけを返すと、呼び出したServletには小計と送料が届きません。だからといって、JSPでもう一度送料を計算すると、送料のルールを変更したときに複数箇所の修正が必要になります。計算は一か所で行い、その結果をまとめて渡す方が、役割を分けやすくなります。
// この書き方では3つの値を返せません(コンパイルエラー)
return subtotal, shippingFee, total;
Javaのメソッドが一度のreturnで返す値は一つです。ただし、複数のフィールドを持つオブジェクトの参照を、一つの戻り値として返すことはできます。そこで、小計・送料・合計を持つ QuoteResult クラスを用意します。
return new QuoteResult(subtotal, shippingFee, total);
returnするものは一つですが、そのオブジェクトに3つの金額が入っています。受け取った側は、必要な金額をgetterで取り出します。
DTOとは「受け渡すデータをまとめる」という役割
DTOは Data Transfer Object の略です。本記事では、Modelの処理結果などを別の役割へ受け渡すためのデータクラスとして扱います。DTO という予約語や、必須のアノテーションがあるわけではありません。
クラス名も QuoteDTO でなければならないわけではありません。今回は「見積もり結果」であることが分かる QuoteResult と名付けます。クラスの名前だけではなく、何をまとめて、どこへ渡すのかがDTOを理解するポイントです。
| 担当 | 今回のクラス・ファイル | すること |
|---|---|---|
| Modelの計算処理 | QuoteModel | 数量を検証し、小計・送料・合計を計算する |
| 結果のデータ | QuoteResult(DTO) | 計算済みの3つの金額を持ち、getterで返す |
| Controller | QuoteServlet | 入力を受け取り、Modelを呼び、結果をJSPへ渡す |
| View | quote-result.jsp | DTOの値を取り出してHTMLに表示する |
MVCのModelは、データや業務ルールを扱う部分を広く指します。DTOはModelと対立する「第4の役割」ではありません。ここではModel側の中でも、計算するクラスと、結果をまとめるクラスを分けていると考えてください。
イラスト:3つの金額を、一つの結果として画面へ渡す

ServletがModelを呼び出した後、計算結果がこの順に渡ります。ブラウザへQuoteResultオブジェクトを直接送るわけではありません。JSPがサーバー側で金額をHTMLに埋め込み、ブラウザはそのHTMLを受け取ります。
1.結果をまとめるQuoteResultクラスを作る
src/main/java/model/QuoteResult.java
package model;
public class QuoteResult {
private final int subtotal;
private final int shippingFee;
private final int total;
public QuoteResult(int subtotal, int shippingFee, int total) {
this.subtotal = subtotal;
this.shippingFee = shippingFee;
this.total = total;
}
public int getSubtotal() {
return subtotal;
}
public int getShippingFee() {
return shippingFee;
}
public int getTotal() {
return total;
}
}
このクラスが、今回のDTOです。コンストラクタで受け取った値をフィールドへ入れ、getterでそのまま取り出します。「3000円以上なら送料無料」といった条件分岐は、ここには書きません。今回は計算済みの結果を持つ役割に絞っています。
| コード | 意味 |
|---|---|
| private final int subtotal; | このオブジェクトが持つ小計。privateなので外から直接書き換えず、finalなので初期化後に再代入しない。 |
| this.subtotal = subtotal; | 引数subtotalの値を、このオブジェクトのフィールドsubtotalへ入れる。 |
| getSubtotal() | 保存してある小計を返す。もう一度小計を計算する処理ではない。 |
この例では、計算後に値がばらばらに書き換わらないよう、finalのフィールドとgetterだけにしています。DTOに必ずfinalが必要、あるいは必ずsetterが必要という決まりはありません。また、DTOという名前が付いていれば値の正しさまで保証されるわけではありません。このサンプルではQuoteModelが整合する金額を計算してから渡します。
2.Modelで計算し、DTOを戻り値にする
src/main/java/model/QuoteModel.java
package model;
public class QuoteModel {
public QuoteResult calculate(int quantity) {
if (quantity < 1 || quantity > 20) {
throw new IllegalArgumentException("数量は1〜20で指定してください。");
}
int subtotal = 500 * quantity;
int shippingFee = 300;
if (subtotal >= 3000) {
shippingFee = 0;
}
int total = subtotal + shippingFee;
return new QuoteResult(subtotal, shippingFee, total);
}
}
メソッドの戻り値の型が int ではなく、QuoteResult になっています。数量5で呼ぶと、最後の行は new QuoteResult(2500, 300, 2800) と同じ値をコンストラクタに渡します。
QuoteModel model = new QuoteModel();
QuoteResult result = model.calculate(5);
System.out.println(result.getSubtotal()); // 2500
System.out.println(result.getShippingFee()); // 300
System.out.println(result.getTotal()); // 2800
これは呼び出す側の処理の抜粋です。result は、戻されたQuoteResultオブジェクトを指す変数です。「計算するためのmodel」と「計算結果を持つresult」は、別のオブジェクトです。
DTOに入れる値は、同じ型だけでなくても構いません。たとえば見積もり番号をString、合計をintで持つこともできます。ただし、今回必要なのは3つの金額なので、関係のない画面情報やDB接続情報まで一緒に入れません。
3.Servletで結果を受け取り、リクエスト属性に入れる
入力画面をアプリ直下に置き、POSTで数量を送ります。ここからはファイル単位のコードです。アプリのコンテキストパスは /lesson06 として説明します。環境が別名なら、その部分を読み替えてください。
src/main/webapp/quote-form.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>見積もり入力</title>
</head>
<body>
<h1>見積もり入力</h1>
<p>単価500円。小計3000円未満は送料300円です。</p>
<form action="quote" method="post">
<label for="quantity">数量(1〜20)</label>
<input id="quantity" name="quantity" type="number"
min="1" max="20" required>
<button type="submit">見積もりを表示</button>
</form>
</body>
</html>
src/main/java/controller/QuoteServlet.java
package controller;
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;
import model.QuoteModel;
import model.QuoteResult;
@WebServlet("/quote")
public class QuoteServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
String text = request.getParameter("quantity");
if (text == null || text.isBlank()) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
request.getRequestDispatcher("/quote-error.jsp")
.forward(request, response);
return;
}
QuoteResult result;
try {
int quantity = Integer.parseInt(text.trim());
QuoteModel model = new QuoteModel();
result = model.calculate(quantity);
} catch (IllegalArgumentException e) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
request.getRequestDispatcher("/quote-error.jsp")
.forward(request, response);
return;
}
request.setAttribute("quote", result);
request.getRequestDispatcher("/quote-result.jsp")
.forward(request, response);
}
}
DTOに関係するのは、次の3か所です。
result = model.calculate(quantity);で計算結果を受け取る。request.setAttribute("quote", result);で、その結果をリクエストに預ける。forward()で、同じリクエストを使ってJSPへ処理を進める。
DTOとリクエストスコープは別のものです。DTOは「受け渡すデータの形」、リクエストスコープは「そのデータを置いておく場所」です。setAttributeでDTOの各フィールドを文字列へ変換するわけでも、別のDTOを自動で作るわけでもありません。このサンプルでは同じオブジェクトの参照を渡しています。
一方、getParameter("quantity") はブラウザから送られた入力をStringとして取得する処理です。入力パラメータのquantityと、サーバー内で設定した属性のquoteは区別してください。
入力が空、整数ではない、範囲外の場合は400とエラー画面を返します。NumberFormatExceptionはIllegalArgumentExceptionの子クラスなので、ここでは数値変換とModelの検証を同じcatchで扱えます。HTMLのmin・max・requiredだけに頼らず、サーバー側でも確認しています。
4.JSPはDTOから値を取り出して表示する
src/main/webapp/quote-result.jsp
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="model.QuoteResult" %>
<%
QuoteResult result =
(QuoteResult) request.getAttribute("quote");
%>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>見積もり結果</title>
</head>
<body>
<h1>見積もり結果</h1>
<% if (result != null) { %>
<p>小計: <%= result.getSubtotal() %>円</p>
<p>送料: <%= result.getShippingFee() %>円</p>
<p>合計: <%= result.getTotal() %>円</p>
<% } else { %>
<p>入力画面から数量を送信してください。</p>
<% } %>
<a href="quote-form.html">入力画面に戻る</a>
</body>
</html>
getAttribute("quote") の戻り値はObject型です。この例ではServletがQuoteResultを入れているので、(QuoteResult) とキャストして受け取ります。キャストで新しいDTOを作ったり、計算したりするわけではありません。
JSPに小計や送料の計算式はありません。getSubtotal()・getShippingFee()・getTotal() で、Modelが計算した値を表示するだけです。送料のルールを変える場合はQuoteModelの計算を直し、JSPの表示はそのまま使えます。
quote-result.jspを直接開くと、Servletを通らないのでquote属性はありません。取得結果はnullになります。この例ではnullを確認し、入力画面からの送信を案内しています。
入力エラー用:src/main/webapp/quote-error.jsp
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>入力内容の確認</title>
</head>
<body>
<h1>入力内容の確認</h1>
<p>数量は1〜20の整数で入力してください。</p>
<a href="quote-form.html">入力画面に戻る</a>
</body>
</html>
この例がHTMLへ出しているのは、固定文言と計算済みのint値です。DTOに商品名などの文字列を追加する場合、DTOに入れたこと自体では安全になりません。外部から来た文字列をHTMLへ表示するときは、出力に応じたエスケープが必要です。
動作確認:DTOの値と画面の値がつながっているか
http://localhost:8080/lesson06/quote-form.html を開きます。Servletの /quote を直接開くのではなく、フォームから送信してください。このServletはPOSTを受け付ける設計です。
| 確認する操作 | 期待する結果 | 見るポイント |
|---|---|---|
| 数量5を送信 | 小計2500円・送料300円・合計2800円 | 3つのgetterで、同じ見積もりの内訳を表示できる |
| 数量6を送信 | 小計3000円・送料0円・合計3000円 | 送料無料の境界がModelの計算に反映される |
| 数量20を送信 | 小計10000円・送料0円・合計10000円 | 許容範囲の上限でも表示できる |
| 数量0・21・abcを直接POST | 400・入力エラー画面 | 画面側の制限を回避した入力も検証される |
| quote-result.jspを直接開く | 入力画面から数量を送信するよう案内 | DTOがない経路でnullのままgetterを呼ばない |
通常のブラウザ操作では、numberやmin・maxの制約で不正な送信が止まる場合があります。サーバー側の検証は、HTTPクライアントなどで不正値を直接送るテストと分けて確認します。
配列やMapで返す方法とは何が違うのか
今回の3つの値は、配列で返すこともできます。ただし、受け取る側に「0番は小計、1番は送料、2番は合計」という約束が必要になります。
// 配列で返す場合の取り出し方
int total = amounts[2];
// DTOで返す場合の取り出し方
int total = result.getTotal();
配列は同じ種類の要素の並びを扱うのが得意です。小計・送料・合計のようにそれぞれ意味が違う値には、名前の付いたフィールドとgetterがあると、使い間違いに気付きやすくなります。
Mapならキーに名前を付けられますが、amounts.get("totla") のようなキーの誤字はコンパイル時には分かりません。DTOで存在しない getTotla() を呼べばコンパイルエラーになります。必要な項目と型をクラスで明示できるのが、DTOを用意するメリットです。
ただし、合計一つしか使わないメソッドなら、intで返すだけで十分な場合もあります。どんな戻り値にもDTOを作るのではなく、関連する複数の値を一つの結果として渡したいかを基準に考えます。
DTOを使うときに混同しやすいこと
- DTOを作るだけではDBには保存されない。保存が必要なら、別途DAOなどでDB処理を書く。今回の例にDBは登場しない。
- DTOはリクエスト専用ではない。通常のJavaのメソッド間でも使える。今回はJSPへ渡すためにリクエスト属性を使っている。
- getterを呼んでも再計算はしない。今回のgetterは保存済みの値を返すだけ。送料のルールはQuoteModelにある。
- DTOに入れればすべて正しい値になるわけではない。入力の検証や計算の責任は、設計した処理側で持つ。
- DTOは「使う目的」の呼び名。この例では特別な継承やimplements、Serializableの指定は必要ない。
確認問題
この記事の単価500円、数量1〜20、小計3000円以上で送料無料という仕様を使って考えてください。
model.calculate(5)が返したDTOについて、getSubtotal()・getShippingFee()・getTotal()が返す値をそれぞれ答えてください。- 数量6ではDTOの3つの値はどうなりますか。また、送料無料になる基準を5000円へ変更するとき、DTO・Model・JSPのどこを修正しますか。
- 次の処理を担当するファイルを答えてください。「送料を決める」「計算結果の3つの値を持つ」「結果をrequest属性へ入れる」「結果の金額をHTMLへ出す」。
- Servletでは
setAttribute("quote", result)、JSPではgetAttribute("result")と書きました。何が起き、どう直しますか。JSPでnew QuoteResult(…)を書いて作り直すべきでしょうか。
演習後の確認:こちらのページを使って、自分で考えた内容を見直せます。
まとめ
- DTOは、関連するデータを一つにまとめて受け渡すためのクラス。
- Modelで計算した結果をDTOで返せば、呼び出し側は名前付きのgetterで取り出せる。
- DTO・リクエストスコープ・JSPの役割は別。データの形、置き場所、表示を分けて考える。
- 計算する処理と表示する処理に同じ計算式を重複させず、一つの結果を受け渡す。