本ページはプロモーションが含まれています

Java入門

JavaのDTOとは?Entity/DAOとの違いを事故防止の視点で解説

CATEGORY
REV.
DRAWN BY
TOM
SHEETS
9枚
DTO(Entity・DAOとの違い)

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

𝕏

DTOとは「Data Transfer Object」の略で、レイヤー間でデータを受け渡す専用オブジェクトです。

DTOは「箱」、Entityは「器」、DAOは「係」と覚えると迷いません。

「Entityをそのまま返せば楽では?」と思うかもしれませんが、それがパスワードハッシュの漏洩やDBカラム変更の連鎖事故につながります。

前提はJava 21/25 LTS・Spring Boot 4.x。比較表とRecord型の実装、アンチパターン4選まで紹介します。

この記事でわかること

  • JavaのDTOとは何か、なぜ必要なのか
  • DTO・Entity・DAOの違い(VO・Form・POJOとの使い分けも)
  • Spring Bootでの実装方法(サンプルコード付き)
  • Java 17以降のRecord型を使った現代的なDTOの書き方
  • 現場でやりがちなDTOアンチパターン4選と回避策
  • DTOを導入すべきか迷ったときの判断フロー

私も新人時代は「DTOは冗長だ」と感じていました。しかしある改修でDBの変更が画面表示まで波及し、1日の作業が3日に膨らむ事故を起こしました。

層を分離する重要性はここで腑に落ちました。

PARTS LIST /目次

JavaのDTOとは?意味と役割をわかりやすく解説

JavaにおけるDTOは、異なるシステムやレイヤー(層)の間でデータを転送するための専用オブジェクトです。

たとえば、ユーザーの情報を扱うWebアプリケーションを考えてみましょう。

  1. ユーザーがブラウザから入力した情報(リクエスト)を、Controllerが受け取る
  2. Controllerは、受け取ったデータをServiceに渡してビジネスロジックを実行する
  3. Serviceは、必要に応じてRepositoryを使い、データベース(Entity)を操作する
  4. 処理結果を、逆の順序でブラウザ(レスポンス)に返す

この一連の流れの中で、各層の間をデータが行き来します。このデータの受け渡しに使われる「運び屋」がDTOの役割です。

DTOとEntityがそれぞれどの層の間を流れるかを図にすると、次のとおりです。

POST /users で、層を移るたびにデータの入れ物(DTO/Entity)がどう変わるか
1
ブラウザ → CONTROLLER / DTOUserCreateRequestDto@RequestBody で JSON が入る。name・email・password(平文)の3項目だけ
2
CONTROLLER → SERVICE / DTOuserService.createUser(requestDto)
3
SERVICE / DTO → ENTITYDTO を Entity User に詰め替えるパスワードはハッシュ化してからセット。id はまだ未採番
4
SERVICE → REPOSITORY → DB / ENTITYDB に保存し、id が自動採番される
5
DB → SERVICE / ENTITY保存済みの User が戻るid・name・email に加え、パスワードハッシュも持っている
SERVICE → ブラウザ何に詰めて返すか?
OK / DTO に詰め替える(この記事のサンプル)○ convertToDto() で UserResponseDto にpassword の項目はない。ブラウザに届くのは id・name・email だけ
NG / Entity をそのまま return(アンチパターン1)✕ パスワードハッシュが漏れるEntity の全項目が JSON になって外に出る。DB のカラムを足すと、レスポンスにも増える
NOTEController は受け取った DTO を返すだけです。詰め替えは Service が受け持ちます。

DTO(Data Transfer Object)の基本概念

DTOは、データを入れるフィールドと、それを読み書きするgetter/setterメソッドだけを持つクラスです。このように特定のフレームワークに縛られないシンプルなJavaオブジェクトをPOJOと呼びます。

ビジネスロジックのような複雑な処理は含みません。

役割を絞るほど各層の責任がはっきりし、システムの構造もシンプルに保てます。

JavaでDTOがよく使われる理由|密結合を防ぐ設計思想

