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

Java入門

JavaのDTOとは?3つのメリットと使い方をコードで解説

トム

・SaaS勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

「Javaのフレームワークを学習中だけど、DTOという言葉がよく出てきて意味がわからない…」

「EntityとDTOって、どちらもデータを扱うクラスのようだけど、何が違うの?」

「DTOを導入するメリットや、具体的な使い方が知りたい」

この記事は、そのような悩みを持つJava初学者から中級者の方に向けて書いています。

私自身、新人時代はDTOの存在意義を理解できず、「同じようなコードを何度も書いて面倒だ」と感じていました。しかし、あるサービスの機能改修プロジェクトで、データベースの変更が画面表示にまで影響を及ぼすという痛い経験をしました。この失敗から、層を分離して変更に強い設計を作ることの重要性を学び、DTOの役割を深く理解するに至ったのです。

この記事を最後まで読めば、JavaのDTOがなぜ必要なのか、Entityとどう使い分けるのか、そして具体的な実装方法まで、体系的に理解できます。 サンプルコードも豊富に用意しているので、あなたの設計スキルを一段階引き上げる手助けとなるはずです。

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

JavaにおけるDTOは、異なるシステムやレイヤー(層)の間でデータを転送するため専用のオブジェクトです。DTOは「Data Transfer Object」の略で、直訳すると「データ転送オブジェクト」となります。

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

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

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

DTO(Data Transfer Object)の基本概念

DTOは、データを格納するためのフィールドと、そのデータにアクセスするためのgetter/setterメソッドだけを持つ、非常にシンプルなJavaオブジェクト(POJO)です。ビジネスロジックのような複雑な処理は含みません。

DTOの目的は、純粋にデータを運ぶことです。そのため、データの入れ物として特化しており、余計な機能は持ちません。これにより、各層の役割が明確になり、システムの構造をシンプルに保つ助けになります。

JavaでDTOがよく使われる理由

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

もしDTOを使わずに、データベースと直接やりとりする「Entity」をそのまま画面表示にまで使い回してしまうと、どうなるでしょうか。UIの都合で少し表示項目を変えたいだけなのに、データベースの構造にまで影響が及ぶ可能性があります。逆に、データベースの項目名を変更しただけで、APIのレスポンス形式が変わってしまい、フロントエンドで大規模な修正が必要になるかもしれません。

このように、各層が密接に結びついてしまうと、一方の変更がもう一方に影響を与える「密結合」な状態になります。DTOは、この密結合を避け、各層を疎結合に保つための重要な緩衝材として機能するのです。特に、Spring Bootなどのフレームワークを使った階層化されたアーキテクチャでは、DTOの利用が一般的なプラクティスとなっています。

Entityとの違いを整理して理解する

Java初学者が最も混乱しやすいのが、DTOとEntityの違いです。どちらもデータを保持するクラスですが、その目的と役割が根本的に異なります。

項目DTO (Data Transfer Object)Entity
目的レイヤー間のデータ転送データベースのテーブルとのマッピング
役割画面表示やAPIの入出力に必要なデータを保持データベースのレコードを表現
ロジック持たない(getter/setterのみ)持つことがある(ドメインロジック)
寿命一時的(リクエストからレスポンスまで)永続的(データベースに保存される)
UserResponseDto, ProductCreateRequestDtoUser, Product

Entityは「永続化層」の住人で、データベースのテーブル構造と密接に関わります。JPA (Java Persistence API) では @Entity アノテーションを付けて、テーブルのレコードを表現します。パスワードのような、外部に公開すべきではない機密情報も保持することがあります。

一方、DTOは「プレゼンテーション層」や「アプリケーション層」の住人です。画面に表示したい項目だけ、あるいはAPIで受け取りたい項目だけを過不足なく定義します。Entityの情報をそのまま使うのではなく、必要なデータだけをDTOに詰め替えてから外部とのやりとりを行うのです。

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

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

