Old-School Backend / 2026 ・ No.01

いにしえのバックエンド入門

第1回 全体図 ── 申請1件が、サーバーの中で DB に入るまで

公開

Wireshark の画面。申請サーバー(192.168.1.20)の口で記録した一覧で、4番が PC からの POST /api/expenses、6番から16番が DB サーバー(192.168.1.30)との PGSQL のやりとり、19番が HTTP/1.1 201。13番が選ばれ、明細の PostgreSQL(Type: Parse)に Query: insert into expense (id, applied_on, item, amount, status) values ($1, $2, $3, $4, 'submitted') と Parameters: 4 が見える
申請1件の裏側。PC からの申請(4番)を受けた申請サーバーは、返事(19番)を書く前に、DB サーバーと4往復しています。選んだ13番は、申請を表に書きこむ SQL です(筆者がクラウド上に作った練習用ネットワークで、2026年10月26日に記録したものを Wireshark 4.2.2 で開いて撮影。画面は英語のまま。ウインドウの上の帯と見出しは説明のために描き足したもの)

フロントエンド編で申請フォームを作り、ネットワーク編でその申請が線を渡る様子を1行ずつ読みました。いよいよ受け取る側、バックエンドです。言語は Java、全15回の予定です。
第1回は全体図です。Java と Spring Boot で作った申請サーバーと、PostgreSQL の DB サーバーを練習用ネットワークで動かし、申請1件がサーバーの中をどう渡って、DB に入るかを実測します。

一

受け取る側へ

新井線の旅は終わりました。今日からはサーバーの中ですね!

古井そうだ。これまで送ってきた POST /api/expenses を、今度は自分で受け取る。

新井Spring Boot で、アノテーションを付ければ動くやつですよね。AI に書いてもらえば一瞬です。

古井一瞬で動く。そして一瞬で、何が起きているかわからなくなる。だから最初に全体図を描いて、裏で何が動いているかを1つずつ開けていく。

全15回の路線図(いまここ:第1回)

1 全体図 2 Java の準備 3 素の HTTP 4 素の JDBC 5 Spring Boot 6 入力チェック 7 例外と返事 8 DB アクセス 9 トランザクション 10 状態遷移 11 認証・認可 12 ファイル 13 ログと監視 14 テスト 15 動かす いまここ 基礎 素手で 受け口 データ 業務と守り 運用
  • 全体図 ── 申請1件が、サーバーの中で8人の手を渡って DB に入るまで(実測と Wireshark)
  • 素手で作ってみる ── 第3・4回はフレームワークなしで HTTP と DB を扱い、Spring が何を肩代わりしているかを確かめる
  • 業務システムの定番 ── 入力チェック、エラーの返し方、トランザクション、承認の流れ、認証、ファイル、ログ、テスト
  • 毎回、実際に測る ── 練習用のサーバーで動かし、ログ・SQL・パケット・時間の数字で説明する

題材は、これまでと同じ経費精算システムです。フロントエンド編の第10回で画面から送り、ネットワーク編の第10回で1行ずつ読んだあの申請を、今度は Java で受け取り、DB にしまいます。

二

サーバーは「待っているプログラム」

練習用ネットワークは3台です。PC(192.168.1.10)、申請サーバー(192.168.1.20、名前は expenses.example)、DB サーバー(192.168.1.30)。申請サーバーで、この回のために作った Java のプログラムを起動しました。

# 申請サーバー(192.168.1.20)で Java のプログラムを起動
app$ java -jar expenses-0.0.1-SNAPSHOT.jar
    … Tomcat initialized with port 8080 (http)
    … HikariPool-1 - Start completed.
    … Started ExpensesApplication in 3.183 seconds
# だれが、どこで待っているか
app$ ss -ltnp 'sport = :8080'
    LISTEN  0.0.0.0:8080   users:(("java",pid=760,fd=18))
# DB への接続(起動したときに、もう張ってある)
app$ ss -tn 'dport = :5432' | wc -l
    10
# このプログラムの大きさ
app$ ls -l expenses-0.0.1-SNAPSHOT.jar ; grep VmRSS /proc/760/status ; ls /proc/760/task | wc -l
    24,319,744 bytes
    VmRSS:  200876 kB
    38