JavaでDTOが頻繁に使われる最大の理由は、システムの関心事を分離し、各層の独立性を高めるためです。

もしDTOを使わずに、データベースと直接やりとりする「Entity」をそのまま画面表示にまで使い回すとどうなるでしょうか。

UIの都合で表示項目を少し変えたいだけなのに、データベースの構造にまで影響が及びます。

逆に、データベースの項目名を変更しただけで、APIのレスポンス形式が変わり、フロントエンドで大規模な修正が必要になる場合もあります。

各層が密接に結びつくと、一方の変更がもう一方に影響する「密結合」な状態に陥ります。

DTOは、この密結合を避け、各層を疎結合に保つための重要な緩衝材として機能するのです。

DTO・Entity・DAOの違いを比較表で整理

Java初学者が最も混乱しやすいのが、DTO・Entity・DAOの違いです。DTOとEntityはどちらもデータを持つクラスですが、目的が違います。DAOはデータを持たず、DBを読み書きする「処理」側のクラスです。

項目DTO (Data Transfer Object)EntityDAO (Data Access Object)
ひとことでデータを運ぶ「箱」テーブルを写した「器」データを取りに行く「係」
目的レイヤー間のデータ転送データベースのテーブルとのマッピングデータベースへの読み書き
役割画面表示やAPIの入出力に必要なデータを保持データベースのレコードを表現SQLを発行してEntityを取得・保存
ロジック持たない(getter/setterのみ)持つことがある(ドメインロジック)DB操作メソッド(find, save 等)
寿命一時的(リクエストからレスポンスまで)永続的(データベースに保存される)データを持たない処理クラス
例UserResponseDto, ProductCreateRequestDtoUser, ProductUserDao, UserRepository

Entityは「永続化層」の住人で、データベースのテーブル構造と密接に関わります。

JPA(Jakarta Persistence。旧称 Java Persistence API)では@Entity アノテーションを付けて、テーブルのレコードを表現します。

パスワードのような、外部に公開すべきではない機密情報も保持することがあります。

一方、DTOは「プレゼンテーション層」や「アプリケーション層」の住人です。

画面に表示したい項目だけ、あるいはAPIで受け取りたい項目だけを過不足なく定義します。

Entityの情報をそのまま使うのではなく、必要なデータだけをDTOに詰め替えてから外部とやり取りするのです。

この使い分けにより、データベースの構造を外部から隠蔽し、セキュリティを高める効果もあります。

3つの関係は、DAOがEntityをDBから取得し、ServiceがEntityをDTOに詰め替え、ControllerがDTOを返すという一方通行の流れで整理できます。DAOとの違いは、後半の「DTOとDAO(Repository)の違い」で詳しく解説します。

JavaでDTOを使うメリット・デメリット

DTOは事故を防ぐ一方で、クラスが増えるという代償もあります。両面を先に押さえておきましょう。

DTOを導入することで得られるメリット

DTOを適切に利用すると、主に以下のメリットが得られます。

  1. DTOを使えば、APIのレスポンスに必要な情報(例: ユーザー名、メールアドレス)だけを抜き出して返せる。意図しない情報漏洩を防ぎ、セキュリティを高められる
  2. フロントエンドとバックエンドの独立性が高まり、それぞれが並行して開発を進めやすくなる

DTOを使うときの注意点・デメリット

最大のデメリットは、コード量が増加することです。

Entityに加えてDTOクラスを定義し、さらにEntityとDTOの間でデータを相互に変換する「Mapper」と呼ばれるロジックを実装します。

単純なアプリケーションでは、この手間が開発速度を落とす原因です。

また、クラスの数が増えることで、管理が煩雑になる側面もあります。

特に、似たような構造のDTOが乱立すると、どれを使えばよいのかわからなくなり、かえって生産性を下げるリスクがあります。

どんなプロジェクトにDTOが向いているか

