「使う」と「分かる」の間にある一枚

 ・ 10

photo by Jeremy Bishop(https://unsplash.com/@jeremybishop?utm_source=templater_proxy&utm_medium=referral) on Unsplash

「Springを使ったことがあります」「Reactを使ったことがあります」「Kubernetes上で運用したことがあります」——履歴書なら一行で済む言葉ですよね。でもその一行が指す深さは、人によって本当に大きく違います。

道具を使うことは誰にでもできます。難しいのは、抽象化の一枚下を覗き込むこと——道具が自分の仕事を止めてしまうその場所まで、入っていけるかどうかです。

「使ったことがある」と「分かっている」の間には一枚の箱があります。その箱を開けられるかどうかが、差を生みます。

この記事は、毎日使っている道具たちの箱を、一枚ずつ開けてみる試みです。「基本的な使い方」の一段下を、5つの領域で見ていきます。模範解答ではなく、自分が使っているスタックをもう一度確かめるチェックリストとして読んでみてくださいね。

Springを使うことと、Springを分かること#

Springを「ただ使う」というのは、@Service@Repository@Autowiredのようなアノテーションを覚えてコンパイルが通るコードを書くことです。分かり始めるのは、そのアノテーションが消えたら何が起きるのかを説明できる時からです。

要点はDI(Dependency Injection) という一語に集まります。オブジェクトが自分の依存性を直接作らずに外部から受け取るようにする——これが結合度を下げ、テストを可能にします。

注入方式は3つありますが、性格が違います。

方式 特徴 推奨度
フィールド注入 短い。しかしコンテナなしでは作れない 非推奨
セッター注入 選択的依存を表現できる 特定のケース
コンストラクタ注入 不変、循環参照を即発見、テストしやすい 基本

Springがコンストラクタ注入を推奨する理由は単純ではありません。

  1. 循環参照を起動時点で検出できます — フィールド/セッター注入はオブジェクトを先に作って後で埋めるので、循環参照は実行時にしか分かりません
  2. 不変性finalで宣言可能、マルチスレッドで安全
  3. テスト容易性new OrderService(mockRepo) でコンテナなしに単体テストができます

3つ目がTDDで決定的です。テストを先に書くにはテスト可能な設計が前提だからです。コンストラクタのシグネチャに依存性が全て現れていれば、どこに偽オブジェクトを差し込むかが明確で、依存性が増えすぎれば自然とクラスを分割するようになります。SRPが自動的についてきます。

@Service
@RequiredArgsConstructor // Lombokがfinalフィールドからコンストラクタを生成
public class OrderService {
    private final PaymentClient paymentClient;
    private final OrderRepository orderRepository;
}

Javaを使うことと、Javaを分かること#

ラムダ一行、stream().map() 一行は誰でも書けます。その中で何が起きているかが、箱の内側です。

匿名クラスとラムダ ― 似ているようで違う#

Runnable a = new Runnable() { public void run() { ... } }; // 匿名クラス
Runnable b = () -> { ... };                                // ラムダ

見た目は似ていますが、内部は違います。

  • 匿名クラスはコンパイラが別の .class ファイルを作ります(Outer$1.class)。キャプチャされた外部変数は匿名クラスのフィールドに入ります。だから effectively final 制約があるのです
  • ラムダ.class を作りません。invokedynamic 命令を使って、ランタイムに LambdaMetafactory がインスタンスを作ります。メモリ負担が少なく、JITに優しいのです

「ラムダは匿名クラスのシンタックスシュガーじゃないの?」というよくある誤解を解いてくれる地点ですね。

Stream ― 遅延評価のパイプライン#

Streamの本質は3つに集約されます。

  • 遅延評価filtermap は即座には実行されず、toList のような終端操作に出会って初めて流れが始まります
  • 不変性 — 元のコレクションは変更されません
  • 合成可能性 — 小さな演算を組み合わせて大きな変換を作れます

Java 21では Stream Gatherers(JEP 461)で独自の中間操作まで作れるようになりました。スライディングウィンドウやバッチ処理のようなものが標準のstreamに入りました。

Virtual Threads ― Javaの新しい並行性#

Java 21 LTSで一番大きな変化は Virtual Threads(JEP 444)です。OSスレッドをほぼ使わずに、数万のブロッキング処理を同時に走らせることができます。

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    urls.forEach(url -> executor.submit(() -> fetch(url)));
}

非同期コードを同期スタイルで書けるようになった、というのが要点です。CompletableFutureでコールバックを組み合わせる時代が終わりに向かっている合図でもあります。

Exception ― CheckedとUnchecked#

  • Checked(Exception を継承、RuntimeException を除く) — コンパイラが処理を強制
  • Unchecked(RuntimeException 系) — 強制なし

実務ではほぼ全てUnchecked + ドメイン例外階層で進みます。Checkedはラムダ/ストリームと相性が悪く、呼び出し側に負担を強いるからです。Spring自体の例外階層(DataAccessException など)も全てUncheckedで設計されています。