「サーバー」は、特別な機械のことではありません。番号(ポート)を開けて、頼みごとを待っているプログラムのことです。ss で見ると、java が 0.0.0.0 の 8080 番で待っています(ネットワーク編第11回の「すべての口で待つ」)。そして起動した時点で、DB サーバーの 5432 番へ10 本の接続を張り終えています。理由は第7章で。

起動には 3.2 秒、メモリは約 200 MB、スレッドは 38 本でした。小さな申請サーバーでも、中ではそれなりの数の仕事が動きはじめています。

三

申請1件の旅

PC から、ネットワーク編と同じ申請を送りました。

# PC から、第10回と同じ申請を送る
pc$ curl -D - -X POST -H 'Content-Type: application/json' \
       -d '{"date":"2026-10-26","item":"交通費","amount":1280}' \
       http://expenses.example:8080/api/expenses
    HTTP/1.1 201
    Location: /api/expenses/EXP-2026-0012
    Content-Type: application/json
    
    {"id":"EXP-2026-0012","status":"submitted"}     (0.281 s)
# 同じ申請をあと5回
    201 (0.009 s)  201 (0.010 s)  201 (0.007 s)  201 (0.005 s)  201 (0.005 s)

201 Created と受付番号が返り、受付番号は Location にも入っています(ネットワーク編第10回)。そのとき申請サーバーの中で何が起きたかは、ログに出しておきました。

# 申請サーバーのログ(2件目。スレッドはどれも http-nio-8080-exec-2)
    15:47:44.473 ExpenseController      窓口:申請を受け取った ExpenseRequest[…, amount=1280]
    15:47:44.473 JdbcTransactionManager Creating new transaction with name [example.expenses.ExpenseService.submit]
    15:47:44.473 JdbcTransactionManager Acquired Connection […] for JDBC transaction
    15:47:44.474 JdbcTemplate           Executing prepared SQL statement [select nextval('expense_no')]
    15:47:44.474 ExpenseService         担当者:受付番号を決めた EXP-2026-0013
    15:47:44.474 JdbcTemplate           Executing prepared SQL statement [insert into expense (…) values (?, ?, ?, ?, 'submitted')]
    15:47:44.475 ExpenseRepository      倉庫番:1行書いた EXP-2026-0013
    15:47:44.475 JdbcTransactionManager Committing JDBC transaction on Connection […]
    15:47:44.476 JdbcTransactionManager Releasing JDBC Connection […] after transaction
    15:47:44.476 ExpenseController      窓口:201 を返す EXP-2026-0013

申請1件は、サーバーの中で8人の手を渡る

受け取る Tomcat JSON を読む Jackson 確かめる Validator 窓口 Controller 担当者 Service 倉庫番 Repository DB PostgreSQL 返事 201 Jackson・Tomcat 3.3 ms DB の前まで 2.5 ms DB と4往復 1.6 ms 返事を書く 温まってからの申請1件(Wireshark の時刻から、計 7.4 ms) 申請1件は、サーバーの中で8人の手を渡る。自分で書いたのは窓口・担当者・倉庫番だけ ほかはフレームワークとライブラリの仕事。何をしてくれているかを、この連載で1つずつ開ける

Tomcat が HTTP を受け取り、Jackson が JSON を Java の値に変え、Hibernate Validator が中身を確かめます。そこまで済んでから、ようやく自分で書いた「窓口」が呼ばれます。窓口は「担当者」に渡し、担当者はトランザクションを始めて受付番号を決め、「倉庫番」が SQL で表に1行書きます。コミットして接続を池に返し、窓口が 201 を返す。ログの時刻では、受け取ってから返すまで 3 ミリ秒でした。

Wireshark の時刻で分けると、申請が届いてから DB に話しかけるまで 3.3 ミリ秒、DB との4往復が 2.5 ミリ秒、返事を書いて送るまで 1.6 ミリ秒です。

四

窓口・担当者・倉庫番

自分で書いた部分は、3つの役割に分けてあります。実際のコードです。

// 窓口(Controller):HTTP を受け、担当者に渡し、201 を書く
@PostMapping
public ResponseEntity<Map<String, String>> submit(@Valid @RequestBody ExpenseRequest req) {
    log.info("窓口:申請を受け取った {}", req);
    String id = service.submit(req);
    log.info("窓口:201 を返す {}", id);
    return ResponseEntity.created(URI.create("/api/expenses/" + id))
            .body(Map.of("id", id, "status", "submitted"));
}