前述のメリット・デメリットを踏まえると、DTOの導入が特に効果的なのは、以下のようなプロジェクトです。

  • 中規模から大規模のWebアプリケーション: 複数の開発者が関わり、機能が複雑になるプロジェクトでは、関心の分離による保守性の向上が大きなメリットになる
  • マイクロサービスアーキテクチャ: サービス間で通信する際は、APIの仕様をDTOで明確に定義する必要がある
  • フロントエンドとバックエンドが分離されているプロジェクト: ReactやVue.jsと連携する場合、DTOを介してJSON形式でやりとりするのが一般的
  • セキュリティ要件が厳しいシステム: 個人情報などを扱うシステムでは、情報漏洩リスクを低減するためにDTOの利用が強く推奨される

逆に、個人開発の小さなツールや、ごく単純なCRUD機能しかない管理画面では、DTOを導入するメリットよりもコードが増えるデメリットのほうが大きくなります。

DTOを導入すべきか迷ったときの判断チェックリストを用意しました。1つでも該当すれば導入を強く推奨します。

  • 外部(フロントエンド・他システム)にAPIを公開する
  • 認証情報・個人情報・決済情報などの機密データを扱う
  • 2名以上のチーム開発である
  • 将来的にテーブル構造の変更が予想される

すべてNoで「個人開発の使い捨てツール」なら、Entityを直接返しても大きな問題にはなりません。

JavaでDTOを実装する方法【サンプルコード付き】

基本的なDTOクラスの書き方

DTOクラスは、データを保持するためのフィールドと、それに対応するgetter/setterを持つシンプルなPOJOです。

たとえば、ユーザー情報を画面に表示するためのDTOは以下のとおりです。

public class UserDto {

    private Long id;
    private String name;
    private String email;

    // GetterとSetter
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

毎回getter/setterを書くのは手間なので、Lombokというライブラリを導入すると、アノテーションを付けるだけで自動生成できます。

import lombok.Data;

@Data // Getter, Setter, toString, equals, hashCodeを自動生成
public class UserDto {
    private Long id;
    private String name;
    private String email;
}

Lombokを使うことで、コードがすっきりします。

Spring BootでDTOを使う例

Spring Bootを使ったWebアプリケーションで、DTOがどのように使われるかを見てみましょう。

ここでは、新しいユーザーを作成するAPIを例にします。

まず、クライアントから送られてくるリクエストデータを受け取るためのDTO(Request DTO)を定義します。

UserCreateRequestDto.java
import lombok.Data;

@Data
public class UserCreateRequestDto {
    private String name;
    private String email;
    private String password;
}

次に、このDTOをリクエストボディとして受け取るControllerを実装します。

UserController.java
@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService userService;

    // UserServiceをコンストラクタインジェクション
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public UserResponseDto createUser(@RequestBody UserCreateRequestDto requestDto) {
        // Service層にDTOを渡してユーザー作成処理を依頼
        return userService.createUser(requestDto);
    }
}

@RequestBody を付けると、SpringがJSON形式のリクエストデータを UserCreateRequestDto に自動でマッピングします。

Controller・Service・Entityとの連携例

Controllerが受け取ったDTOは、Service層に渡されてビジネスロジックが実行されます。

Service層では、DTOからEntityへの変換が行われ、データベースに保存されます。

User.java (Entity)
import lombok.Data;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Data
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    private String password; // 実際はハッシュ化されたパスワードを保存
}
UserService.java
@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public UserResponseDto createUser(UserCreateRequestDto requestDto) {
        // DTOからEntityへの変換
        User user = new User();
        user.setName(requestDto.getName());
        user.setEmail(requestDto.getEmail());
        // パスワードはハッシュ化するなどの処理を行う
        user.setPassword(hashPassword(requestDto.getPassword()));

        // Repositoryを使ってデータベースに保存
        User savedUser = userRepository.save(user);

        // 保存したEntityをResponse DTOに変換して返す
        return convertToDto(savedUser);
    }

    // EntityからDTOへの変換メソッド
    private UserResponseDto convertToDto(User user) {
        UserResponseDto dto = new UserResponseDto();
        dto.setId(user.getId());
        dto.setName(user.getName());
        dto.setEmail(user.getEmail());
        return dto;
    }
    
    // パスワードハッシュ化のダミーメソッド
    private String hashPassword(String password) {
        // Spring SecurityのPasswordEncoderなどを使う
        return "hashed_" + password;
    }
}
UserResponseDto.java
import lombok.Data;