DTOの概念を理解したところで、次に導入による具体的なメリットと、知っておくべきデメリットを見ていきましょう。

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

Java開発でDTOを適切に利用すると、主に3つの大きなメリットが得られます。

  1. セキュリティの向上Entityクラスには、ユーザーのパスワードハッシュや内部管理用のフラグなど、外部に漏洩してはならない情報が含まれる場合があります。DTOを使えば、APIのレスポンスに必要な情報(例: ユーザー名、メールアドレス)だけを抜き出して返せます。これにより、意図しない情報漏洩を防ぎ、アプリケーションのセキュリティを高めることが可能です。
  2. 関心の分離と保守性の向上DTOは、UI(フロントエンド)とビジネスロジック(バックエンド)の間の「契約」の役割を果たします。たとえば、フロントエンドは「このAPIを叩けば、UserDto の形式でデータが返ってくる」ということだけを知っていればよく、バックエンドで使われているEntityの構造やデータベースのスキーマを意識する必要はありません。これにより、フロントエンドとバックエンドの独立性が高まり、それぞれが並行して開発を進めやすくなります。また、将来的にデータベースの構造を変更しても、DTOへの変換処理を修正するだけで済み、フロントエンドへの影響を最小限に抑えられます。
  3. API仕様の安定化DTOを使うことで、APIのインターフェース(リクエストやレスポンスの形式)を安定させられます。データベースの都合でEntityのフィールドが増減しても、DTOの構造を変えなければAPIの仕様は変わりません。これは、特に外部に公開するAPIを開発する場合に重要です。安定したAPIは、クライアント側での実装を容易にし、信頼性を高めます。

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

多くのメリットがある一方で、DTOの導入にはいくつかの注意点もあります。

最大のデメリットは、コード量が増加することです。Entityに加えてDTOクラスを定義し、さらにEntityとDTOの間でデータを相互に変換する「Mapper」と呼ばれるロジックを実装する必要があります。単純なアプリケーションでは、この手間が開発速度を低下させる原因になるかもしれません。

また、クラスの数が増えることで、管理が煩雑になる側面もあります。特に、似たような構造のDTOが乱立すると、どれを使えばよいのか分からなくなり、かえって生産性を下げてしまう危険性もはらんでいます。

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

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

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

逆に、個人開発の小さなツールや、ごく単純なCRUD機能しかない管理画面などでは、DTOを導入するメリットよりもコードが増えるデメリットの方が大きくなる可能性があります。プロジェクトの規模や特性に応じて、導入を検討するのがよいでしょう。

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

ここからは、実際のコードを見ながらJavaでDTOをどのように実装し、活用していくのかを解説します。

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

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

たとえば、ユーザー情報を画面に表示するための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 アノテーションを付けることで、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を使い分けることで、外部に公開する情報を安全にコントロールできます。

DTOと他の設計パターンの違い

DTOと似た目的で使われる言葉や、関連する概念がいくつかあります。ここでは、VOとPOJOとの違い、そしてDTO変換を効率化するMapperについて解説します。

VO(Value Object)との違い

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

VOは「値そのもの」を表現するオブジェクトです。VOの大きな特徴は、不変(Immutable)であることと、等価性の判断をIDではなく保持している値で行うことです。

たとえば、「金額」を表現する Money クラスを考えます。「1,000円」という値は、誰が持っていても「1,000円」です。オブジェクトが作られた後に金額が変わることはありません(不変)。そして、2つのMoneyオブジェクトが同じ「1,000円」という値を持っていれば、それらは等しいと判断されます。

一方、DTOは可変(Mutable)であり、IDで個別に識別されるエンティティのデータを転送するために使われます。UserDto は、同じ名前やメールアドレスを持っていても、IDが違えば別のユーザーのデータです。

項目DTO (Data Transfer Object)VO (Value Object)
性質可変 (Mutable)不変 (Immutable)
等価性IDで判断保持する値で判断
目的データ転送値の表現、ドメインルールのカプセル化
UserDtoMoney, ZipCode

