「え? なんでパスワードが見えるの?」
入社したばかりの頃、私はフロントエンドとバックエンドを行ったり来たりしながら必死に開発していた。ある日、同期が作った「会員一覧取得API」をフロントエンド画面に連携している最中だった。
データが正しく入ってくるか確認しようと、Chrome DevTools(F12)のネットワークタブを開いた。そしてレスポンスデータを見た瞬間、自分の目を疑った。
[
{
"id": 1,
"username": "tester",
"password": "$2a$10$D...", // 暗号化されたパスワードが露出
"ssn": "900101-1...", // 個人番号(住民番号)が露出
"createdAt": "2024-01-01"
}
]
驚いて同期に聞いた。 「おい、このAPIレスポンス、パスワードと住民登録番号まで見えてるんだけど?」
同期はあっさり答えた。 「ああ、それ? どうせ画面には名前しか出さないでしょ。面倒だから userRepository.findAll() の結果をそのまま返したんだけど、何か問題ある?」
背筋が寒くなった。同期はただ「楽をしたかった」だけなのに、結果として深刻なセキュリティ事故の種をまいていたわけだ。
「画面に見えないから安全、ではない!」
ブラウザのネットワークタブは誰でも見られる。もし悪意ある攻撃者がこのAPIを呼び出したらどうなるだろうか。
- 個人情報の流出: 全会員の住民番号と電話番号が抜き取られる。
- アカウント乗っ取り: 暗号化されたパスワードであっても、総当たり攻撃(Brute Force)の手がかりになりうる。
- 内部ロジックの露出:
createdAtやcreatedByのようなフィールドから、DB構造や管理者IDのパターンを読み取られかねない。
エンティティをそのまま外に出すのは、自宅の寝室を全面ガラス張りにするのと変わらなかった。

DTO: データの「包装紙」
「エンティティ(Entity)」は工場の中でだけ扱う「原材料」だ。そこには顧客に見せてはいけない機密情報(パスワード)や、内部管理用の情報(作成日、更新日、削除フラグ)がべったり付いている。
一方、「DTO(Data Transfer Object)」は顧客に届けるためにきれいに包装された「完成品」だ。
- Entity:
User(id, password, name, ssn, createdAt, createdBy…) -> 工場用 - DTO:
UserResponse(id, name) -> 配達用
エンティティを直接返すというのは、レストランで焼き上がったステーキを出す代わりに、血のついた生肉を皿に投げるようなものだ。必ず「変換(マッピング)」の工程を挟まなければならない。
[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)はこう動く。
- User を開ける。 -> 中に Team があるぞ?
- Team を開ける。 -> 中に Users があるぞ?
- Users リストの最初の User を開ける。 -> 中に Team があるぞ?
- Team を開ける。 -> 中に Users があるぞ?
- (無限に繰り返す…)
結果: StackOverflowError とともにサーバーは壮烈に散る。エンティティ間の関係は互いのしっぽを追い続けるので、それを切らずにそのまま出すと「無限ループ」に落ちる。DTOを使って必要なフィールド、たとえば teamName だけを切り出して持たせるべき決定的な理由がここにある。

良いAPIの条件: RESTfulであること
DTOでデータをきちんと包装したら、次はその荷物を送る「住所」をきちんと決める必要がある。これがまさに「REST API設計」だ。
大学生の頃の私は、URLを好き勝手に付けていた。
- 会員登録:
/joinUser - 会員情報の修正:
/updateUser - 会員の削除:
/deleteUser
join や update のような動詞をURLに入れるのは良くない。RESTfulな設計では、「リソース(名詞)」はURLに、「振る舞い(動詞)」はHTTP Methodに任せる。
- POST
/users: 会員登録(作れ) - GET
/users/1: 1番の会員を取得(持ってこい) - PUT
/users/1: 1番の会員を修正(差し替えろ) - DELETE
/users/1: 1番の会員を削除(消せ)
こうしておけば、URLを見るだけで「ああ、ユーザーというリソースを扱っているんだな」と分かるし、フロントエンド開発者とのコミュニケーションもずっと楽になる。これは世界中の開発者が共有している約束事だ。
実務アドバイス: 名前が半分を決める(DTO命名戦略)
今の会社のプロジェクトコードを開くと、時々ため息が先に出る。UserDto、MemberVo、InfoParam、ResultData… 前任者たちが好き勝手に名前を付けているので、これがリクエスト用のオブジェクトなのか、レスポンス用なのか、コードを開くまで分からない。
DTOはデータの「器」だ。器の用途が名前に書かれていなければ、汁物用の器にご飯を盛り、ご飯茶碗に水を入れるような混乱が起きる。実務でいちばんおすすめしたい命名ルールは次のとおりだ。
- 接尾辞(Suffix)で役割を明示せよ
- リクエスト(Request):
動作+ドメイン+Request- 例:
JoinUserRequest,UpdateUserRequest
- 例:
- レスポンス(Response):
ドメイン+Response- 例:
UserResponse,UserListResponse
- 例:
- 単に
UserDtoと書くと、会員登録で使うものなのか照会で使うものなのか紛らわしく、毎回ファイルを開いて確かめる羽目になる。
- リクエスト(Request):
- インナークラス(Inner Class)でグループ化せよ ファイルが増えすぎるのが嫌なら、「インナークラス」戦略を強くおすすめする。
UserDtoというクラスを1つ作り、その中に関連するDTOをstatic classとしてまとめて入れるのだ。こうするとコードではUserDto.SignUpRequestと書くことになり、「ああ、ユーザー関連の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の世界へ入っていこう。