@Data
public class UserResponseDto {
    private Long id;
    private String name;
    private String email;
}

この例では、以下の流れで処理が進みます。

  1. Controllerが UserCreateRequestDto を受け取る。
  2. Serviceが UserCreateRequestDto を受け取り、User Entityに変換する。
  3. 変換したUser Entityをデータベースに保存する。
  4. 保存されたUser Entityを、レスポンス用の UserResponseDto に変換する。
  5. Controllerが UserResponseDto をクライアントに返す。

注目すべきは、レスポンス用の UserResponseDto にはパスワードが含まれていない点です。

DTOを使い分けることで、外部に公開する情報を安全にコントロールできます。

Java 21/25のRecord型で書くDTO(現代的な書き方)

Java 14でプレビュー導入され、Java 16で正式化(JEP 395)されたRecord型は、DTOの記述量を大幅に減らせる機能です。

Record型はJava 21/25 LTSで標準的に使えます。Spring Boot 4.x 以降のプロジェクトでは、LombokよりもRecord型を第一候補にするケースが増えています。

Record型を使ったDTOの書き方

前述の UserResponseDto をRecord型で書き直すと、以下の1行で済みます。

public record UserResponseDto(Long id, String name, String email) {}

Record型では、コンパイラが以下を自動生成します。

  • プライベートなfinalフィールド
  • フィールド名と同じ名前のアクセサメソッド(getId() ではなく id())
  • 全フィールドを引数に取るコンストラクタ
  • equals()、hashCode()、toString() メソッド

Record型のメリットと注意点

Record型を使う最大のメリットは、不変(Immutable)なDTOをシンプルに定義できることです。

setterが存在しないため、値が意図せず書き換えられるバグを防げます。フィールドがfinalなので、中身(構成要素)も不変であればスレッド間で安全に共有でき、APIレスポンスとの相性も良好です。

私の現場では、Lombokの@DataクラスをRecord型に置き換えました。するとequals()/hashCode()を手動テストしていたユニットテストが不要になり、DTO関連のテストコード行数が実測で約3割減りました。

Record型はコンパイラが等価性判定を保証してくれます。そのため「フィールドを追加したのにequalsの更新を忘れて比較テストが通らない」という事故が起きなくなったのが、実務上の一番の恩恵です。

一方、以下の点には注意が必要です。

  • Bean Validationは効く: @NotNull や @Size はコンポーネント引数に付与でき、Spring Bootでも問題なく動作する
  • 継承ができない: Record型は暗黙的にfinalなので、共通親クラスを継承する設計には使えない
  • JPA Entityには向かない: Entity は引数なしコンストラクタと可変フィールドが必要なため、Record型は避けてclass + Lombokにする
  • Jacksonでのデシリアライズは自動対応: Jackson 2.12+ は Record型を追加設定なしでデシリアライズできる

新規プロジェクトでは、まずRecord型で書けないかを考えます。可変性やデフォルト値が必要な場合だけLombokの @Data を使う、という使い分けが2026年8月時点の主流です。

また、Java 21 LTSで正式化されたRecord Patterns(JEP 440)も使えます。if (obj instanceof UserResponseDto(Long id, String name, String email)) と書くと、Record型DTOをフィールドに分解しながら変数へ束縛できます。

読み取りコードの記述量がさらに削減できるため、Java 21以降のプロジェクトでは積極的に活用を検討しましょう。

Record型とLombokの使い分けフロー

実務で迷ったときの判断基準を整理しておきます。

  • 不変でOK・DBマッピング不要(レスポンスDTOの大半) → Record型を第一候補
  • 途中で値加工が必要・Builderパターンで組み立てたい → Lombokの @Data や @Builder
  • JPA Entity(引数なしコンストラクタと可変フィールドが必要) → class + Lombok(Record型は使えない)