// 担当者(Service):業務のきまり。ここから先は1つのトランザクション
@Transactional
public String submit(ExpenseRequest req) {
    String id = "EXP-2026-%04d".formatted(repo.nextNumber());
    log.info("担当者:受付番号を決めた {}", id);
    repo.insert(id, req);
    return id;
}

// 倉庫番(Repository):SQL を書いて DB と話す
void insert(String id, ExpenseRequest r) {
    jdbc.sql("insert into expense (id, applied_on, item, amount, status) values (?, ?, ?, ?, 'submitted')")
        .params(id, r.date(), r.item(), r.amount())
        .update();
    log.info("倉庫番:1行書いた {}", id);
}

役割ごとにクラスを分ける

窓口 Controller HTTP を受ける 返事を書く 31 行 担当者 Service 業務のきまり トランザクション 26 行 倉庫番 Repository SQL を書く DB と話す 28 行 DB PostgreSQL HTTP → 役割ごとにクラスを分ける。窓口は SQL を知らず、倉庫番は HTTP を知らない 行数は、この回の実際のファイルの、空行を除いた数

窓口(Controller)は HTTP のことだけ、担当者(Service)は業務のきまりだけ、倉庫番(Repository)は SQL のことだけを知っています。窓口に SQL を書けば動くことは動きますが、承認の流れや権限のチェックが増えるにつれ、1つのクラスが手に負えなくなります。役割で分けておけば、「DB を変える」なら倉庫番だけ、「金額の上限を変える」なら担当者だけを直せば済みます。

新井ネットワークの「層」と同じ考え方ですね。上の層は下の層の中身を知らなくていい。

古井そうだ。いにしえの職人は、帳場と台所と蔵を分けた。

五

DB は別のプログラム

DB(データベース)は、申請サーバーの中の部品ではなく、別のサーバーで動く別のプログラムです。申請サーバーとは、TCP の 5432 番でつながっています。表紙の写真は、申請サーバーの口で記録したものです。PC からの申請(4番)と返事(19番)の間に、DB サーバーとのやりとり(6〜16番)がはさまっています。

やりとりは4往復でした。最初の「空の問い合わせ」は、接続の池から借りた接続がまだ生きているかの確認です。次に BEGIN(トランザクションの始まり)と受付番号の取り出し、そして INSERT、最後に COMMIT(確定)。トランザクションは第9回でくわしく扱います。

Wireshark の画面。同じ13番の明細で、2つ目の PostgreSQL(Type: Bind)を開いたところ。Parameter formats: 4、Parameter values: 4。右のバイトの欄に EXP-2026-0012 と 2026-10-26 の文字が見える
同じ13番の後半。SQL の文(Parse)とは別に、$1〜$4 に入る値(受付番号、日付、品目、金額)を Bind で送っています。文と値を混ぜないことが、第4回の SQL インジェクション対策の要になります(筆者がクラウド上に作った練習用ネットワークで、2026年10月26日に記録したものを Wireshark 4.2.2 で開いて撮影。画面は英語のまま。ウインドウの上の帯と見出しは説明のために描き足したもの)

注目してほしいのは、SQL の文と値が別々に送られていることです。文の中は $1, $2, $3, $4 という空欄のままで、そこに入る受付番号・日付・品目・金額は Bind という別の箱で届きます。こうしておくと、品目に悪意のある文字列が入っていても、SQL の文として読まれることはありません。第4回で、わざと文と値を混ぜて、どう壊れるかを見ます。

# DB サーバーで、表の中を見る
db$ psql -d expenses -c 'select id, applied_on, item, amount, status from expense order by id limit 3'
          id       | applied_on |  item  | amount  |  status
    ---------------+------------+--------+---------+-----------
     EXP-2026-0012 | 2026-10-26 | 交通費 | 1280.00 | submitted
     EXP-2026-0013 | 2026-10-26 | 交通費 | 1280.00 | submitted
     EXP-2026-0014 | 2026-10-26 | 交通費 | 1280.00 | submitted

DB サーバーの側で表を見ると、申請はたしかに1行ずつ入っていました。金額は 1280.00。画面から送った 1280 が、DB では小数2桁の数になっています。お金を Java でどう持つかは、次回の大事な話です。

六

最初の1件は遅い

第3章の申請は、curl の時計で 0.281 秒かかりました。ところが、そのあとの5件は 0.005〜0.010 秒です。