public abstract class BusinessException extends RuntimeException { }
public class OrderNotFoundException extends BusinessException { }

@RestControllerAdvice で一括変換して、一貫した応答を返すのが標準です。


Reactを使うことと、Reactを分かること#

Reactは「JSXを書けば画面が出る」が入口です。その向こうに、知っておくべき層がいくつかあります。

JSXと仮想DOMが本当にやっていること#

JSXは結局 React.createElement(...) の呼び出しにコンパイルされます。その呼び出しが作り出すオブジェクトのツリーが仮想DOMで、Reactは前回のツリーと比較(diffing)して、実際のDOMには最小限の変更だけを反映します

これが分かると、「なぜkeyが重要なのか」「なぜ同じ位置のコンポーネントがunmountされないのか」といった質問が解けます。

Hooksの核心#

Hook 役割
useState コンポーネントローカルな状態
useEffect 外部システムとの同期(購読、fetchなど)
useMemo 高コストな計算結果のキャッシュ
useCallback 関数参照の安定化
useRef レンダリングと無関係な可変値
useContext 直近のProviderの値を購読

React 19では useActionStateuseOptimisticuse のようなフックが追加され、コンパイラ(React Compiler) がメモ化を自動化してくれて、useMemo/useCallback を手で書く場面が減りました。

コンポーネント合成 ― データフローの骨格#

Reactのデータフローは一方向です。親 → 子はprops、子 → 親はコールバックprops

<Child title={title} onSave={(data) => handleSave(data)} />

問題は祖父コンポーネントに状態を届けたい時です。propsを一段ずつ遡って渡すprop drillingは、深くなるほど辛くなります。

選択肢は3つです。

  1. Context — ツリーのどの深さからでも同じ値にアクセス。頻繁に変わる状態には不向き(再レンダーのコスト)
  2. 状態管理ライブラリ — Zustand、Jotai、Redux Toolkit。グローバルなstoreを置いてどこからでも購読
  3. 状態のリフトアップ(lifting state up) — 共通の親に移動。最もReactらしいやり方ですが、親が肥大化すると限界あり

スタイリングと隔離#

CSSのグローバル性はコンポーネントモデルと衝突します。だから CSS Modules(Button.module.css)、Tailwind(クラス合成)、CSS-in-JS のような道具が隔離を担います。

最近はTailwind + shadcn/uiの組み合わせが事実上の標準で、Next.jsのApp Routerではサーバーコンポーネントとの互換性の問題でランタイム CSS-in-JSは立場を失いつつあります。


キャッシュと分散を使うことと、分かること#

データを扱う領域では、深さが一番早く露わになります。

Cache Stampede ― 期限切れ直後の罠#

@Cacheable を付けた」が使うことなら、キャッシュが期限切れになった瞬間に同時に入ってきたリクエストが全てDBに殺到する現象を説明できるのが分かることです。これをCache Stampede(またはthundering herd)と呼びます。

対応策

  • @Cacheable(sync = true) — 同じキーへの同時呼び出しのうち1つだけが元に降りる
  • 分散ロック — Redissonの RLock で再生成権限を1つのリクエストに与える
  • Stale-While-Revalidate — 期限切れの値をひとまず返し、バックグラウンドで更新
  • TTLジッター — 同じキー群が同時に切れないように無作為なオフセットを足す

Redis Persistenceとその先#

Redisが落ちると、消えるのはキャッシュだけではありません。セッション、分散ロック、キューも一緒に揺れます。

  • RDB — 特定時点のスナップショット。復旧が速い、一部データ消失の可能性
  • AOF — 全ての書き込みコマンドのログ。消失は少ない、ファイルが大きい

実務では両方を有効化するハイブリッドモード(aof-use-rdb-preamble yes)が標準です。そこに Sentinel(高可用性)または Cluster(シャーディング)を載せ、アプリケーション側では Circuit Breaker(Resilience4j)でフォールバックの経路を開けておきます。

2024年のRedisライセンス変更以降、OSS陣営は Valkey(Linux Foundationフォーク)へ急速に移行し、AWS ElastiCache/MemoryDBもValkeyを既定のオプションとして提供しています。

MSA ― 何を得て、何を失うか#

サービスを細かく分割して得るもの

  • 独立デプロイ、技術スタックの自由、障害の隔離、チームの自律性

失うもの

  • ネットワーク遅延/トラフィック、分散トランザクション、結合テストの複雑さ、運用負担

分散トランザクションはSagaやOutboxパターンで解きます。結合テストはConsumer-Driven Contract Testing(Pact)やTestcontainersで対応します。

大規模ストレージ ― キー設計が全て#

HBaseのようなワイドカラムストアで最も重要なのは Row Key設計 です。キーは辞書順に格納されてRegionが分割されるのですが、タイムスタンプを先頭に置くと最新データが1つのRegionに集中します(ホットスポット)。