私のチームでは、Spring Boot 3.2から4.1へのアップグレード時にレスポンスDTOをすべてRecord型に移行しました。

setterを廃止すると、レビューで「このフィールド、途中で書き換えられていないか?」を確認する手間がなくなりました。コードレビューの平均所要時間も、体感で2割ほど短縮されています。

ただし、リクエストDTOの一部はあえてRecord型に移行しませんでした。

バリデーションエラー時にフィールドごとの補足情報を後付けで詰め直す処理があり、Record型のfinalフィールドではその実装が回りくどくなったためです。

私なりの線引きは「新規はRecord型、既存の可変ロジックが絡む箇所は無理に移行しない」です。Record型を導入する現場なら、一度は検討する価値があると考えています。

DTOとEntity・DAO・VO・Form・POJOの違いを比較

DTOと似た目的で使われる言葉や、関連する概念がいくつかあります。

DTOとDAO(Repository)の違い|役割は別物

DAOは「Data Access Object」の略で、データベースへのアクセス処理を担当する層です。

名前が似ているためDTOと混同されがちですが、役割は全く異なります。

端的に言うと、DAOは「データを取りに行く人」、DTOは「データを運ぶ箱」です。

DAOがSELECTやINSERTなどのSQLを発行してデータベースから結果を取得します。

その結果をEntityやDTOに詰めて、呼び出し元に返します。

Spring Bootの世界では、DAOと近い役割を担うのがRepositoryクラスです。

項目DTO (Data Transfer Object)DAO (Data Access Object)
役割データを運ぶ「箱」データにアクセスする「処理」
中身フィールドとgetter/setterDB操作メソッド(find, save 等)
対応層レイヤー間のやりとり全般永続化層
例UserDtoUserDao、UserRepository

つまり、DAO・Entity・DTOは競合する概念ではなく、役割分担している協力関係にあります。

VO(Value Object)との違い

VO(Value Object)もデータを保持するオブジェクトですが、DTOとは異なる特性を持ちます。

VOは「値そのもの」を表現するオブジェクトです。

VOの大きな特徴は、不変(Immutable)であることと、等価性の判断をIDではなく保持している値で行うことです。

具体例として、「金額」を表現する Money クラスを考えます。「1,000円」という値は、誰が持っていても「1,000円」です。

オブジェクトが作られた後に金額が変わることはありません(不変)。

そして、2つのMoneyオブジェクトが同じ「1,000円」という値を持っていれば、それらは等しいと判断されます。

一方、DTOは値そのものを表すのではなく、転送したいデータをまとめる入れ物です。getter/setter型のDTOは可変(Mutable)ですが、Record型を使えば不変にもできます。

UserDto は、同じ名前やメールアドレスを持っていても、IDが違えば別のユーザーのデータです。

項目DTO (Data Transfer Object)VO (Value Object)
性質可変が一般的(Record型なら不変にもできる)不変 (Immutable)
等価性特に決まりはない(@DataやRecordの自動生成equalsは全フィールドの値で比較)保持する値で判断
目的データ転送値の表現、ドメインルールのカプセル化
例UserDtoMoney, ZipCode

DTOとForm(フォームオブジェクト)の違い

Spring MVCでは、画面の入力フォームから送信されたデータを受け取るための「Formオブジェクト」を使うことがあります。

FormもDTOも「データの入れ物」という点では似ていますが、データの流れる方向と用途が異なります。

項目DTO (Data Transfer Object)Form(フォームオブジェクト)
主な用途レイヤー間・API間のデータ転送画面入力データの受け取り
バリデーション任意(付けることも多い)ほぼ必須(@NotNull, @Size等)
利用フレームワークSpring Boot全般・マイクロサービスSpring MVC(テンプレートエンジン連携)
データの方向双方向(リクエスト/レスポンス)主にリクエスト(画面→サーバー)

