いろいろな会社のバックエンド開発者面接で出そうな質問を、テーマ別に整理してみました。会社ごとに少しずつ色は違いますが、Java / Spring / DB / 運用 という大きな幹はどこでも共通しています。応募する前に一度チェックしておくと役に立つ項目を中心にまとめました。
データ構造 & アルゴリズム#
基礎の確認として、ほぼ毎回出てくる領域です。特にハッシュ衝突、探索アルゴリズム、ページ置換は本当に定番ですね。
ハッシュ衝突の解決方式#
ハッシュ関数がどれだけ優れていても、鳩の巣原理から衝突は必ず発生します。だからこそ、衝突をどう扱うかでデータ構造の性能が決まります。
Open Addressing(オープンアドレス法)
テーブルの空きスロットを使って衝突を解決します。追加メモリが要らないのでメモリ効率がよく、連続したメモリにデータを配置するのでキャッシュにも優しいです。
- Linear Probing: 衝突したら隣を順番に探します。実装は最もシンプルですが、Primary Clustering というクラスタリング問題があります。データが一カ所に集まると探索時間が急激に伸びるんですよね。
- Quadratic Probing: n² 個離れたスロットを探します。Linear Probing のクラスタリングは緩和されますが、ハッシュ値が同じだと探索経路が同じになる Secondary Clustering が残ります。
- Double Hashing: 1次ハッシュで位置を決め、2次ハッシュで移動幅を決めます。最も不規則な分布を作れて衝突回避性能は高いですが、計算コストが2倍になります。
一般に Open Addressing は Load Factor が 0.7 を超えると性能が急落するので、適切なタイミングでリハッシュが必要になります。
Separate Chaining(分離連鎖法)
同じバケットにリンクリストやツリーでデータを連結する方式です。Java の HashMap が代表例ですが、面白いディテールがあります。
- Java 8 から、バケット内のノード数が 8 を超えるとリンクリストを赤黒木に変換します(Treeify)
- ツリーのノード数が 6 以下に減ると、再びリンクリストに戻ります(Untreeify)
- これは Worst Case の O(n) を O(log n) に抑えるためです
面接の答え方のコツ: 「どちらが好み?」という追加質問が来ることがあります。正解はなく、データ分布とメモリ制約によって変わると答えるのがいいですね。例えば、キャッシュ効率が大事な小さいデータセットなら Open Addressing、衝突が頻繁で動的にサイズが変わるなら Separate Chaining が有利です。
DFS と BFS#
探索アルゴリズムの二大巨頭ですね。単に「何か」より、いつどちらを使うかを知っているかが大事です。
| 項目 | DFS | BFS |
|---|---|---|
| データ構造 | スタック(再帰) | キュー(FIFO) |
| メモリ | 深さに比例 | 幅に比例 |
| 最短経路 | 保証されない | 保証される(重みなしグラフ) |
| 用途 | バックトラッキング、トポロジカルソート、サイクル検出 | 最短距離、レベル探索 |
いつどちらを使うか?
- すべての経路を探索する必要がある → DFS
- 最短経路(辺の数基準)が必要 → BFS
- グラフの深さがとても深い場合、DFS はスタックオーバーフローの危険があります
- グラフがとても広い場合、BFS はメモリが膨らむ危険があります
重み付きグラフの最短経路には Dijkstra や Bellman-Ford を使います。「BFS だけでは足りない」と一緒に言えると深みが出ますよ。
ページ置換アルゴリズム#
OS 領域では LRU がほぼ必須で出てきます。キャッシュポリシーにも同じ概念が当てはまるので、覚えておいて損はないです。
- FIFO: 最も古いページから置き換えます。実装はシンプルですが、Belady's Anomaly(ページフレームを増やしたのにページフォルトが増える現象)が起きることがあります。
- Optimal (OPT): これから最も長く使われないページを置き換えます。理論上最適ですが、未来を知る必要があるため実装不能。他のアルゴリズムの性能比較の基準として使われます。
- LRU (Least Recently Used): 最も長く使われていないページを置き換えます。時間的局所性(Temporal Locality)を活かして Optimal に近い性能を出します。実装は通常 HashMap + 双方向リンクリストで O(1) を保証します。
- LFU (Least Frequently Used): 参照回数が最も少ないページを置き換えます。一度よく参照されたページが居座り続けて新しいデータが入りにくい(キャッシュ汚染)という弱点があります。
- MFU: LFU の逆。参照回数が多いものは「もう十分使った」と考えて置き換えます。
LRU キャッシュを自分で実装する問題は LeetCode にもあって、面接でもときどき出ます。
LinkedHashMapを使う方法と、双方向リンクリスト + HashMap で自作する方法、両方知っておくといいですよ。
Java & JVM#
Java バックエンド面接の核となる領域です。GC、スレッド、コレクションの 3 つは絶対に深く準備しておきましょう。
JVM のメモリ構造#
GC を語る前に、メモリ構造を知っておく必要があります。
- Method Area (Metaspace): クラス情報、static 変数、定数プール
- Heap: すべてのオブジェクトと配列が格納される領域。GC の対象。
- Stack: メソッド呼び出しごとに作られるフレーム。ローカル変数、引数を格納。
- PC Register: 現在実行中の命令アドレス
- Native Method Stack: JNI 経由のネイティブメソッド呼び出し用
Java 8 から PermGen が消えて Metaspace に置き換わりました。Metaspace はネイティブメモリを使うので、OOM の発生パターンも変わりました。
GC (Garbage Collection)#
ヒープは大きく Young Generation と Old Generation に分かれます。これは「ほとんどのオブジェクトは短命で死ぬ」という Weak Generational Hypothesis に基づいた設計です。
Young Generation の構造:
- Eden: 新しいオブジェクトが最初に割り当てられる場所
- Survivor 0, 1: Eden で生き残ったオブジェクトが移動する場所。2 つの領域を交互に使います。
GC の種類:
- Minor GC: Young Generation で発生。速くて頻繁。
- Major GC (Full GC): Old Generation を含む全体の GC。遅く、Stop-the-World が長くなります。
GC アルゴリズム:
- Serial Collector: 単一スレッド。100MB 未満の小さいアプリ向け。
-XX:+UseSerialGC - Parallel Collector: マルチスレッド、スループット重視。JDK 8 のデフォルト。
-XX:+UseParallelGC - CMS (Concurrent Mark Sweep): 並行マーキングで STW を最小化。JDK 9 で Deprecated、JDK 14 で削除。
- G1 (Garbage First): ヒープを Region 単位で管理。JDK 9 からデフォルト。大きなヒープ(4GB 以上)に向いています。
- ZGC: STW を 10ms 以下に抑えます。JDK 11+ で実験的、JDK 15+ で正式。TB 単位の大容量ヒープ向け。
- Shenandoah: ZGC に似た低遅延 GC。Red Hat が主導。
面接の答え方のコツ: 「どの GC を使ったか」という質問には、単に名前だけでなく なぜそれを選んだのか を一緒に答えるといいですね。例:「応答時間が重要なサービスだったので G1 を使いました。CMS と比べて Full GC の発生頻度を減らせたんです。」
OOM とヒープダンプ#
OOM は種類によって原因と対処法が違います。
| OOM の種類 | 原因 |
|---|---|
Java heap space |
ヒープ不足。メモリリークまたはヒープサイズ不足 |
GC Overhead limit exceeded |
GC に時間を使いすぎているのに回収量が少ない |
Metaspace |
クラスメタデータ領域が不足 |
Direct buffer memory |
NIO Direct Buffer のリーク |
Unable to create new native thread |
OS のスレッド生成上限に到達 |
ヒープダンプ解析の流れ:
- 自動ダンプ設定:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump - 手動生成:
jmap -dump:format=b,file=heap.hprof <pid> - 解析ツール: Eclipse MAT (Memory Analyzer Tool)、VisualVM、JProfiler
- チェックポイント: Dominator Tree でメモリを最も占有するオブジェクトを探し、GC Roots を追跡
よくあるリークパターン:
staticコレクションにオブジェクトが溜まり続けるThreadLocalを使った後remove()を呼ばない- リスナーやコールバックを登録した後に解除しない
- DB Connection、Stream を閉じない
Thread Safe と同期#
スレッド安全性を保証する方法には段階的にいくつかあります。
1段階: 共有資源そのものをなくす(最も安全)
- 不変オブジェクト(Immutable Object)を使う
- ThreadLocal でスレッドごとのインスタンスに分離
- 関数型プログラミング(副作用を排除)
2段階: 同期メカニズム
synchronized: 最も基本。モニターロックを使う。進入時に他のスレッドは待機。volatile: 可視性(visibility)を保証。原子性は保証しません。ReentrantLock:synchronizedより柔軟。tryLock、fair モードに対応。ReadWriteLock: 読み取りロックと書き込みロックを分離。読み取りが多い場合に有利。StampedLock: Java 8+。楽観ロック対応で性能向上。
3段階: 原子操作(Lock-Free)
AtomicInteger、AtomicLong、AtomicReferenceなど- 内部的に CAS (Compare-And-Swap) を使用
- ロックがないのでデッドロックの心配なし、性能も速い
- 弱点: 競合が激しいとスピンが増えてかえって遅くなることがあります(ABA 問題にも注意)
4段階: 並行コレクション
ConcurrentHashMap: HashMap の並行版。Java 8 から CAS と synchronized の混合。CopyOnWriteArrayList: 書き込み時にコピー。読み取りが圧倒的に多い時に有利。BlockingQueue系: 生産者-消費者パターン
synchronized の問題点#
面接でよく追加される質問です。
- 性能低下: モニターロックの取得/解放コスト
- デッドロックのリスク: ロック取得順序が崩れると発生
- 競合時にスループット急減: ロック競合が激しいとほぼ直列処理
- 公平性の欠如: どのスレッドがロックを取るか保証なし
- 割り込み不可: ロック待機中に割り込みで起こせない(
ReentrantLockは可能)
関数型インターフェース#
Java 8 以降、ラムダと一緒によく聞かれます。唯一の抽象メソッド (SAM) を持つインターフェースのことです。
@FunctionalInterface
interface MyFunction {
int apply(int x);
}
// ラムダで実装
MyFunction square = x -> x * x;よく使う標準関数型インターフェース:
| インターフェース | シグネチャ | 用途 |
|---|---|---|
Supplier<T> |
() -> T |
値を供給 |
Consumer<T> |
T -> void |
値を消費 |
Function<T,R> |
T -> R |
変換 |
Predicate<T> |
T -> boolean |
条件判定 |
BiFunction<T,U,R> |
(T,U) -> R |
2 値変換 |
UnaryOperator<T> |
T -> T |
同型変換 |
コレクションフレームワーク#
Array vs ArrayList:
| 項目 | Array | ArrayList |
|---|---|---|
| サイズ | 固定 | 動的(内部配列拡張) |
| 型 | プリミティブ可 | オブジェクトのみ(ジェネリクス) |
| 性能 | 速い | わずかなオーバーヘッド |
| メモリ | 効率的 | メタデータ追加 |
HashMap の内部動作:
- デフォルト capacity: 16、load factor: 0.75
- 75% が埋まると 2 倍にリサイズ
- ハッシュ値を
(n - 1) & hashでインデックス計算(capacity が 2 のべき乗だから可能) - Java 8 から衝突が多いバケットはツリー化
HashSet の性能低下ケース:
equals()とhashCode()の実装ミスで衝突が多い場合- mutable オブジェクトをキーにして、ハッシュ値に影響するフィールドを変更した場合
- 解決策: 分散の良い hashCode を書く、可能ならイミュータブルなキーを使う
Stream と Parallel Stream#
// Stream
List<Integer> result = list.stream()
.filter(x -> x > 10)
.map(x -> x * 2)
.collect(Collectors.toList());
// Parallel Stream
list.parallelStream()
.filter(x -> x > 10)
.map(x -> x * 2)
.collect(Collectors.toList());Parallel Stream はいつ使うと良い?
向いている場合:
- データが十分大きい(通常 10,000 件以上)
- CPU バウンドな作業
- 作業単位が独立している
- 結果の順序が重要でない
避けるべき場合:
- 小さいデータセット(オーバーヘッドの方が大きい)
- I/O バウンドな作業(スレッド効率が悪い)
- 共有状態の変更(同期コスト)
- トランザクションコンテキスト内(スレッドが違うとトランザクション伝播しない)
Parallel Stream はデフォルトで 共通の ForkJoinPool を使います。一つの作業がプールを占有すると他の作業まで影響を受けるので、本番環境では別途プールを明示的に指定するのが安全です。
Spring#
Java バックエンドでは事実上必須のフレームワークなので、ディテールまで聞かれます。
DI と IoC#
- IoC (Inversion of Control): オブジェクトの生成とライフサイクルを開発者ではなくコンテナが管理。制御の流れが逆転する。
- DI (Dependency Injection): IoC を実装する一つの方法。依存性を外から注入。
注入方法:
// 1. Constructor Injection(推奨)
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
// 2. Setter Injection
@Service
public class UserService {
private UserRepository userRepository;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
// 3. Field Injection(避ける)
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
}なぜ Constructor Injection が推奨されるのか?#
面接の定番質問です。答えられる理由が多いほどいいですね。
- 循環参照を起動時に発見できる — フィールド/セッター注入は実行時のメソッド呼び出しで初めて発覚しますが、コンストラクタ注入はコンテナ初期化時に即失敗します。Spring Boot 2.6+ からはデフォルトで循環参照を禁止します。
- 不変オブジェクトの保証 —
finalキーワードで一度注入した依存性を変えられなくします。 - テストが書きやすい — コンテナなしで
new UserService(mockRepo)で直接注入可能。フィールド注入はリフレクションなしではテスト不可。 - 不足の発見 — コンパイル時に必須依存性の不足を検出できる。
- 単一責任原則を強制 — コンストラクタの引数が増えすぎると、クラスが多くの責任を持っているサインになります。
Bean Scope#
| Scope | ライフサイクル |
|---|---|
| Singleton(デフォルト) | コンテナごとに 1 個 |
| Prototype | リクエストごとに新しいインスタンス |
| Request | HTTP リクエスト単位(Web) |
| Session | HTTP セッション単位(Web) |
| Application | ServletContext 単位(Web) |
| WebSocket | WebSocket 単位(Web) |
Singleton ビーンに Prototype ビーンを注入すると、最初に注入されたインスタンスが使われ続けます。毎回新しいインスタンスが欲しい場合は
ObjectProviderや@Lookupアノテーションを使う必要があります。
AOP#
主要な関心事と付加的な関心事を分離してモジュール化するパラダイムです。ロギング、トランザクション、認証/認可のような横断的関心事に向いています。
主要用語:
- Aspect: 横断的関心事をモジュール化したもの
- Join Point: Aspect が適用されうるポイント(メソッド実行など)
- Pointcut: どこに適用するかを定義する式
- Advice: 何をするかを定義(
@Before、@After、@Around、@AfterReturning、@AfterThrowing) - Weaving: Aspect を実際のコードに適用する過程
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long elapsed = System.currentTimeMillis() - start;
log.info("{} took {}ms", joinPoint.getSignature(), elapsed);
return result;
}
}Spring AOP の限界:
- プロキシベースなので、同じクラス内の内部呼び出し(self-invocation)には適用されない
- メソッドレベルのみ可能(フィールドアクセスは不可)
- public メソッドのみ適用(CGLIB 使用時を除く)
トランザクション (@Transactional)#
面接でよく深掘りされるトピックです。
伝播属性 (Propagation):
REQUIRED(デフォルト): 既存のトランザクションがあれば参加、なければ新規作成REQUIRES_NEW: 常に新しいトランザクションを作成。既存は一時停止。NESTED: ネストされたトランザクション。SavePoint を使用。SUPPORTS: あれば参加、なければトランザクションなしで実行MANDATORY: 必ず既存のトランザクションが必要NEVER: トランザクションがあれば例外
分離レベル (Isolation Level):
READ_UNCOMMITTED: Dirty Read 発生可能READ_COMMITTED: Non-Repeatable Read 可能(Oracle のデフォルト)REPEATABLE_READ: Phantom Read 可能(MySQL のデフォルト)SERIALIZABLE: 最も厳格、性能低下
よく陥る罠:
@Transactionalは public メソッドにのみ 適用されます- 同じクラス内の内部呼び出し はプロキシを経由しないので適用されません
- デフォルトでは RuntimeException と Error のみ ロールバック。Checked Exception は明示的に
rollbackForを指定する必要があります。
データベース & JPA#
性能に直結する領域なのでディテールを聞かれます。
インデックス#
なぜ速いのか?
- B+ Tree をベースとしたデータ構造。すべての leaf ノードが同じ深さにあり、バランスが取れています。
- 100 万件のデータでも通常 3〜4 段階のツリー探索でアクセス可能
- Leaf ノード同士が連結されているので 範囲検索にも強い
B+ Tree と B-Tree の違い:
- B-Tree はすべてのノードにデータを格納
- B+ Tree は leaf にのみデータを格納し、内部ノードはインデックス役のみ
- B+ Tree は範囲検索に有利で、ディスク I/O 効率も良い
インデックスは常に良いわけではありません:
- 書き込みコスト増加(INSERT、UPDATE、DELETE のたびにインデックスも更新)
- ディスク容量を追加で使用
- カーディナリティが低いカラム(例: 性別)には非効率
- データが少ないとフルスキャンの方が速いこともある
複合インデックスの罠:
(A, B, C) のインデックスがあるとき:
WHERE A = ?→ 使用WHERE A = ? AND B = ?→ 使用WHERE A = ? AND C = ?→ 部分使用WHERE B = ?→ 使用されない(左側のカラムからマッチ)
カバリングインデックス (Covering Index):
クエリに必要なすべてのカラムがインデックスに含まれていれば、テーブルにアクセスせずインデックスだけで結果を返せます。とても速い検索が可能です。
N+1 問題#
JPA を使うとほぼ必ず遭遇する問題です。
問題の状況:
// Member を取得後、各 Member の Team を参照
List<Member> members = memberRepository.findAll(); // クエリ 1 回
for (Member m : members) {
System.out.println(m.getTeam().getName()); // クエリ N 回
}合計 N+1 回のクエリが発行されます。Member が 100 人なら 101 回の DB 呼び出しです。
解決方法:
JOIN FETCH:@Query("SELECT m FROM Member m JOIN FETCH m.team") List<Member> findAllWithTeam();@EntityGraph:@EntityGraph(attributePaths = {"team"}) List<Member> findAll();- Batch Size 設定:
N 個を IN 句でまとめて取得(N+1 が N/batch+1 に減る)spring.jpa.properties.hibernate.default_batch_fetch_size: 100 - QueryDSL: 動的クエリに柔軟
- Projection (DTO 直接取得): 必要なカラムだけ取得
JOIN FETCHはコレクションを取得するときにページングできません(全データをメモリに乗せてからページングするので危険)。ページングが必要な場合は Batch Size 方式を使うべきです。
キャッシュ戦略#
- いつ更新する? — データが変更された時に無効化または更新
- 整合性が壊れるケース — キャッシュと DB が不一致になるすべての時点
キャッシュパターン:
- Cache Aside (Look Aside): 最も一般的。キャッシュ確認 → なければ DB → キャッシュに保存
- Write Through: 書き込み時に DB とキャッシュを同時更新
- Write Behind (Write Back): 書き込みはキャッシュにのみ、非同期で DB 反映
- Read Through: キャッシュミス時にキャッシュライブラリが DB 取得まで処理
キャッシュ無効化戦略:
- TTL (Time To Live): 時間ベースの期限切れ
- イベントベース: DB 変更時にキャッシュ削除(
@CacheEvict) - バージョニング: キーにバージョンを含めて新しいキーで迂回
JPA のキャッシュ:
- 1次キャッシュ:
EntityManager単位。トランザクション内で同じエンティティは 1 回だけ取得。 - 2次キャッシュ:
SessionFactory単位。アプリケーション全体で共有。EhCache、Hazelcast などを使用。
Redis の障害対応#
Redis が落ちた時にどうするか聞かれることがあります。
永続化戦略:
- RDB (Snapshot): 特定時点のメモリ全体をディスクにダンプ。復旧は速いが、データ損失の可能性。
- AOF (Append Only File): すべての書き込みコマンドをログに保存。データの安全性は高いが、ファイルが大きくなり復旧が遅い。
- ミックスモード: RDB + AOF を併用 (Redis 4.0+)
高可用性構成:
- Replication: マスター・スレーブ複製
- Sentinel: 自動フェイルオーバー、マスター監視
- Cluster: データシャーディング + 複製、水平スケール
キャッシュ障害時の備え:
- Circuit Breaker を適用 (Resilience4j、Hystrix)
- DB 直接参照へのフォールバック
- Local Cache 併用 (Caffeine などで 2 段階キャッシュ)
- Cache Stampede 防止: キャッシュ期限切れ時に同時に DB 参照が殺到するのを防ぐロック (PER アルゴリズムなど)
ネットワーク & HTTP#
RESTful API 設計原則#
- リソース中心 URL:
/users/123/orders(行為ではなくリソース) - HTTP メソッドで行為を表現: GET、POST、PUT、PATCH、DELETE
- ステータスコードを意味通りに使う: 200、201、204、400、401、403、404、409、500
- HATEOAS(任意): レスポンスに次の行動リンクを含める
PUT vs PATCH:
- PUT: リソース全体を置き換え。冪等性を保証。
- PATCH: 部分修正。冪等性は実装次第。
HTTP メソッドの特性:
| メソッド | Safe | Idempotent | Cacheable |
|---|---|---|---|
| GET | ○ | ○ | ○ |
| HEAD | ○ | ○ | ○ |
| OPTIONS | ○ | ○ | × |
| POST | × | × | 条件付き |
| PUT | × | ○ | × |
| PATCH | × | × | × |
| DELETE | × | ○ | × |
- Safe: サーバーの状態を変更しない
- Idempotent: 何度呼び出しても結果が同じ
HTTPS と SSL/TLS#
- TLS Handshake: クライアントとサーバーが暗号方式と鍵を交渉
- 証明書: CA が発行、サーバーの身元を保証
- 対称鍵 vs 非対称鍵: Handshake は非対称(RSA、ECDH)、実際の通信は対称(AES)
- HTTP/2: マルチプレクシング、ヘッダー圧縮、Server Push。事実上 HTTPS が必須。
- HTTP/3: QUIC (UDP) ベース。接続セットアップが速い。
Cookie vs Session vs JWT#
| 項目 | Cookie | Session | JWT |
|---|---|---|---|
| 保存場所 | クライアント | サーバー | クライアント |
| セキュリティ | 低 | 高 | 中 |
| スケーラビリティ | - | サーバー同期が必要 | Stateless、スケール容易 |
| 無効化 | 即時 | 即時 | 困難(期限まで有効) |
Cookie のセキュリティオプション:
HttpOnly: JavaScript アクセスを遮断 (XSS 防御)Secure: HTTPS でのみ送信SameSite: CSRF 防御 (Strict、Lax、None)
SQL Injection の防御#
核心原則: ユーザー入力を絶対に SQL 文字列に直接結合しないこと。
防御方法:
- PreparedStatement (パラメータバインディング):
PreparedStatement ps = conn.prepareStatement( "SELECT * FROM users WHERE id = ?"); ps.setString(1, userInput); - ORM を使う: JPA や MyBatis の
#{}は自動でバインディング - MyBatis
${}に注意: 文字列置換なのでインジェクション可能。ORDER BY カラムなどに限定し、ホワイトリスト検証必須。 - 入力検証: ホワイトリストベースの検証
- 最小権限の原則: DB ユーザーには必要な権限のみ付与
プロキシ vs ゲートウェイ#
- Forward Proxy: クライアントの前にある。クライアントのリクエストを代行(社内ネットワーク → 外部)
- Reverse Proxy: サーバーの前にある。外部リクエストを受けて内部サーバーに転送 (Nginx、HAProxy)
- API Gateway: Reverse Proxy + 認証、ルーティング、変換、ロギングなどの付加機能 (Spring Cloud Gateway、Kong)
運用 & インフラ#
モニタリング (Observability の 3 軸)#
- Metrics(指標): CPU、メモリ、応答時間、スループットなどの数値
- Prometheus + Grafana
- Datadog、New Relic
- Logs(ログ): イベント記録
- ELK Stack (Elasticsearch + Logstash + Kibana)
- Fluentd、Loki
- Traces(分散トレース): リクエストが複数サービスを通る流れ
- Jaeger、Zipkin
- OpenTelemetry(標準)
APM (Application Performance Monitoring):
- Pinpoint、Scouter(オープンソース、韓国でよく使われる)
- New Relic、Datadog APM(商用)
デプロイ戦略#
20 台以上のサーバーをどうデプロイするか、という質問が出たことがあります。
無停止デプロイ方式:
- Rolling Update: サーバーを順次アップデート。段階的だが、2 つのバージョンが共存する時間がある。
- Blue-Green: 同じ環境を 2 つ用意して一気にトラフィックを切り替え。リソースが 2 倍必要だが、即座にロールバック可能。
- Canary: 一部のトラフィックだけ新バージョンに送り、問題なければ徐々に拡大。A/B テストに似ています。
CI/CD パイプライン:
- コードプッシュ → CI トリガー
- テスト + ビルド
- Docker イメージ生成、レジストリプッシュ
- ステージングデプロイ + 検証
- 本番デプロイ(手動承認または自動)
- ヘルスチェック + モニタリング
- 異常時に自動ロールバック
障害対応#
「障害が起きたら何を最初に見るか」という質問もよく出ます。
対応順序:
- 影響範囲の把握: 全ユーザー? 一部? どの機能?
- モニタリングダッシュボード確認: CPU、メモリ、応答時間、エラー率の急変
- ログ確認: ERROR、Exception キーワード、トランザクション ID 追跡
- 最近の変更確認: デプロイ履歴、設定変更、DB マイグレーション
- ロールバック判断: 原因が分からず時間が経つなら、まずロールバック
- 応急対応: 障害が広がらないよう遮断(フィーチャーフラグ、トラフィック制限)
- 根本原因分析 (RCA): 事後振り返り、再発防止
良い回答パターン: 「段階別に何を確認し、なぜその順序なのか」を説明できると印象が良いです。
Kubernetes のリソース管理#
resources:
requests:
memory: '64Mi'
cpu: '250m'
limits:
memory: '128Mi'
cpu: '500m'- requests: コンテナに最低保証されるリソース
- limits: コンテナが使用できる最大リソース
- requests = limits に設定すると Guaranteed QoS クラス(最も安定)
- メモリ limit 超過時は OOMKilled、CPU limit 超過時は Throttling
アーキテクチャ (MSA)#
MSA の長所と短所#
長所:
- 独立デプロイ: サービスごとに別々にデプロイ可能
- 技術スタックの自由: サービスごとに異なる言語/フレームワークを使用可能
- 障害の隔離: 1 つのサービスの障害が全体に波及しない
- チームの自律性: 各チームが自分のサービスのオーナーシップを持つ
短所:
- ネットワークレイテンシ: サービス間呼び出しがすべてネットワーク通信
- 分散トランザクション: 複数サービスにまたがるトランザクション処理が困難 (Saga パターンなどが必要)
- 複雑な統合テスト: 複数のサービスを一緒に立ち上げる必要がある
- 運用の複雑さが増す: モニタリング、ロギング、トレースのインフラが必要
- データ整合性: サービスごとに DB が別れていると整合性保証が難しい
MSA でよく使うパターン#
- API Gateway: 単一の入り口、ルーティング、認証
- Service Discovery: Eureka、Consul でサービス位置を動的管理
- Circuit Breaker: 障害伝播防止 (Resilience4j)
- Saga: 分散トランザクション。Choreography(イベントベース) vs Orchestration(中央調整役)
- Event Sourcing: 状態ではなくイベントを記録
- CQRS: コマンド (Command) と参照 (Query) を分離
テスト#
なぜ TDD を導入するのか?#
- 回帰バグ防止 — リファクタリングに自信を与えてくれます
- 設計の改善 — テストしにくいコードはたいてい設計が悪いコード
- ドキュメント効果 — テストコード自体が使用例
- 速いフィードバック — 小さい単位で開発するので問題を早く発見
TDD サイクル (Red-Green-Refactor):
- Red: 失敗するテストを書く
- Green: テストを通す最小限のコードを書く
- Refactor: 重複を取り除き、可読性を改善
F.I.R.S.T 原則#
- Fast: 速くないと頻繁に回せない
- Independent: テスト同士に依存があってはいけない
- Repeatable: 環境に関係なく同じ結果
- Self-validating: 成功/失敗が明確
- Timely: 適切な時期に作成 (TDD: コードより先に)
Mock の活用#
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void findUser() {
// given
given(userRepository.findById(1L))
.willReturn(Optional.of(new User("kim")));
// when
User user = userService.findById(1L);
// then
assertThat(user.getName()).isEqualTo("kim");
verify(userRepository).findById(1L);
}
}Service vs Controller のテスト:
- Service 単位: Mockito で依存オブジェクトをモックし、ビジネスロジックだけ検証
- Controller 単位:
MockMvcで HTTP リクエスト・レスポンスのフローを検証mockMvc.perform(get("/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("kim")); - Controller にはビジネスロジックを置かない方がいいですね。呼び出しの委譲くらいをテストすれば十分です。
テストの種類#
- Unit Test: 単一のクラス/メソッド。速くて隔離されている。
- Integration Test: 複数のコンポーネントを統合。DB や外部 API を含むこともある。
@SpringBootTest - E2E Test: ユーザーシナリオを最後まで。最も遅くてコストが高いが、最も信頼性が高い。
- Test Pyramid: Unit が最も多く、E2E が最も少ない形が理想
最後に#
面接の質問は技術ごとに似た流れをたどります。「なぜこれを使うのか? 短所は? 代替案は?」 という 3 つの軸で答えを準備しておけば、どの会社に行っても揺るがずに答えられますよ。
特に自分が直接使った技術については、「なぜその選択をしたのか」 を一行でまとめておくのがおすすめです。面接官は正解を聞きたいのではなく、思考過程 を見たいんですよね。
最後にもう一つ。知らない質問が出たときに 「知りません」 と素直に答えるのも能力のうちです。その後に「こう接近してみると思います」と思考過程を見せると、かえって良い印象になることもあります。すべてを知っている人なんていないですから。
Lose an hour in the morning, and you will spend all day looking for it.
— Richard Whately