解決策

  • ソルト(salt) — キーの先頭にハッシュの一部を足す
  • リバースタイムスタンプLong.MAX_VALUE - ts
  • 複合キーuserId#reverseTimestamp

DynamoDB、BigTableも本質は同じです。パーティションキーとソートキーの設計こそがシステム性能 です。

分散コーディネーション ― ZooKeeperのマスター選出#

複数ノードのうち1つがマスターになる必要がある時、ZooKeeperは一時順序ノード(ephemeral sequential) で解決します。

  1. 各ノードがZKに順序ノードを作成
  2. 一番小さい番号がマスター
  3. マスターのセッションが切れると、一時ノードが自動削除
  4. 前のノードをWatchしていた次の候補が通知を受ける

セッションの維持は定期的なpingが担い、決められたタイムアウト内にpingが来なければZKがセッションを失効させます。

KafkaはKRaftモードでZK依存を断ち、新規プロジェクトなら etcd(Kubernetesも使う)の方が一般的な選択です。パターンは同じです。


コンテナの上で動かすことと、運用すること#

docker run 一行、kubectl apply 一回は、ほんの始まりに過ぎません。

リソース管理 ― requestsとlimits#

resources:
  requests:
    memory: '256Mi'
    cpu: '250m'
  limits:
    memory: '512Mi'
    cpu: '500m'
  • requests — スケジューリングの基準。ノードがこれだけは保証
  • limits — 使用上限。CPUは throttling、メモリは OOMKilled

JVMコンテナではメモリ認識が重要です。Java 10+ からはcgroupを認識してヒープを決めますが、-XX:MaxRAMPercentage=75.0 のように明示的に指定しておくのが安全です。limits全部をヒープに使うと、メタスペースやスレッドスタックが足りなくてOOMになります。

自動調整は HPA(水平)、VPA(垂直)が標準です。

リリースからデプロイまで ― 段階で見る#

開発者がコードを書いて運用まで届ける、典型的な流れです。

  1. コミット & プッシュ — Git
  2. CI — ビルド、テスト、静的解析(GitHub Actions、GitLab CI)
  3. イメージビルド — Dockerfile、マルチステージで容量を最小化
  4. レジストリプッシュ — Docker Hub、ECR、GHCR。意味のあるタグ(v1.2.3)
  5. CD — ArgoCD/FluxのようなGitOpsツールがマニフェスト変更を検知、またはHelm/Kustomize
  6. ローリングアップデート — Deploymentのイメージが更新されると、新しいReplicaSetが立ち上がり順次差し替え
  7. ヘルスチェックとトラフィック切替 — readiness probeが通れば、Serviceが新しいPodへトラフィックを流す
  8. 観測とロールバック — Prometheus/Grafana + OpenTelemetry。問題があれば kubectl rollout undo

デプロイ戦略はRollingが既定で、無停止が大事ならBlue-Green、段階的検証が必要ならCanaryに行きます。

障害対応 ― 何から見るか#

順番が大事です。

  1. 影響範囲 — どのユーザー/サービスが影響を受けているか
  2. 直近の変更 — 直前のデプロイ、設定/シークレットの変更、依存性の更新
  3. メトリクス — CPU、メモリ、エラー率、遅延(Grafana)
  4. ログERRORExceptionCaused by、5xx、timeoutconnection refused
  5. 分散トレーシング — OpenTelemetryでサービス間の流れを追う
  6. ロールバック判断 — 原因究明より影響遮断を優先すべき場面が多い

「原因が分からないから戻せない」より「ひとまず戻して分析しよう」が運用の基本です。


もう一枚、箱を開ける習慣#

5つの領域を見てきましたが、結局同じ話に戻ってきます。

フレームワークの魔法が止まる場所で、本当の理解が始まります。

@Autowired がなかったら、オブジェクトはどうやって作られるのでしょう。useState がなかったら、画面はどうやって更新されるのでしょう。@Cacheable がなかったら、キャッシュミスの殺到はどう防ぐのでしょう。kubectl apply 一行の裏で、スケジューラは何を見てPodをどこへ送るのでしょう。

それぞれを一段下まで覗き込めば、道具を見る目が変わります。同じコードを書いていても、なぜそれを選んだのか、壊れたら何が起きるのかを説明できるようになります。

今日、自分のコードからアノテーション一つ、フック一つ、マニフェストの一行を選んで、「これを取り除いても、同じ動きを自分で実装できるだろうか?」 と自問してみてください。答えが詰まるその地点が、次に開けるべき箱です。


As we risk ourselves, we grow. Each new experience is a risk.

— Fran Watson


他の投稿
バックエンドシステムを最後まで描けますか 커버 이미지
 ・ 7

バックエンドシステムを最後まで描けますか

ARグラスは次のスマートフォンになるのか 커버 이미지
 ・ 18

ARグラスは次のスマートフォンになるのか

バックエンド面接でよく聞かれる質問38選まとめ 커버 이미지
 ・ 14

バックエンド面接でよく聞かれる質問38選まとめ