Spring BootのDTO変換はどこでやるべきかを解説する記事のアイキャッチ。若手エンジニアがJava道場風の学習空間でDTOとEntityの変換を学ぶイラスト。

Spring

Spring BootのDTO変換はどこでやるべきか

AIによる要約

Spring BootのDTO変換は、必ずController、必ずServiceと決まるものではありません。大事なのは、Entityを外へ直接出さず、画面/API用の形に変換することです。単純な変換はControllerやService、複雑な変換はMapperへ分けると読みやすくなります。

新人SE
新人SE
EntityをDTOに変換する処理って、ControllerとServiceのどちらに書くべきですか?
ポンコツSE
ポンコツSE
変換の意味で考えます。画面/APIの形に整えるだけならController寄り、業務判断を含むならService寄り、複雑ならMapperへ分けます。

Spring Bootの現場では、Entityをそのまま画面やAPIに返さず、DTOへ変換することが多いです。

しかし、DTO変換をどこに書くかは初学者が迷いやすいポイントです。Controllerに書くのか、Serviceに書くのか、Mapperを作るのか、現場でも方針が分かれます。

この記事のポイント

  • Entityを外部向けレスポンスとして直接返さない
  • 単純な表示用変換はController寄りでもよい
  • 業務判断を含む変換はService寄りで考える
  • 複雑な変換や再利用する変換はMapperへ切り出す

まずEntityをそのまま返さない

EntityはDBと関係するクラスです。画面やAPIで返したい項目と、DB上の項目は一致しないことがあります。

@GetMapping("/users/{id}")
public UserEntity getUser(@PathVariable Long id) {
    return userService.findEntity(id);
}

この書き方では、Entityの内部構造がAPIに出てしまいます。不要な項目、内部管理項目、リレーションが混ざると、画面側にも影響します。

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

外部へ返す形は、レスポンスDTOとして明示する方が安全です。

単純な変換ならControllerでも読める

ServiceがEntityを返し、ControllerでレスポンスDTOへ詰め替える方針の現場もあります。変換が単純で、画面/APIの表現に近い場合は読みやすいことがあります。

@GetMapping("/users/{id}")
public UserResponse getUser(@PathVariable Long id) {
    User user = userService.findUser(id);
    return new UserResponse(user.getId(), user.getName(), user.getEmail());
}

この変換は、取得したUserをAPIレスポンスの形に整えているだけです。Controllerにあっても、処理の意図は大きく崩れていません。

ただし、変換が長くなったり、複数Controllerで同じ変換を使ったりするなら、Mapperへの切り出しを考えます。

業務判断を含むならServiceで扱う

DTO変換の中に、会員ランク、利用可否、表示制御、権限判定のような業務判断が入る場合は、Controllerだけで完結させると責務が混ざります。

public UserDetailResponse findDetail(Long userId) {
    User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException(userId));

    boolean editable = permissionService.canEdit(user);

    return new UserDetailResponse(
            user.getId(),
            user.getName(),
            editable
    );
}

この例では、編集可能かどうかという業務判断が含まれています。このような場合はService側に置く方が自然です。

複雑ならMapperへ切り出す

項目数が多い、ネストしたDTOがある、複数の場所で同じ変換を使う場合は、Mapperクラスへ切り出すと読みやすくなります。

@Component
public class UserMapper {
    public UserResponse toResponse(User user) {
        return new UserResponse(
                user.getId(),
                user.getName(),
                user.getEmail()
        );
    }
}

Mapperを作ると、ControllerやServiceの中に詰め替え処理が広がるのを防げます。特に、画面が増えてDTOが複数になると効果があります。

Request DTOからEntityへ直接詰め替えない

リクエストDTOをそのままEntityへ詰め替える処理にも注意が必要です。画面入力とDB更新は意味が違うからです。

public void update(UserUpdateRequest request) {
    User user = userRepository.findById(request.getId()).orElseThrow();
    user.setName(request.getName());
    user.setEmail(request.getEmail());
    userRepository.save(user);
}

この程度なら分かりやすいですが、更新してよい項目、権限、状態チェック、null時の扱いが増えると、単純な詰め替えでは危険です。

更新処理では、DTO変換というより「どの項目を、どの条件で更新してよいか」という業務処理としてServiceで扱います。

どこに書くかの判断基準

DTO変換の置き場所を決める基準

  • 表示/APIの形に整えるだけならController寄り
  • 業務判断や権限判定を含むならService寄り
  • 同じ変換を複数箇所で使うならMapper
  • 項目数が多くControllerが読みにくいならMapper
  • Entityを外に出すための言い訳としてDTO変換を省略しない

大事なのは、どこに置くかを機械的に決めることではなく、変換の意味を説明できることです。

現場レビューではどう見られるか

// レビューコメント例
// Controller内でEntityを直接返しています。
// APIレスポンスとして不要な項目まで出る可能性があるため、
// UserResponseへ変換してください。
//
// 変換処理が複数箇所で使われているので、UserMapperへの切り出しも検討してください。

この指摘は、DTOを作ること自体が目的ではありません。外部に出す情報を制御し、ControllerやServiceを読みやすく保つための指摘です。

レビュー前のセルフチェック

DTO変換の確認リスト

  • Entityをそのまま返していないか
  • 外部に出してよい項目だけDTOに入っているか
  • 変換処理がControllerで長くなりすぎていないか
  • 業務判断をControllerの変換処理に混ぜていないか
  • 同じ変換が複数箇所に重複していないか

この記事とあわせて読みたい

まとめ

DTO変換の置き場所に絶対の正解はありません。ただし、Entityを直接外へ出さないこと、変換の意味を説明できること、複雑ならMapperへ切り出すことを意識すると、現場でレビューされにくい設計になります。

-Spring
-, , , , , ,