POJO(Plain Old Java Object)との関係

POJOは「Plain Old Java Object」の略で、直訳すると「古き良き普通のJavaオブジェクト」となります。これは、特定のフレームワークやライブラリが要求する特別なルールに縛られない、Javaの基本的な仕様だけで作られたオブジェクトを指します。

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

DTOは、このPOJOの一種です。 上記で作成した UserDto も、特定のフレームワークへの依存がないため、POJOと言えます。同様に、EntityやVOも多くの場合POJOとして実装されます。POJOは非常に広義な言葉で、DTOはその具体的な使い方の1つだと理解しておくとよいでしょう。

Mapperを使ったDTO変換の実践

先ほどのUserServiceの例では、DTOとEntityの変換を手動で行いました。

// DTOからEntityへの変換
User user = new User();
user.setName(requestDto.getName());
user.setEmail(requestDto.getEmail());

フィールド数が少ない場合は問題ありませんが、多くなってくるとこの変換コードは非常に冗長になり、ミスも発生しやすくなります。そこで役立つのが、オブジェクト間のマッピングを自動化してくれるライブラリです。代表的なものに ModelMapperMapStruct があります。

これらのライブラリを使うと、フィールド名が同じであれば、面倒なsetterの呼び出しを一行で済ませられます。

ModelMapperを使った例:

ModelMapper modelMapper = new ModelMapper();
// DTOからEntityへ
User user = modelMapper.map(requestDto, User.class);
// EntityからDTOへ
UserResponseDto responseDto = modelMapper.map(savedUser, UserResponseDto.class);

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

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

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

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

DTOクラスは、その役割がひと目でわかるように命名することが重要です。

  • リクエスト用DTO: UserCreateRequest, ProductUpdateRequest のように RequestForm を接尾辞につける。
  • レスポンス用DTO: UserResponse, ProductDetailResponse のように ResponseDto を接尾辞につける。

また、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: コンパイル時にマッピングコードを自動生成するため、高速に動作する。アノテーションベースで設定が少し複雑だが、パフォーマンスが求められる場合に強力な選択肢となる。

プロジェクトの要件に合わせて適切なライブラリを選定し、変換ロジックをシンプルに保つことが大切です。

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

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

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

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

この記事では、JavaにおけるDTOの基本概念から、Entityとの違い、具体的な実装方法、そしてベストプラクティスまでを解説しました。

最後に、重要なポイントを振り返ります。

  • DTOは、異なるレイヤー間でデータを転送するための専用オブジェクトである。
  • EntityはDBのテーブルを表現し、DTOは画面やAPIの都合に合わせたデータ構造を持つ。
  • DTOを使うことで、セキュリティの向上保守性の向上API仕様の安定化という3つの大きなメリットが得られる。
  • 実装時には、LombokMapStructなどのライブラリを活用すると、コードを簡潔に保てる。

DTOは、一見するとコード量を増やすだけの冗長な存在に思えるかもしれません。しかし、その役割を正しく理解し、適切に活用することで、アプリケーションの各層を疎結合に保ち、変更に強く、メンテナンスしやすい堅牢なシステムを構築できます。

DTOを使うべきシーンを見極めよう

すべてのプロジェクトでDTOが必須というわけではありません。しかし、システムの規模が大きくなり、複雑性が増すほど、DTOによる関心の分離がもたらす恩恵は大きくなります。あなたのプロジェクトが将来的に拡張していく可能性を考慮し、DTOを導入すべきかを見極めることが、優れた設計への第一歩です。

コード設計をシンプルに保つための心構え

DTOを導入する目的は、あくまでも設計をクリーンに保つことです。DTOのルールに縛られすぎて、過剰にクラスを作成したり、複雑な変換ロジックを組んだりしては本末転倒です。常に「なぜDTOを使うのか?」という目的に立ち返り、シンプルで分かりやすい設計を心がけましょう。

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

トム

・SaaS勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

-Java入門