REST APIが主流の現在では、Formオブジェクトを使わずにリクエストDTOで直接受け取るケースが増えています。

Spring MVCのテンプレートエンジン(Thymeleaf等)を使う場合は、Formを分ける意味があります。一方、JSON APIのみの構成ならリクエストDTOに統一しても問題ありません。

POJO(Plain Old Java Object)との関係

POJOは「Plain Old Java Object」の略で、直訳すると「古き良き普通のJavaオブジェクト」となります。

これは、特定のフレームワークやライブラリが要求する特別なルールに縛られない、Javaの基本的な仕様だけで作られたオブジェクトを指します。

extends で特定のクラスを継承したり、implements で特定のインターフェースを実装したりする制約がなく、自由に定義できるのが特徴です。

DTOは、このPOJOの一種です。 上記で作成した UserDto も、特定のフレームワークへの依存がないため、POJOです。

同様に、EntityやVOも多くの場合POJOとして実装されます。

POJOは広義な言葉で、DTOはその具体的な使い方の1つだと理解しておくとよいでしょう。

DTOを使うときのベストプラクティス

最後に、DTOを効果的に活用し、メンテナンスしやすいコードを書くためのベストプラクティスをいくつか紹介します。

命名規則とパッケージ構成の考え方

DTOクラスは、その役割がひと目でわかるように命名してください。

  • リクエスト用DTO: UserCreateRequest, ProductUpdateRequest のように Request や Form を接尾辞につける
  • レスポンス用DTO: UserResponse, ProductDetailResponse のように Response や Dto を接尾辞につける

加えて、DTOクラスは専用のパッケージにまとめて管理するのが一般的です。

com.example.myapp
├── controller
│   └── UserController.java
├── service
│   └── UserService.java
├── repository
│   └── UserRepository.java
├── entity
│   └── User.java
└── dto
    ├── UserCreateRequest.java
    └── UserResponse.java

このようなパッケージ構成にすることで、プロジェクトの全体像が把握しやすくなります。

ModelMapperやMapStructを使った変換効率化

前述のとおり、手動でのマッピングはコードの肥大化とバグの温床です。

中規模以上のプロジェクトでは、ModelMapperやMapStructのようなマッピングライブラリの導入を積極的に検討しましょう。

  • ModelMapper: 設定が簡単で手軽に導入できる。リフレクションを利用するため、実行速度はMapStructに劣る場合がある
  • MapStruct: コンパイル時にマッピングコードを自動生成するため、高速に動作する。アノテーションベースで設定が少し複雑だが、パフォーマンスが求められる場合に強力な選択肢となる

プロジェクトの要件に合わせて適切なライブラリを選定し、変換ロジックをシンプルに保ってください。

ModelMapperを使った例:
ModelMapper modelMapper = new ModelMapper();
// DTOからEntityへ
User user = modelMapper.map(requestDto, User.class);
// EntityからDTOへ
UserResponseDto responseDto = modelMapper.map(savedUser, UserResponseDto.class);

手動でフィールドを一つずつセットするコードが不要になり、生産性が大幅に向上します。

私の経験では、ModelMapperからMapStructに切り替えると、フィールド名の不一致をコンパイル時にエラーとして検出できるようになりました。その結果、「DTOにフィールドを追加したのにマッピング漏れで画面にnullが表示される」というバグが激減しています。

初期の学習コストはやや高いものの、中長期で見ればMapStructのほうが保守コストは低くなります。

導入初期に苦労したのは、チームの新人メンバーが「生成されたマッパー実装クラスがどこにあるかわからない」と混乱したことです。

MapStructはアノテーションプロセッサでビルド時にコードを自動生成するため、ソースコード上には実装が見当たりません。

target/generated-sources(Mavenの場合)配下を見る習慣を最初にオンボーディング資料へ明記しておくと、この種のつまずきを防げます。

メンテナンスしやすいDTO設計のポイント

