REST API設計とDTO: 包装のない贈り物は危険だ

「え? なんでパスワードが見えるの?」

入社したばかりの頃、私はフロントエンドとバックエンドを行ったり来たりしながら必死に開発していた。ある日、同期が作った「会員一覧取得API」をフロントエンド画面に連携している最中だった。

データが正しく入ってくるか確認しようと、Chrome DevTools(F12)のネットワークタブを開いた。そしてレスポンスデータを見た瞬間、自分の目を疑った。

[
  {
    "id": 1,
    "username": "tester",
    "password": "$2a$10$D...", // 暗号化されたパスワードが露出
    "ssn": "900101-1...",      // 個人番号(住民番号)が露出
    "createdAt": "2024-01-01"
  }
]

驚いて同期に聞いた。 「おい、このAPIレスポンス、パスワードと住民登録番号まで見えてるんだけど?」

同期はあっさり答えた。 「ああ、それ? どうせ画面には名前しか出さないでしょ。面倒だから userRepository.findAll() の結果をそのまま返したんだけど、何か問題ある?」

背筋が寒くなった。同期はただ「楽をしたかった」だけなのに、結果として深刻なセキュリティ事故の種をまいていたわけだ。

「画面に見えないから安全、ではない!」

ブラウザのネットワークタブは誰でも見られる。もし悪意ある攻撃者がこのAPIを呼び出したらどうなるだろうか。

エンティティをそのまま外に出すのは、自宅の寝室を全面ガラス張りにするのと変わらなかった。

中身が丸見えの透明な袋は、泥棒への招待状と同じだ。

DTO: データの「包装紙」

「エンティティ(Entity)」は工場の中でだけ扱う「原材料」だ。そこには顧客に見せてはいけない機密情報(パスワード)や、内部管理用の情報(作成日、更新日、削除フラグ)がべったり付いている。

一方、「DTO(Data Transfer Object)」は顧客に届けるためにきれいに包装された「完成品」だ。

エンティティを直接返すというのは、レストランで焼き上がったステーキを出す代わりに、血のついた生肉を皿に投げるようなものだ。必ず「変換(マッピング)」の工程を挟まなければならない。

[Code Verification] 無限ループの沼(循環参照)

セキュリティ問題以上に開発者を狂わせるのが「循環参照(Circular Reference)」の問題だ。前回学んだJPAの双方向マッピング、User <-> Team を覚えているだろうか。

UserエンティティをそのままJSONに変換して返したら、どうなるだろう。

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne // ユーザーはチームに所属する
    private Team team;
}

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team") // チームは複数のユーザーを持つ
    private List<User> users = new ArrayList<>();
}

この状態で User を返すと、JSONシリアライザ(Jackson)はこう動く。

結果: StackOverflowError とともにサーバーは壮烈に散る。エンティティ間の関係は互いのしっぽを追い続けるので、それを切らずにそのまま出すと「無限ループ」に落ちる。DTOを使って必要なフィールド、たとえば teamName だけを切り出して持たせるべき決定的な理由がここにある。

関係を切り離さなければ、DTOなしのデータは永遠に回り続ける。

良いAPIの条件: RESTfulであること

DTOでデータをきちんと包装したら、次はその荷物を送る「住所」をきちんと決める必要がある。これがまさに「REST API設計」だ。

大学生の頃の私は、URLを好き勝手に付けていた。

joinupdate のような動詞をURLに入れるのは良くない。RESTfulな設計では、「リソース(名詞)」はURLに、「振る舞い(動詞)」はHTTP Methodに任せる。

こうしておけば、URLを見るだけで「ああ、ユーザーというリソースを扱っているんだな」と分かるし、フロントエンド開発者とのコミュニケーションもずっと楽になる。これは世界中の開発者が共有している約束事だ。

実務アドバイス: 名前が半分を決める(DTO命名戦略)

今の会社のプロジェクトコードを開くと、時々ため息が先に出る。UserDtoMemberVoInfoParamResultData… 前任者たちが好き勝手に名前を付けているので、これがリクエスト用のオブジェクトなのか、レスポンス用なのか、コードを開くまで分からない。

DTOはデータの「器」だ。器の用途が名前に書かれていなければ、汁物用の器にご飯を盛り、ご飯茶碗に水を入れるような混乱が起きる。実務でいちばんおすすめしたい命名ルールは次のとおりだ。

public class UserDto {
    // 会員登録リクエスト
    public static class SignUpRequest {
        private String email;
        private String password;
    }

    // 会員情報レスポンス
    public static class Response {
        private String name;
        private String email;
    }
}

シリーズ2を終えて: これで家は建った

これで [Monolith Builder] シリーズは終了だ。私たちはSpring Boot(DI)で土台を固め、フロントエンドとバックエンドを分離し(CORS)、JPAでDBを扱い、DTOとREST APIでやり取りする方法まで身につけた。

これで私たちは「一人でもちゃんとしたWebサービスを作れる力」を手に入れた。でも、開発者の旅はここで終わらない。自分が作ったサービスが自分のPC、つまり Localhost でしか動かないなら意味がない。実際のユーザーに見せるには「サーバー」に載せなければならない。

ところがサーバーはWindowsではなく、黒い画面だけのLinuxだ。マウスもないその場所で、どうやってプログラムをインストールし、実行するのだろうか。

次のシリーズ [Works on My Machine] では、開発者たちの永遠の悩みである「自分のPCでは動くのにサーバーでは動かない」を解決するために、LinuxとDockerの世界へ入っていこう。

コメントする