Wireshark のパケット一覧を2つ縦に並べたもの。どちらも中身のあるパケットだけを表示。上は1回目で、POST が 0.000083 秒、最初の PGSQL が 0.219919 秒、201 が 0.280707 秒。下は温まってからの57件目で、POST が 0.000116 秒、最初の PGSQL が 0.003453 秒、201 が 0.008110 秒
同じ申請でも、起動して最初の1件は 0.28 秒、温まってからは 0.008 秒。差のほとんどは、DB に話しかける前(JSON を読む道具や入力チェックの準備)にあります(筆者がクラウド上に作った練習用ネットワークで、2026年10月26日に記録したものを Wireshark 4.2.2 で開いて撮影。画面は英語のまま。ウインドウの上の帯と見出しは説明のために描き足したもの)

差のほとんどは、DB に話しかける前にあります。1件目は、届いてから最初の DB への問い合わせまで 0.22 秒、温まってからは 0.003 秒。ログを見ると、1件目は窓口に届く前に約 0.15 秒かかっていました。JSON を読む道具や入力チェックの道具が、最初の1回で準備を整え、Java のクラスが読みこまれ、よく通る道筋は JVM がだんだん速い機械語に直していきます。

POINT

Java のサーバーは「起動直後がいちばん遅い」。性能を測るときは、最初の数件を捨ててから測ります。本番で入れかえた直後に応答が遅くなるのも、同じ理由です(第15回)。

七

同時にたくさん

月末には、経理の締め切りに向けて申請が集中します。PC から 50 件を同時に送ってみました。

# PC から 50 件を同時に送る
pc$ python3 par.py
    status {201: 50}
    total 0.157 s  slowest 0.135 s  fastest 0.030 s
# 申請サーバーのログで、受け取ったスレッドの名前を数える
app$ grep 窓口:申請を受け取った app.log | (スレッド名) | sort -u | wc -l
    36
# Java のプログラムのスレッドの数(起動直後は 38)
app$ ls /proc/760/task | wc -l
    66

1件に1本のスレッド、DB への接続は池から借りる

同時に 50 件 Tomcat のスレッド 受け持ったのは 36 本 接続の池 HikariCP 10 本 DB 1件に1本のスレッド。DB への接続は、あらかじめ張った 10 本を使い回す 50 件は 0.157 秒ですべて 201。接続を毎回張り直さないのは、ネットワーク編第6回の話 スレッドの上限(既定 200 本)と接続の数(既定 10 本)は、第15回で調べる

50 件は 0.157 秒ですべて 201 になりました。ログを見ると、受け取ったスレッドは 36 種類。Tomcat は、届いた頼みごとをスレッド(プログラムの中の、同時に動ける作業員)に1件ずつ割り当てます。一方、DB への接続は起動時に張った 10 本を使い回します(接続の池、コネクションプール)。毎回 TCP のあいさつからやり直さないための工夫で、ネットワーク編第6回の「接続の使い回し」そのものです。

36 人の作業員が 10 本の電話を順番に使う形なので、電話の空き待ちが起きます。スレッドの数、接続の数、仮想スレッド(Java 21 から)の話は、第15回で測ります。

八

道具立て

この申請サーバーは、Spring Initializr(start.spring.io)で作った雛形に、Java のファイル5つと表の定義(schema.sql)を書き足し、設定を少し書きかえたものです。ビルドすると、1つの jar ファイルになります。

jar の中身。この回の Java 112 行と、48 個のライブラリ

expenses-0.0.1-SNAPSHOT.jar(24.3 MB、ライブラリ 48 個) Spring Boot 4.1.2 組み立て Tomcat 11.0 HTTP を受ける Jackson 3.1 JSON Hibernate Validator 9.1 入力チェック HikariCP 7.0 接続の池 PostgreSQL JDBC 42.7 DB と話す この回の Java は 6 ファイル、112 行(雛形の1つを含む) JVM(OpenJDK 25) この 112 行を、ほかのたくさんの職人の仕事が支えている
道具役目この回で使った版
JDK(OpenJDK)Java をコンパイルし、JVM で動かす25.0.4
Mavenライブラリを集めて、ビルドする雛形の mvnw
Spring Bootライブラリを組み立て、設定の手間を省く4.1.2
TomcatHTTP を受ける(Spring Boot に同梱)11.0
PostgreSQLDB サーバー16