長期的にメンテナンスしやすいDTOを設計するためには、以下の点を意識するとよいでしょう。

  • 必要なデータだけを持つ: DTOには、そのユースケースで本当に必要なフィールドだけを定義する。使わないデータをとりあえず入れておく、という設計は避ける
  • バリデーションを活用する: リクエストDTOに @NotNull や @Size などのBean Validationアノテーションを付けると、Controllerに届く前に不正なデータを弾ける。これにより、Service層のロジックをシンプルに保てる
  • DTOのネストを適切に利用する: 関連する情報が複数ある場合、DTOの中に別のDTOを持たせる(ネストする)ことで、複雑なデータ構造を表現できる。ただし、ネストが深くなりすぎると逆に扱いにくくなるため、バランスが重要

DTO設計でよくあるアンチパターンと回避策

DTOは使い方を誤ると、むしろコードを複雑にし、バグを生みます。私が実プロジェクトで見かけた代表的なアンチパターンを4つ紹介します。

アンチパターン1: EntityをDTO代わりに使う

「手間だから」という理由でEntityをそのままControllerから返してしまうパターンです。

APIレスポンスにパスワードハッシュなどの機密情報が漏れたり、データベースのカラム追加がフロント側に波及したりと、事故の元です。

私が参画したプロジェクトでは、Entityをそのまま返していたAPIがありました。ある日、「管理画面から見えてはいけない退会済みユーザーのメールアドレス」がフロントエンドに露出していることが発覚しました。

DTOで公開フィールドを明示的に選別していれば防げた事故です。

回避策: APIの境界には必ず専用DTOを定義し、EntityからDTOへの詰め替え処理をService層で実施します。

アンチパターン2: 巨大な汎用DTOの使い回し

1つの UserDto に30フィールドを詰め込み、登録・更新・一覧・詳細すべてで使い回すパターンです。

これにより未使用フィールドが大量に発生し、nullチェックや意図しない情報漏洩を招きます。

私の現場では、肥大化UserDtoのレビュー時に使用フィールドの判定だけで平均10分以上かかり、PRが渋滞しました。

回避策: ユースケース単位でDTOを分割します。たとえば UserCreateRequest、UserUpdateRequest、UserListResponse、UserDetailResponse のように分けます。

クラス数は増えますが、各APIの契約が明確になります。

アンチパターン3: DTOにビジネスロジックを書く

DTO内に金額計算や認可判定などのロジックを書いてしまうと、Service層とDTOの責務が混ざり、テストもしづらくなります。

回避策: DTOは「データの入れ物」に徹します。

ロジックが必要な表示値は、Service層でドメインオブジェクトから計算し、DTOには計算結果だけを詰めます。

アンチパターン4: setter公開で可変DTOを多用する

可変DTOが危ないのは、渡した先が同じ箱を書き換えられるからです。図で動かすJVMメモリ|スタックとヒープ・参照・GCの関係では、引数として渡されるのが「値のコピー」なのか「場所のコピー」なのかを、矢印で見ながら確認できます。

レスポンスDTOなのにsetterを公開していると、途中の処理でうっかり値を書き換えてしまい、バグの再現が難しくなります。

回避策: レスポンス用DTOはRecord型で不変にします。

リクエスト用DTOも、基本はコンストラクタで値を受け取り、setterは避ける方針が安全です。

JavaのDTOに関するよくある質問(FAQ)

Q. DTO・Entity・DAOの違いを一言で言うと?

DTOは層の間でデータを運ぶ箱、EntityはDBのテーブルを表すクラス、DAOはDBを読み書きする処理クラスです。DAOが取得したEntityを、ServiceがDTOに詰め替えて画面やAPIに返します。

Q. 小規模プロジェクトでもDTOは必須ですか?

個人開発の小さなCRUDアプリなら、必須ではありません。ただし、APIを公開する、認証情報を扱う、複数人で開発するのいずれかに当てはまる場合はDTOを導入する価値があります。後からDTOを差し込むほうがコストが高いので、早めの導入が安全です。

