【Spring Boot】DTOとEntityを使い分ける設計の考え方

Java

こんにちは、かつコーチです。

「EntityをそのままControllerでレスポンスとして返しているけど、これで大丈夫かな」と不安になったことはありませんか。

動くには動くのですが、この書き方は運用が進むにつれて確実に問題を起こします。

この記事では、DTO(Data Transfer Object)とEntityをなぜ使い分けるべきか、実際のコードを見ながら解説します。

DTOとEntityとは何か

Entityはデータベースの構造をそのまま表すクラス

Entityとは、@Entityアノテーションを付けた、データベースのテーブル構造をそのまま表すクラスです。

Spring Data JPAがこのクラスをテーブルとマッピングし、CRUD操作を可能にします。

Entityの役割は「データベースとどう対応するか」であり、外部にどう見せるかとは本来別の関心事です。

DTOは層をまたいでデータをやり取りするためのクラス

DTOとは、Controllerとクライアントの間、あるいはService層とController層の間でデータをやり取りするために使う、専用のクラスです。

Entityとは異なり、データベースのテーブル構造とは無関係に、「この画面・APIで何を渡したいか」だけを表現します。

なぜEntityをそのまま公開してはいけないのか

❌ Before:EntityをそのままAPIレスポンスにする

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String email;
    private String passwordHash;
    private LocalDateTime createdAt;

    // getter・setterは省略
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
    return userRepository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException("ユーザーが見つかりません"));
}

このコードには重大な問題があります。

passwordHashのような、外部に絶対に返してはいけないフィールドまでそのままJSONに変換されてしまうことです。

筆者は以前、動作確認用に作った簡易APIをそのまま残してしまい、レスポンスにpasswordHashが含まれていることに気づかず数日運用していた経験があります。

幸い外部公開前の検証環境でしたが、本番であればセキュリティインシデントになっていたところでした。

✅ After:DTOを経由して必要な項目だけ返す

public class UserResponse {
    private Long id;
    private String name;
    private String email;

    public UserResponse(User user) {
        this.id = user.getId();
        this.name = user.getName();
        this.email = user.getEmail();
    }

    // getterは省略
}
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) {
    User user = userRepository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException("ユーザーが見つかりません"));
    return new UserResponse(user);
}

DTOを挟むことで、「外部に公開してよい項目」だけを明示的に選べます。

passwordHashのような機密情報が、うっかりレスポンスに混ざるリスクを構造的になくせます。

実装手順

手順1:用途別にDTOを分ける

DTOは「リクエスト用」と「レスポンス用」を分けて設計するのが基本です。

// リクエスト用(作成時に受け取る項目のみ)
public class UserCreateRequest {
    @NotBlank
    private String name;

    @Email
    private String email;

    @NotBlank
    private String password;
}

// レスポンス用(公開してよい項目のみ)
public class UserResponse {
    private Long id;
    private String name;
    private String email;
}

作成時に受け取りたい項目(password)と、公開してよい項目(idnameemail)は一致しないことがほとんどです。

同じクラスで両方を兼用しようとすると、片方のためだけに存在するフィールドが増え、意図が読み取りにくくなります。

手順2:Entity⇔DTOの変換をService層に集約する

変換ロジックをControllerに書くと、変換処理があちこちに散らばります。

@Service
public class UserService {

    private final UserRepository userRepository;

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

    public UserResponse createUser(UserCreateRequest request) {
        User user = new User();
        user.setName(request.getName());
        user.setEmail(request.getEmail());
        user.setPasswordHash(hashPassword(request.getPassword()));

        User saved = userRepository.save(user);
        return new UserResponse(saved);
    }
}

ControllerはUserServiceを呼ぶだけになり、Entityの構造を意識する必要がなくなります。

つまずきやすい設定・注意点

変換処理が増えてくると、UserからUserResponseへの変換コードを毎回手書きするのが大変になります。

実務ではMapStructのようなマッピングライブラリを導入し、変換コードの自動生成に任せるケースも多いです。

@Mapper(componentModel = "spring")
public interface UserMapper {
    UserResponse toResponse(User user);
}

小規模なプロジェクトであれば手書きでも十分ですが、DTOの種類が増えてきたら導入を検討する価値があります。

応用・一歩先の使い方

レイヤーごとに責務を分けて考える

DTOとEntityの使い分けは、次のように整理すると迷いにくくなります。

レイヤー使うクラス役割
ControllerDTO外部とのやり取り専用
ServiceEntity・DTO両方ビジネスロジックと変換の橋渡し
RepositoryEntityデータベースとのやり取り専用

この整理を徹底すると、「Entityの内部構造を変えてもAPIの形は変わらない」という状態を作れます。

データベースのカラムを増減するたびに、外部公開しているAPI仕様まで変わってしまうのを防げるのが、この設計の一番の利点です。

レコード型でDTOを簡潔に書く

Java 17以降であれば、recordを使うとDTOのボイラープレートを大幅に減らせます。

public record UserResponse(Long id, String name, String email) {
    public static UserResponse from(User user) {
        return new UserResponse(user.getId(), user.getName(), user.getEmail());
    }
}

getterやコンストラクタを自動生成してくれるため、レスポンス専用の不変DTOとの相性が非常によいです。

まとめ

この記事のポイント

  • EntityをそのままAPIレスポンスにすると、機密情報の漏えいリスクがある
  • DTOは「リクエスト用」「レスポンス用」で分けて設計する
  • Entity⇔DTOの変換はService層に集約する
  • Java 17以降ならrecordでDTOを簡潔に書ける

次に読むべき記事

DTOで受け取った入力値を検証する仕組みについては、前回の記事で解説しています。

→ 前の記事:Bean Validationでフォームの入力チェックを実装する

続く認証・セキュリティ編では、Spring Securityの基本を解説します。

タグ: #SpringBoot #中級者向け #設計

タイトルとURLをコピーしました