Java は雛形の1つを含めて 112 行。HTTP の受け取り、JSON の読み書き、入力チェック、接続の池は、すべてライブラリの仕事でした。それ自体は良いことです。ただ、ライブラリが何をしているか知らないと、動かなくなったときに手が出ません。だからこの連載は、第3回と第4回であえて素手で HTTP と DB を扱い、第5回で Spring Boot に戻ってきます。

九

バックエンドの定番の仕事

今日の申請サーバーは、まだ「受け取って、しまう」だけです。業務システムのバックエンドには、ほかにも定番の仕事があります。この連載で扱う順に並べました。

仕事気をつけること回
値を正しく持つ金額を小数で計算しない、日付と時刻の「どこの時刻か」第2回
入力チェック画面のチェックは親切、守るのはサーバー第6回
エラーの返し方ステータスコードと、読めるエラーの本文第7回
DB の読み書きSQL インジェクション、件数が増えると遅くなる書き方(N+1)第4・8回
トランザクション途中で失敗したら、どこまで戻るか。同時に更新されたら第9回
承認の流れ申請 → 承認 → 差戻しの状態をどこで守るか第10回
認証と権限他人の申請が見えてしまう穴、パスワードの保存第11回
ファイル領収書のアップロード、明細の CSV・Excel 出力第12回
ログと監視あとから追える記録、個人情報を出さない第13回
テスト直したら、自動で確かめる第14回
動かし続ける配置、性能、ライブラリの更新第15回

DB との読み書きには、JPA と MyBatis という2つの有名なやり方があります。第8回で両方を同じ申請で試し、違いを比べます。それまでは、SQL を自分で書く素直なやり方(Spring の JdbcClient)で進めます。

十

自分の PC で準備する

次回から手を動かせるように、道具をそろえておきましょう。

用意するもの入れ方の例確かめ方
JDK 25Eclipse Temurin などの配布をインストールjava -version
エディターIntelliJ IDEA、または VS Code と Java の拡張機能新しいプロジェクトを開ける
PostgreSQLDocker で動かすのが手軽docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=practice postgres:16
雛形start.spring.io で Spring Web、JDBC API、PostgreSQL Driver、Validation を選ぶ./mvnw spring-boot:run
curlMac・Windows・Linux とも最初から入っているcurl --version

Maven は、雛形に入っている mvnw(Windows は mvnw.cmd)が自動で用意してくれるので、別に入れなくてもかまいません。

結 — 一枚まとめと、次回

  1. サーバーは、番号を開けて待っているプログラム。申請サーバーは 8080 番、DB サーバーは 5432 番。
  2. 申請1件は、Tomcat → Jackson → Validator → 窓口 → 担当者 → 倉庫番 → DB → 返事と、8人の手を渡る。温まってからは 7.4 ミリ秒。
  3. 自分で書くのは窓口・担当者・倉庫番。役割ごとにクラスを分ける。
  4. DB は別のプログラム。SQL の文と値は別々に送る。BEGIN と COMMIT ではさむ。
  5. Java のサーバーは最初の1件が遅い(0.28 秒 → 0.008 秒)。
  6. 同時に来たら、1件に1本のスレッド。DB への接続は池から借りて使い回す。
  7. この回の Java 112 行を、48 個のライブラリが支えている。中身を知るために、第3・4回は素手で作る。

新井1件の申請に、こんなにたくさんの人が関わっていたなんて。

古井では宿題だ。申請日は 26 日で送ったのに、返事の Date ヘッダーは 25 日、ログの時刻は 15 時台だった。DB の記録は 0 時台。どれが正しい?

新井……全部、正しい?

古井次回は Java の準備。お金と日付と時刻を、正しく持つ方法から始める。

参考

記録は、筆者がクラウド上の Linux(Ubuntu 24.04、カーネル 6.18)にネットワーク名前空間で作った練習用ネットワークで、2026年10月26日に取ったものです。申請サーバーは OpenJDK 25.0.4 と Spring Boot 4.1.2(Spring Initializr の雛形に、この回のためのコードを足したもの。DB とのやりとりは Spring の JdbcClient)、DB サーバーは PostgreSQL 16、PC 側は curl 8.5.0 と Python 3 です。名前と住所はすべて練習用に作ったものです。ログの時刻は申請サーバーの設定のまま(UTC)です。表の数字や出力はすべて、この記録とそのときのコマンドの出力から読んだ実際の値です(長い出力は一部を省いています)。登場人物は架空の人物です。

いにしえのバックエンド入門 第1回 — 全体図 / 2026.10

← 記事一覧へ