Q. DTOとRecord型、どちらを使えばいいですか?

Java 17以降のプロジェクトなら、レスポンスDTOはRecord型が第一候補です。Jackson連携も自動で動き、不変性でバグを防げます。リクエストDTOも基本はRecord型で問題ありませんが、バリデーション後に値を加工する必要があるならLombokの @Data や @Builder を検討してください。

Q. DTOのネストは深くしてもいいですか?

ネストは2階層までが実務上の目安です。3階層以上になると、JSON構造が読みにくくなり、マッピングコードも複雑化します。深くなりそうなときは、APIを分割するか、フラットなDTOに組み替えるのが無難です。

Q. EntityとDTOの変換が面倒です。楽にできませんか?

MapStructの利用が最も効率的です。コンパイル時にマッピングコードを生成するためリフレクションに頼らず、実行速度が高速で、型チェックも効きます。2026年8月時点のSpring Bootプロジェクトでは、ModelMapperよりもMapStruct(安定版1.6系。1.7系はベータ提供中)を選ぶ現場が増えています。

Q. DTOとFormオブジェクトは同じものですか?

厳密には異なります。Formは画面入力を受け取る専用オブジェクトで、バリデーションが主な役割です。DTOはレイヤー間のデータ転送全般に使います。ただし、REST APIのみの構成ではリクエストDTOがFormの役割を兼ねるため、実質的に同じものとして扱うプロジェクトも多いです。

Q. DTOの読み方は?Java以外の言語でも使いますか?

DTOの読み方は「ディーティーオー」で、Data Transfer Object(データ・トランスファー・オブジェクト)の頭文字です。一般的にはアルファベットをそのまま読みます。

メモ

DTOはJavaに限らず、C#(.NET)・PHP(Laravel)・TypeScript(NestJS)など多くの言語で「レイヤー間のデータ転送専用オブジェクト」として同じ考え方が使われている。

本記事のコード例はJava/Spring Boot前提ですが、Entityを直接返さずDTOに詰め替えるという原則はどの言語でもそのまま通用します。

まとめ|DTOを理解すると設計の質が上がる

冒頭の「EntityをそのままAPIで返せば楽では?」への答えは、「楽なのは最初だけ」です。ポイントを振り返ります。

  • DTOは、異なるレイヤー間でデータを転送するための専用オブジェクトである。
  • EntityはDBのテーブルを表現し、DTOは画面やAPIの都合に合わせたデータ構造を持つ。
  • DAO(Repository)はDBを読み書きする処理クラスで、DAOが取得したEntityをDTOに詰め替えて返すのが基本の流れ。
  • DTOを使うことで、セキュリティの向上と、フロントエンドとバックエンドの独立性(変更に強い設計)という大きなメリットが得られる。
  • 実装時には、LombokやMapStructなどのライブラリを活用すると、コードを簡潔に保てる。
  • Record型を使えば不変なDTOをシンプルに書け、setterの誤用やequals/hashCodeの手動テストの手間も減らせる。
  • 本記事で紹介したアンチパターン4選(Entityの流用、巨大DTOの使い回し、ロジックの混入、可変DTOの多用)を避けるだけで、事故のリスクは大きく下がる。

DTOは一見冗長に見えます。しかし役割を理解すれば、各層を疎結合に保てます。結果として変更に強く、メンテナンスしやすい堅牢なシステムになります。

REVISIONS / 改訂履歴
REV.DATE内容
62026-10-21同じ説明の繰り返しを減らし、読みやすく整えた
52026-10-15図解を描き直し(図面のデザインにそろえた)
42026-10-14一人称と言い回しの表記をそろえた
32026-08-222026年8月時点の情報で全体を見直し
… ほか1回の改訂
12025-10-11初版
NEXT READ本棚から1本

AIで開発を組み直すなら、まずこの5冊から

入門・実践・設計まで、Claude Codeの本をレベル別に5冊選んで比べました。

→ おすすめ5冊を見る

  • この記事を書いた人
  • 最新記事

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

𝕏

Java入門