0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

初めて入る現場の巨大なコードベースの読み方 ― 「横に広く」ではなく「縦に1本」通す5ステップ

0
Posted at

株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。

Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
コーポレートサイト

はじめに

新しい現場に入った初日。リポジトリをクローンして src を開いた瞬間、こう思ったことはないでしょうか。

ファイルが多すぎる。どこから読めばいいのか分からない。

SES常駐でも、受託の引き継ぎ案件でも、社内異動でも同じです。数万〜数十万行のコードと、書いた人がもういないドキュメントと、「とりあえずこのチケットお願いします」という初日のタスクだけが渡されます。

この記事は その状況で翌日から使える「読む順序」 をまとめたものです。「気合いで読め」「慣れだよ」といった精神論は一切書きません。手順とコマンドだけを書きます。

結論を先に言うと、こうです。

コードベースは「横に広く」読むと必ず失敗する。「縦に1本」通すと勝てる。

検証環境について

この記事のコマンドと出力は、すべて実際のオープンソースリポジトリで実行して確認したものです。手元で同じことを再現できます。

項目 内容
題材 spring-projects/spring-petclinic(Spring Boot公式サンプル)
ブランチ / コミット main / f182358(2026-07-16時点)
技術スタック Spring Boot 4.1.0 / Java 17 / Spring Data JPA / Thymeleaf
規模 Javaファイル49本・4,106行(main 1,861行 / test 2,245行)、コミット1,037件
git clone https://github.com/spring-projects/spring-petclinic.git
cd spring-petclinic

題材のリポジトリ自体は「巨大」ではありません(再現性を優先して、誰でもすぐクローンできる公開リポジトリを選びました)。ただし 紹介するコマンドと手順は規模が大きいほど効きます。10万行のコードベースでも、コマンドはそのまま同じです。むしろ規模が大きいほど「全部読む」が不可能になるので、この手順の価値が上がります。

なお、掲載している出力例のうち、パッケージパス org/springframework/samples/petclinic が長くなる箇所は紙面の都合で ... と省略しています(実際の出力はフルパスで出ます)。それ以外は実行結果そのままです。


1. なぜ「全部読む」は失敗するのか

失敗パターン3つ

新しい現場に入って最初の1週間、多くの人がこうします。

よくある行動 何が起きるか
① ディレクトリを上から順にファイルを開いていく 3日経っても「クラス名をなんとなく見た」以上のものが残らない
② 設計書を最初から最後まで読む 更新が止まっていて、コードと食い違う。しかもそれに気づけない
③ 「全体を理解してから着手しよう」とする 着手が遅れ、進捗を聞かれ、焦ってさらに読む量を増やす悪循環

3つに共通する原因はひとつです。読む量を減らす工夫をしないまま読み始めている こと。

10万行のコードを1行1秒で読んでも27時間かかります。しかも1周しただけでは覚えていません。つまり、初動の設計はこうあるべきです。

「どう速く読むか」ではなく、「どう読まずに済ませるか」

目的を置き換える

新しい現場での最初の目標を、次のように置き換えてください。

  • ❌ 悪い目標:このシステムの全体を理解する
  • ⭕ 良い目標:「ユーザーの1操作が、どのコードを通ってDBに着地するか」を1本だけ、正確に説明できるようにする

1本通すと何が起きるか。そのシステムの「型」が手に入ります。

  • リクエストはどこで受けるのか
  • 入力チェックはどのレイヤーの責務なのか
  • DBアクセスの書き方はどれが正なのか(同じ処理に3通りの書き方があるコードベースは普通にあります)
  • ログとエラーハンドリングの流儀はどうなっているか
  • テストはどの粒度で書かれているか

2本目以降は、この型との「差分」だけを読めば済みます。読む量が劇的に減ります。これが「縦に1本」の狙いです。


2. 読む順序の型(全体像)

所要時間の目安は「初日にStep 1〜2、2日目にStep 3、3日目からStep 4」です。Step 3を飛ばしてStep 4に行かないこと。型がないまま個別ファイルを読むと、そのコードが「この現場の普通」なのか「例外的にヤバい箇所」なのか判断できません。


3. Step 1:外形把握 ― 最初に見る7つのファイル

コードは1行も読みません。リポジトリの「外側」だけを見ます。 ここで分かることが、後の読解の解像度を決めます。

# 見るもの 分かること
1 README / CONTRIBUTING 起動方法・前提ミドルウェア・開発フロー。古い可能性があるので鵜呑みにしない
2 依存定義(pom.xml / build.gradle / package.json 等) 技術スタックとバージョン。フレームワークの世代が分かる
3 ディレクトリ構成 レイヤ分割か機能分割か。設計思想が出る
4 設定ファイル(application.properties / .env.example 等) 外部接続先・機能フラグ・性能に効く設定
5 DBスキーマ / マイグレーション ドメインの正体。ここが一番嘘をつかない
6 テストコード 現時点で保証されている仕様。実行可能な設計書
7 CI設定(.github/workflows 等) 何が品質ゲートか。ビルド・テスト・静的解析の実際の手順

以下、実際のコマンドと出力です。

3-1. 規模と言語構成をつかむ

# 管理対象ファイル数
git ls-files | wc -l
     130
# 拡張子ランキング=この現場の主戦場が分かる
git ls-files | grep -o '\.[a-zA-Z0-9]*$' | sort | uniq -c | sort -rn | head -12
  49 .java
  15 .properties
  12 .html
   8 .yml
   7 .sql
   4 .txt
   4 .scss
   4 .png
   3 .xml
   3 .svg
   2 .woff
   2 .ttf
# 行数(対象言語を変えれば他言語でも同じ)
git ls-files '*.java' | xargs wc -l | tail -1
    4106 total

.properties が15本ある時点で「多言語対応がある」と当たりがつきます。.sql が7本あるので、スキーマがコード管理されていることも分かります。この段階では推測でOKです。後で答え合わせします。

3-2. ディレクトリの重心を見る

tree を眺めるより、ファイル数の分布 を見るほうが速いです。

git ls-files | awk -F/ '{ if (NF>1) print $1"/"$2; else print $1 }' | sort | uniq -c | sort -rn | head -12
  83 src/main
  20 src/test
   3 .github/workflows
   2 src/checkstyle
   2 gradle/wrapper
   1 settings.gradle
   1 pom.xml
   1 mvnw.cmd
   1 mvnw
   1 k8s/petclinic.yml
   1 k8s/db.yml
   1 gradlew.bat

重心が src/main にあること、k8sマニフェストがあること、checkstyle設定があること(=コーディング規約が機械チェックされる)が一気に分かります。

3-3. ビルド定義・インフラ・スキーマを一発で拾う

# ビルド定義
git ls-files | grep -iE '(pom\.xml|build\.gradle|package\.json|requirements\.txt|go\.mod|Gemfile|composer\.json)$'
build.gradle
pom.xml

Maven と Gradle が 両方 あります。「実際にCIで使われているのはどっち?」という疑問が生まれます。これは後でCI設定を見れば確定します。

# インフラ・CI関連
git ls-files | grep -iE '^(\.github/|Dockerfile|docker-compose|k8s/|\.gitlab-ci)'
.github/dco.yml
.github/workflows/deploy-and-test-cluster.yml
.github/workflows/gradle-build.yml
.github/workflows/maven-build.yml
docker-compose.yml
k8s/db.yml
k8s/petclinic.yml

答え合わせ完了。Maven も Gradle も両方CIで回っていますmaven-build.ymlgradle-build.yml の両方が存在)。こういう「ドキュメントに書いていないが事実である」情報が、外形把握では一番価値があります。

# スキーマ・マイグレーション
git ls-files | grep -iE 'migration|schema|flyway|liquibase'
src/main/resources/db/h2/schema.sql
src/main/resources/db/mysql/schema.sql
src/main/resources/db/postgres/schema.sql

3-4. DBスキーマを読む(ここが最重要)

外形把握で一番時間をかけるべきはDBスキーマです。 理由は3つ。

  1. 設計書と違って、スキーマは嘘をつけない(動いているシステムの実体そのもの)
  2. テーブルとカラムを見れば、そのシステムのドメイン(業務の中身)が分かる
  3. 外部キーと一意制約を見れば、業務ルール が分かる
grep -E 'CREATE TABLE|CONSTRAINT|CREATE INDEX' src/main/resources/db/h2/schema.sql
CREATE TABLE vets (
CREATE INDEX vets_last_name ON vets (last_name);
CREATE TABLE specialties (
CREATE INDEX specialties_name ON specialties (name);
CREATE TABLE vet_specialties (
ALTER TABLE vet_specialties ADD CONSTRAINT fk_vet_specialties_vets FOREIGN KEY (vet_id) REFERENCES vets (id);
ALTER TABLE vet_specialties ADD CONSTRAINT fk_vet_specialties_specialties FOREIGN KEY (specialty_id) REFERENCES specialties (id);
CREATE TABLE types (
CREATE INDEX types_name ON types (name);
CREATE TABLE owners (
CREATE INDEX owners_last_name ON owners (last_name);
CREATE TABLE pets (
ALTER TABLE pets ADD CONSTRAINT fk_pets_owners FOREIGN KEY (owner_id) REFERENCES owners (id);
ALTER TABLE pets ADD CONSTRAINT fk_pets_types FOREIGN KEY (type_id) REFERENCES types (id);
ALTER TABLE pets ADD CONSTRAINT unique_owner_pet_name UNIQUE (owner_id, name);
CREATE INDEX pets_name ON pets (name);
CREATE TABLE visits (
ALTER TABLE visits ADD CONSTRAINT fk_visits_pets FOREIGN KEY (pet_id) REFERENCES pets (id);
CREATE INDEX visits_pet_id ON visits (pet_id);

カラム定義を読む前に、テーブル名・外部キー・一意制約だけ を見るのがコツです。20行足らずでシステムの骨格が見えます。

ここから、コードを1行も読まずに次が確定します。

  • 飼い主(owners) 1 ─ n ペット(pets) 1 ─ n 診察(visits) という構造
  • 同じ飼い主が同じ名前のペットを2匹登録することはできないunique_owner_pet_name

最後の1行が業務ルールです。この制約を知らずに「ペット登録機能」を触ると事故ります。逆に知っていれば、UNIQUE 違反時のエラーハンドリングがコードのどこにあるかを探しに行けます。

3-5. 設定ファイルは「性能と挙動のスイッチ」を探す

grep -vE '^\s*(#|$)' src/main/resources/application.properties

コメントと空行を除いた実質12行だけが残ります。

database=h2
spring.sql.init.schema-locations=classpath*:db/${database}/schema.sql
spring.sql.init.data-locations=classpath*:db/${database}/data.sql
spring.thymeleaf.mode=HTML
spring.jpa.hibernate.ddl-auto=none
spring.jpa.open-in-view=false
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategySnakeCaseImpl
spring.jpa.properties.hibernate.default_batch_fetch_size=16
spring.messages.basename=messages/messages
management.endpoints.web.exposure.include=*
logging.level.org.springframework=INFO
spring.web.resources.cache.cachecontrol.max-age=12h

読み取れること:

  • ddl-auto=noneスキーマはJPAが自動生成していない。SQLファイルが正
  • open-in-view=false → ビュー層で遅延ロードできない。Controllerに渡す前に必要なデータを取り切る設計
  • default_batch_fetch_size=16 → N+1問題を意識した設定が入っている
  • physical-strategy=...SnakeCaseImpl → Javaの lastName がDBの last_name に対応する(Entityにカラム名が書いていない理由がこれ
  • spring.messages.basename → 3-1で推測した多言語対応が確定
  • management.endpoints.web.exposure.include=* → Actuatorが全公開(後述の/actuator/mappingsが使える)

設定ファイル1枚から、コードの書き方の制約が読み取れます。 ここを飛ばして実装コードを読むと、「なぜこんな書き方をしているのか」が分からずモヤモヤします。

3-6. テストコードは「実行可能な設計書」

テストは、ドキュメントの中で唯一 壊れたらCIが教えてくれる 仕様書です。まずはテストのファイル名だけ眺めます。

git ls-files 'src/test/*' | head -20
src/test/java/org/springframework/samples/petclinic/MySqlIntegrationTests.java
src/test/java/org/springframework/samples/petclinic/MysqlTestApplication.java
src/test/java/org/springframework/samples/petclinic/PetClinicConcurrencyTests.java
src/test/java/org/springframework/samples/petclinic/PetClinicIntegrationTests.java
src/test/java/org/springframework/samples/petclinic/PostgresIntegrationTests.java
src/test/java/org/springframework/samples/petclinic/model/ValidatorTests.java
src/test/java/org/springframework/samples/petclinic/owner/OwnerControllerTests.java
src/test/java/org/springframework/samples/petclinic/owner/PetControllerTests.java
src/test/java/org/springframework/samples/petclinic/owner/PetTypeFormatterTests.java
src/test/java/org/springframework/samples/petclinic/owner/PetValidatorTests.java
src/test/java/org/springframework/samples/petclinic/owner/VisitControllerTests.java
src/test/java/org/springframework/samples/petclinic/service/ClinicServiceTests.java
src/test/java/org/springframework/samples/petclinic/system/CrashControllerIntegrationTests.java
src/test/java/org/springframework/samples/petclinic/system/CrashControllerTests.java
src/test/java/org/springframework/samples/petclinic/system/I18nPropertiesSyncTest.java
src/test/java/org/springframework/samples/petclinic/system/WelcomeControllerTests.java
src/test/java/org/springframework/samples/petclinic/vet/VetControllerTests.java
src/test/java/org/springframework/samples/petclinic/vet/VetTests.java
src/test/jmeter/petclinic_test_plan.jmx

ファイル名だけで、Controller 単体テスト・DB統合テスト(MySQL/Postgres別)・並行性テスト・JMeterの負荷試験計画まであることが分かります。テストの構成は、そのチームが「何を怖がっているか」の一覧です。 並行性テストがあるということは、過去に競合で事故った(あるいは事故りかけた)と推測できます。

Step 1の終了条件:「このシステムは何をするシステムで、主要テーブルは何と何で、どうビルド・テストされるか」を口頭で30秒で説明できる状態。実装の詳細は一切不要です。


4. Step 2:エントリポイントを特定する

「入口」が分からないコードは読めません。逆に入口さえ分かれば、あとは呼び出しを辿るだけの単純作業になります。入口は種類ごとに探し方が決まっています。

入口の種類 探し方(例)
Webのリクエスト @RestController @Controller @GetMapping などを grep
アプリ起動処理 main( / @SpringBootApplication を grep
バッチ・定期実行 @Scheduled / cron定義 / ジョブ定義ファイル
非同期メッセージ @KafkaListener @JmsListener / SQSコンシューマ
画面(SPA / モバイル) ルーティング定義(createBrowserRouterNavigationStack など)
grep -rn "@SpringBootApplication" --include='*.java' src/main
src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java:28:@SpringBootApplication
grep -rEn "@(Get|Post|Put|Delete|Request)Mapping" --include='*.java' src/main | head -20
src/main/java/.../owner/VisitController.java:90:	@GetMapping("/owners/{ownerId}/pets/{petId}/visits/new")
src/main/java/.../owner/VisitController.java:97:	@PostMapping("/owners/{ownerId}/pets/{petId}/visits/new")
src/main/java/.../system/WelcomeController.java:25:	@GetMapping("/")
src/main/java/.../vet/VetController.java:44:	@GetMapping("/vets.html")
src/main/java/.../vet/VetController.java:65:	@GetMapping({ "/vets" })
src/main/java/.../owner/OwnerController.java:72:	@GetMapping("/owners/new")
src/main/java/.../owner/OwnerController.java:77:	@PostMapping("/owners/new")
src/main/java/.../owner/OwnerController.java:89:	@GetMapping("/owners/find")
src/main/java/.../owner/OwnerController.java:94:	@GetMapping("/owners")
src/main/java/.../owner/OwnerController.java:136:	@GetMapping("/owners/{ownerId}/edit")
src/main/java/.../owner/OwnerController.java:141:	@PostMapping("/owners/{ownerId}/edit")
src/main/java/.../owner/OwnerController.java:166:	@GetMapping("/owners/{ownerId}")
src/main/java/.../system/CrashController.java:31:	@GetMapping("/oups")

(パスは紙面の都合で org/springframework/samples/petclinic 部分を ... に省略しています。実際の出力はフルパスで出ます)

これがこのシステムの「機能一覧」です。 画面の一覧表を探しに行くより速く、確実です。

補足:Spring Bootなら実行時に一覧を取る手もある

Spring Boot Actuator が有効なら、起動後に /actuator/mappings でURLとハンドラメソッドの対応表をJSONで取得できます(このリポジトリは management.endpoints.web.exposure.include=* なので取得可能)。アノテーションの継承やライブラリ側が追加したエンドポイントまで含めて実物が見える のが強みです。

ただし本番環境でActuatorが全公開されていることは(セキュリティ上)まずないので、ローカル or 開発環境で試す前提 の技です。


5. Step 3:縦に1本通す ― この記事の核心

ここが本題です。1つの操作を、入口から出口まで、寄り道せずに追い切ります。

対象に選ぶべきなのは次の条件を満たす操作です。

  • そのシステムの中心的なユースケースであること(マスタメンテ画面などの端っこを選ばない)
  • 入力があり、DBに触り、画面かレスポンスを返すこと(=全レイヤーを通る)
  • できれば、担当タスクに関係すること

今回は「飼い主を苗字で検索する(GET /owners?lastName=Franklin)」を選びます。

5-1. トレースの結果

先に完成形を示します。この表が作れれば、Step 3は完了です。

# レイヤー 実体 分かったこと
1 入口 OwnerController#processFindForm@GetMapping("/owners"), L94) クエリパラメータが Owner オブジェクトにバインドされる
2 入力の組み立て @ModelAttribute("owner") findOwner(L64) ownerId が無ければ空の Owner。あればDBから取得
3 入力の防御 @InitBinder setAllowedFields(L60) id*.id をバインド禁止(不正な上書きの防止)
4 ページング findPaginatedForOwnersLastName(L130〜) 1ページ5件固定(L131 int pageSize = 5;
5 データ取得 OwnerRepository#findByLastNameStartingWith(L45) 実装コードなし。メソッド名からクエリが自動生成される
6 ドメイン Owner@Entity / @Table(name="owners") @OneToManyPet を保持
7 出口(分岐) processFindForm 0件→エラー表示 / 1件→詳細へリダイレクト / 複数→一覧
8 画面 templates/owners/ownersList.html th:each="owner : ${listOwners}" でモデル名が一致

5-2. 実際の追い方

① 入口のメソッドを読む

@GetMapping("/owners")
public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner owner, BindingResult result,
        Model model) {
    // allow parameterless GET request for /owners to return all records
    String lastName = owner.getLastName();
    if (lastName == null) {
        lastName = ""; // empty string signifies broadest possible search
    }

    // find owners by last name
    Page<Owner> ownersResults = findPaginatedForOwnersLastName(page, lastName);
    if (ownersResults.isEmpty()) {
        // no owners found
        result.rejectValue("lastName", "notFound", "not found");
        return "owners/findOwners";
    }

    if (ownersResults.getTotalElements() == 1) {
        // 1 owner found
        owner = ownersResults.iterator().next();
        return "redirect:/owners/" + owner.getId();
    }

    // multiple owners found
    return addPaginationModel(page, model, ownersResults);
}

src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java L94〜 より引用)

ここで 「1件だけヒットしたら一覧を出さずに詳細へリダイレクトする」 という仕様に気づけます。これは設計書に書かれていないことが多い、しかしテストで確実に守られている類の仕様です。

② データ取得のメソッドを辿る

private Page<Owner> findPaginatedForOwnersLastName(int page, String lastname) {
    int pageSize = 5;
    Pageable pageable = PageRequest.of(page - 1, pageSize);
    return owners.findByLastNameStartingWith(lastname, pageable);
}

pageSize がハードコードされています。「設定で変えられるはず」という思い込みを持たずに、実物を見る。 これがトレースの価値です。

③ Repositoryの実装を探す ― そして「無い」ことを知る

// src/main/java/.../owner/OwnerRepository.java(ライセンスヘッダとJavadocを省略した抜粋)
public interface OwnerRepository extends JpaRepository<Owner, Integer> {

    Page<Owner> findByLastNameStartingWith(String lastName, Pageable pageable);   // L45

    Optional<Owner> findById(Integer id);

}

インターフェースの宣言だけで、実装クラスがありません。Spring Data JPA が メソッド名からクエリを生成する ためです。

ここが初見殺しポイントで、implements OwnerRepository を探しても見つからず時間を溶かします。「実装が見つからないときは、フレームワークが生成している」を疑う のが定石です(同種の仕組みは MyBatis のMapperインターフェース、TypeScriptのDIコンテナ、Railsのmethod_missingなど、どの言語にもあります)。

④ 仕様をテストで裏取りする

推測を確定させるには、テストを読むのが最短です。

grep -rn "findByLastNameStartingWith" src --include='*.java'

呼び出し元と、それを検証しているテストが一覧で出ます。実際のテストはこうなっています。

@Test
void shouldFindOwnersByLastName() {
    Page<Owner> owners = this.owners.findByLastNameStartingWith("Davis", pageable);
    assertThat(owners).hasSize(2);

    owners = this.owners.findByLastNameStartingWith("Daviss", pageable);
    assertThat(owners).isEmpty();
}

src/test/java/.../service/ClinicServiceTests.java L90〜 より引用)

前方一致であること が、テストで明示的に保証されていると分かりました。同様に OwnerControllerTests には「1件ヒット時はリダイレクト」「0件時は lastNamenotFound エラーが付く」というテストがあり、①で読んだ分岐が仕様であることが確定します。

@Test
void processFindFormNoOwnersFound() throws Exception {
    Page<Owner> tasks = new PageImpl<>(List.of());
    when(this.owners.findByLastNameStartingWith(eq("Unknown Surname"), any(Pageable.class))).thenReturn(tasks);
    mockMvc.perform(get("/owners?page=1").param("lastName", "Unknown Surname"))
        .andExpect(status().isOk())
        .andExpect(model().attributeHasFieldErrors("owner", "lastName"))
        .andExpect(model().attributeHasFieldErrorCode("owner", "lastName", "notFound"))
        .andExpect(view().name("owners/findOwners"));
}

src/test/java/.../owner/OwnerControllerTests.java L158〜 より引用)

⑤ 出口(画面)まで通す

Controllerが返した文字列 "owners/ownersList" がビュー名です。

grep -n "listOwners\|totalPages" src/main/resources/templates/owners/ownersList.html | head -3
20:      <tr th:each="owner : ${listOwners}">
31:  <div th:if="${totalPages > 1}">
34:    <span th:each="i: ${#numbers.sequence(1, totalPages)}">

Controllerで model.addAttribute("listOwners", ...) した名前と、テンプレート側の ${listOwners} が一致しました。ここで初めて「1本通った」と言えます。

5-3. なぜ「縦に1本」が効くのか

横に読む(=Controllerを全部読む、次にServiceを全部読む…)と、レイヤーごとの断片が増えるだけで、つながりが手に入りません。しかも量が多い。

縦に1本通すと、量は圧倒的に少ないのに、次の全部が同時に手に入ります。

  • レイヤー構成と各層の責務
  • 命名規約(processFindFormownersList のような命名の癖)
  • 入力バリデーションの流儀
  • エラー時の分岐の作法
  • テストの粒度と書き方
  • 使っているフレームワーク機能の範囲

そして何より「次に何かを調べるときの探し方」が身につきます。 これが最大の収穫です。


6. Step 4:変更対象の周辺だけを深掘りする

1本通したら、あとは 担当タスクに必要な範囲だけ を深く読みます。ここでやるのは「影響範囲の確定」です。

6-1. 呼び出し元を全部出す

IDEの「参照を検索」が最速ですが、grepでも確実にできます(設定ファイルやテンプレートからの参照も拾えるので、実はgrepのほうが漏れが少ない場面があります)。

# 対象メソッドの呼び出し元(テスト含む)
grep -rn "findByLastNameStartingWith" src --include='*.java'

# 定数・設定キーが使われている箇所(言語を問わない)
grep -rn "pageSize" src/main/java
src/main/java/.../vet/VetController.java:60:		int pageSize = 5;
src/main/java/.../vet/VetController.java:61:		Pageable pageable = PageRequest.of(page - 1, pageSize);
src/main/java/.../owner/OwnerController.java:131:		int pageSize = 5;
src/main/java/.../owner/OwnerController.java:132:		Pageable pageable = PageRequest.of(page - 1, pageSize);

「ページサイズを変えたい」というタスクなら、2箇所ある ことがこれで確定します。片方だけ直して画面ごとに件数が変わる、という典型的な事故を回避できます。

6-2. 深掘りの順序

順序 見るもの 理由
1 対象クラスのテスト 現在保証されている振る舞いを壊さないため
2 対象メソッドの呼び出し元 影響範囲の確定
3 対象が触るテーブルの制約 DB制約違反はテストをすり抜けやすい
4 関連する設定値 環境ごとに挙動が変わる箇所の把握

「読み終わってから着手」ではなく「着手しながら読む」 に切り替えるのがStep 4です。実際に1行変えてテストを走らせると、理解の精度が一気に上がります。読むだけで得られる理解には限界があります。


7. gitで「歴史」を読む ― コードに書いていない情報を取り出す

コードは「現在」しか教えてくれません。なぜそうなっているか はgitの中にあります。ここは新しい現場で圧倒的に効くのに、やっていない人が多い領域です。

7-1. ホットスポット(変更頻度の高いファイル)を特定する

変更頻度の高いファイルは、そのシステムの心臓部か、あるいは不安定な箇所 です。どちらにせよ最優先で読む価値があります。

git log --since='2 years ago' --name-only --pretty=format: -- 'src/main/**/*.java' \
  | grep -v '^$' | sort | uniq -c | sort -rn | head -10
  12 src/main/java/org/springframework/samples/petclinic/owner/PetController.java
  11 src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java
   9 src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java
   8 src/main/java/org/springframework/samples/petclinic/owner/Pet.java
   8 src/main/java/org/springframework/samples/petclinic/owner/Owner.java
   7 src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java
   6 src/main/java/org/springframework/samples/petclinic/owner/VisitController.java
   6 src/main/java/org/springframework/samples/petclinic/PetClinicRuntimeHints.java
   5 src/main/java/org/springframework/samples/petclinic/vet/Vet.java
   5 src/main/java/org/springframework/samples/petclinic/owner/Visit.java

owner パッケージに変更が集中していると一目で分かります。このシステムの重心はowner周辺 ということです。Step 3で追う対象を選ぶときも、この結果を参考にできます。

ポイント-- 'src/main/**/*.java' のパス指定を省くと、既に削除されたファイルも上位に出てきます(例:過去に存在した JdbcOwnerRepositoryImpl.java)。「今存在するファイル」に絞りたいときはパス指定を効かせるか、期間を短くしてください。逆に 削除済みファイルが上位に来ること自体が「大きな作り直しが過去にあった」というシグナル でもあります。

7-2. 直近のコミットで「今の関心事」を知る

マージコミットだけを辿ると、PR単位の粒度で履歴が読めます。

git log --first-parent --oneline -8
f182358 i18n: add Hindi translation
e0db9b1 Enforce unique pet names per owner
51045d1 test: add WelcomeController test and PetValidator supports() tests
b3ee2c5 Add test for duplicate pet name during update
b11afec Add test for updating pet with same name
1312314 Remove unused Vets object from VetController
9a5d50c Upgrade to Spring Boot 4.1
a2c2ef9 Register native resource hints for nested db/{h2,mysql,postgres}/ scripts

「ペット名の重複」というテーマが直近で連続している ことが読み取れます。つまり今このチームの関心はそこにあり、あなたがその周辺を触るなら、直近の議論を踏まえる必要があります。

そのコミットが何を触ったかは --stat で見ます。

git show --stat --format='%h %ad %s' --date=short e0db9b1
e0db9b1 2026-07-14 Enforce unique pet names per owner

 .../samples/petclinic/owner/PetController.java     |  32 ++++-
 src/main/resources/db/h2/schema.sql                |   3 +-
 src/main/resources/db/mysql/schema.sql             |   3 +-
 src/main/resources/db/postgres/schema.sql          |   1 +
 .../petclinic/PetClinicConcurrencyTests.java       | 134 +++++++++++++++++++++
 .../petclinic/owner/PetControllerTests.java        |  35 ++++++
 .../petclinic/service/ClinicServiceTests.java      |  64 ++++++++++
 7 files changed, 266 insertions(+), 6 deletions(-)

これはこのチームの「変更の作法」の教科書です。 1つの業務ルールを入れるとき、Controller・3種類のDBスキーマ・並行性テスト・単体テストをセットで触っている。あなたが同種の変更をするなら、同じセットを触るべきだと分かります。

Step 1 で見つけた unique_owner_pet_name 制約とも、ここで線がつながりました。

7-3. GitHub/GitLabのPRを見る(議論が残っている場所)

コミットメッセージより、PRの議論のほうが情報量が多いことが多いです。

gh pr list -R spring-projects/spring-petclinic --state merged --limit 5
2577	i18n: add Hindi translation	...	MERGED	2026-07-16T17:14:54Z
2573	Prevent duplicate pet creation with similar name for the same owner under concurrent requests	...	MERGED	2026-07-13T19:55:18Z
2559	Add WelcomeController test, fix deprecated SerializationUtils, add Pe…	...	MERGED	2026-07-02T16:43:01Z
2537	Add test for updating pet with same name	...	MERGED	2026-06-18T12:24:09Z
2536	Add test coverage for duplicate pet name validation on update	...	MERGED	2026-06-17T08:31:16Z

(3列目のブランチ名は ... に省略しています)

PR #2573 のタイトルには「concurrent requests(並行リクエスト下で)」とあります。コミットメッセージだけでは分からなかった「なぜ並行性テストが追加されたか」の答えがここにありました。自社のリポジトリなら、gh pr view <番号> --comments でレビューのやりとりまで読めます。

7-4. git blame で「この1行の理由」を辿る

コードを読んでいて「なんだこの条件は?」となったら、git blame です。

git blame -L 58,63 --date=short -- src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java
4e1f8740 (Dave Syer 2020-01-03 58)
4e1f8740 (Dave Syer 2020-01-03 59) 	@InitBinder
4e1f8740 (Dave Syer 2020-01-03 60) 	public void setAllowedFields(WebDataBinder dataBinder) {
c7ee1704 (Dave Syer 2026-04-20 61) 		dataBinder.setDisallowedFields("id", "*.id");
4e1f8740 (Dave Syer 2020-01-03 62) 	}
4e1f8740 (Dave Syer 2020-01-03 63)

61行目だけ2026年に変わっています。そのコミットを見ると:

git show c7ee170 -- src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java

差分の該当箇所を抜き出すと、たった1行です。

 	@InitBinder
 	public void setAllowedFields(WebDataBinder dataBinder) {
-		dataBinder.setDisallowedFields("id");
+		dataBinder.setDisallowedFields("id", "*.id");
 	}

コミットメッセージは "Be more careful with allowed fields in binders"。ネストしたオブジェクトの id も外部から書き換えられないようにした、というセキュリティ上の意図が読めます。この1行を「なんとなく冗長だから」と消すと脆弱性を作り込みます。

blameの必須オプション2つ

素の blame は、フォーマット修正やファイル移動で履歴が途切れて「全部同じ人が同じ日に書いた」ように見えることがあります。次の2つを付けてください。

  • -w:空白の変更を無視
  • -C:ファイル間のコード移動・コピーを追跡
git blame -L 58,63 --date=short -w -C -- src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java
521d01db src/main/java/.../web/FindOwnersForm.java    (Costin Leau 2009-11-25 58)
521d01db src/main/java/.../web/FindOwnersForm.java    (Costin Leau 2009-11-25 59) 	@InitBinder
521d01db src/main/java/.../web/FindOwnersForm.java    (Costin Leau 2009-11-25 60) 	public void setAllowedFields(WebDataBinder dataBinder) {
c7ee1704 src/main/java/.../owner/OwnerController.java (Dave Syer   2026-04-20 61) 		dataBinder.setDisallowedFields("id", "*.id");
521d01db src/main/java/.../web/FindOwnersForm.java    (Costin Leau 2009-11-25 62) 	}
521d01db src/main/java/.../web/FindOwnersForm.java    (Costin Leau 2009-11-25 63)

同じ行が、2009年の FindOwnersForm.java から移動してきた ことまで分かりました。オプションなしの結果(2020年)とは全く違う答えです。

7-5. 「いつこの概念が入ったか」を探す(pickaxe)

特定の文字列が追加・削除されたコミットだけを抽出できます。設定キー・フラグ名・エラーメッセージの出自を追うのに強力です。

git log --oneline -S 'setDisallowedFields' -- src/main/java
c7ee170 Be more careful with allowed fields in binders
e9b6aff Merged Pet Controllers into a single one
0fe4793 improvements to Owner Controller
521d01d SPR-6447 SPR-6448 + commit the gross of the files + added maven pom

この機構に触れた4コミットが、時系列で全部出ました。

7-6. 誰に聞けばいいかを特定する

読んでも分からないときに聞く相手を、勘ではなくデータで決めます。

git shortlog -sn HEAD --no-merges -- src/main/java/org/springframework/samples/petclinic/owner/PetController.java | head -6
     9	Dave Syer
     4	YiXuan Ding
     3	Patrick Baumgartner
     3	bijomutta
     2	Stephane Nicoll
     1	Antoine Rey

git shortlog はリビジョンを省略すると標準入力を待って 固まったように見えます。パス指定だけで使うときは、上のように HEAD を明示してください(git shortlog -sn HEAD -- <path>)。

質問するときは「この処理が分かりません」ではなく「このファイルの◯◯を触るタスクを持っています。git logを見ると△△さんが直近で変更されているようなので、◯◯の意図だけ確認させてください」と持っていく。 相手の負荷が段違いに下がり、答えも具体的になります。

7-7. リポジトリの「年輪」を見る

git log --date=format:'%Y' --pretty=format:'%ad' | sort | uniq -c | tail -8
  43 2019
  51 2020
  46 2021
  57 2022
  49 2023
  79 2024
  56 2025
  23 2026

コミット数の年次推移から、活発な時期と停滞期が分かります。実務では 「3年前に大量コミットがあり、その後ほぼ止まっている」 といった形で現れることが多く、それは「主要メンバーが抜けた時期」とほぼ一致します。読解の心構えが変わります。


8. Claude Code で読解を加速する ― ただし検証は必須

コードリーディングは、AIエージェントの得意分野そのものです。探索・要約・関連ファイルの列挙 は人間より速い。一方で、存在しないメソッドやそれらしい嘘の説明を返す ことがあります。読解フェーズでのハルシネーションは「間違った前提で実装する」に直結するので、扱い方に規律が要ります。

8-1. まずは読み取り専用で起動する

新しい現場のリポジトリでいきなり書き込みを許可しないでください。Plan モードなら、探索と計画だけを行い、変更は承認するまで適用されません。

claude --permission-mode plan

8-2. 効くプロンプト(読解フェーズ用)

以下は プロンプトの例 です(AIの出力例ではありません。出力は毎回変わるため、この記事には載せません)。

① 外形把握を代行させる

このリポジトリの外形を把握したい。以下を、根拠となるファイルパスと行番号を必ず付けて答えて。
1. 技術スタックとバージョン(依存定義ファイルから)
2. ディレクトリ構成とレイヤ分割の方針
3. 主要なDBテーブルとその関係(スキーマ/マイグレーションから)
4. CIで何が実行されているか
推測が入る場合は「推測」と明記して。ファイルに書き込みはしないで。

② 縦に1本通すのを手伝わせる(この記事の本題)

GET /owners のリクエストが、入口からDBアクセス、レスポンス生成までに通るコードを
順番に列挙して。各ステップについて「ファイルパス:行番号」と、そのステップで何をしているかを1行で。
実装が見つからない箇所(フレームワークが生成している等)は、その旨を明記して。

③ 変更の影響範囲を洗い出させる

ページサイズ(1ページあたりの表示件数)を設定ファイルから変更できるようにしたい。
変更が必要なファイルと、既存テストのうち影響を受けるものを、ファイルパス:行番号で列挙して。
まだコードは変更しないで。

8-3. ハルシネーション対策の鉄則3つ

AIの説明は「仮説」であって「事実」ではありません。 読解フェーズでは次の3つを必ず守ってください。

鉄則1:必ず「ファイルパス:行番号」を出させ、その場で自分で開く

行番号まで出させると、検証コストがほぼゼロになります。そして 実際に開くまでは信じない。存在しないファイルや、ズレた行番号が返ることは普通にあります。

鉄則2:固有名詞は grep で裏を取る

AIが「pageSizeapplication.properties で設定されています」と答えたとします。事実かどうかは一発で分かります。

grep -rn "pageSize" src/main/java src/main/resources
src/main/java/.../vet/VetController.java:60:		int pageSize = 5;
src/main/java/.../vet/VetController.java:61:		Pageable pageable = PageRequest.of(page - 1, pageSize);
src/main/java/.../owner/OwnerController.java:131:		int pageSize = 5;
src/main/java/.../owner/OwnerController.java:132:		Pageable pageable = PageRequest.of(page - 1, pageSize);

.properties には一切現れません。この回答は嘘だった と即座に判定できます。「メソッド名・設定キー・テーブル名・エラーメッセージ」が出てきたら、必ずgrepしてください。10秒で終わります。

鉄則3:仕様の最終確認はテストと実行結果で行う

「この処理は前方一致です」というAIの説明を信じる前に、テストコードを読むか、実際に動かす。テストと実行結果だけが一次情報 です。AIの要約もコメントも設計書も、すべて二次情報です。

8-4. 使いどころの線引き

用途 AIに任せる 自分でやる
「このパターンのコードがどこにあるか」の探索
大量ファイルの一次要約
命名規約・ディレクトリ方針の推定
1本通したトレースの最終確認
業務ルールの確定 ⭕(テスト・スキーマ・有識者)
変更を入れるかどうかの判断

AIは 「読む場所を絞り込む装置」 として使うのが最も費用対効果が高いです。「読まなくていい範囲を確定させる」のが目的であって、「読まずに済ませる」のが目的ではありません。

(Claude Code の設定やプロンプトの型そのものについては、別記事のClaude Code実践Tips集Claude Codeでの「仕様駆動開発」完全ガイド も参考にしてください)


9. 読んだ内容の残し方 ― 次に入る人への資産にする

読解は揮発します。3日後の自分は、今日読んだことの半分を忘れています。 そして半年後、次に入る誰かが同じ苦労を最初からやり直します。

残し方は凝らなくて大丈夫です。「トレースメモ」を1操作につき1枚 作るだけで十分な資産になります。

9-1. トレースメモのテンプレート

# トレース:飼い主を苗字で検索する

- 対象: GET /owners?lastName=xxx
- 調査日: 2026-07-29 / 調査時のコミット: f182358

## 経路
| # | レイヤー | 実体(ファイル:行) | メモ |
|---|---------|-------------------|------|
| 1 | Controller | owner/OwnerController.java:94 | processFindForm |
| 2 | 入力防御 | owner/OwnerController.java:60 | id と *.id をバインド禁止 |
| 3 | ページング | owner/OwnerController.java:131 | pageSize=5 がハードコード |
| 4 | Repository | owner/OwnerRepository.java:45 | 実装なし(Spring Dataが生成) |
| 5 | View | templates/owners/ownersList.html:20 | モデル名 listOwners |

## 分かった仕様
- 苗字は「前方一致」(ClinicServiceTests#shouldFindOwnersByLastName で保証)
- ヒット0件 → findOwners画面にエラー / 1件 → 詳細へリダイレクト / 複数 → 一覧
- 1ページ5件。OwnerControllerとVetControllerの2箇所に同じ値がある

## 引っかかった点 / 未解決
- Repositoryの実装が無く探して30分溶かした → Spring Dataのクエリメソッド生成
- pageSize が2箇所ある理由は未確認(設定化されていないのは意図的? 要確認)

## 聞く相手
- owner配下の直近変更者(git shortlog -sn HEAD -- <path> で確認)

「引っかかった点 / 未解決」の欄が、一番価値があります。 ここは書いた本人にしか書けず、しかも次に入る人が確実に同じ場所で引っかかります。

9-2. 図は1枚だけ、簡素に

図は作り込むほど陳腐化します。Mermaidでこの程度で十分です(コードと同じリポジトリに置くと、変更時に一緒に直されやすくなります)。

9-3. 置き場所

置き場所 向き / 不向き
リポジトリ内 docs/onboarding/ ◎ コードと一緒にレビューされ、腐りにくい。まずここを検討
チームのWiki / Confluence ○ コード外の運用知識向き。ただし更新されにくい
自分のローカルメモのみ △ 資産にならない。せめてチームに共有を

入って2週間の人が書くメモは、チームで一番価値があります。 ベテランは「知っていて当然」のことを書けません。分からなかったことを覚えているうちに書くのが、最大の貢献になります。実際、初日〜2週間で作ったオンボーディングメモをPRで出すと、それ自体が「この人は現場を良くする人だ」という評価につながります。


10. やってはいけないこと7つ

# NG行動 なぜダメか 代わりにやること
1 いきなりリファクタを提案する 背景(歴史・制約・過去の事故)を知らずに触ると事故る。信頼も失う まず git log で経緯を読む。提案は3ヶ月後でも遅くない
2 命名や書き方にケチをつける 規約や過去の議論の結果であることが多い。技術以前に人間関係を壊す checkstyle等の規約設定を先に読む。それでも気になるなら質問形式で
3 全体を理解してから着手しようとする 永遠に終わらない。進捗が出ず信頼を失う 1本通したら着手。残りは着手しながら読む
4 設計書だけ読んでコードを読まない 設計書は更新が止まっている前提で扱うべき 設計書は「地図」、コードとテストが「現地」
5 AIの要約を検証せず信じる 存在しないメソッド・逆の説明が混ざる ファイルパス:行番号を出させ、grepで裏取り
6 分からないことを1人で3時間抱える 3時間は自分だけの損失ではなく、チームの損失 30分ルール。gitで担当者を特定して具体的に聞く
7 「このコード汚いですね」と口に出す 一発で敵を作る。しかも自分も半年後には同じコードを書く 事実だけ言う(「この分岐の意図が読み取れませんでした」)

特に 1と7は、技術力ではなく信頼の問題 です。新しい現場での最初の1ヶ月は、コードを読む力と同じくらい「どう振る舞うか」が見られています。


11. 初日〜2週間のチェックリスト

そのままコピーして使えるように、時系列でまとめました。

初日

  • クローンして、ビルドとテストがローカルで通ることを確認する(ここで詰まるなら最優先で解決)
  • git ls-files | wc -l と拡張子ランキングで規模と主戦場を把握
  • 依存定義でフレームワークとバージョンを確認
  • DBスキーマ/マイグレーションを読み、主要テーブルとER関係をメモ
  • CI設定を読み、品質ゲート(何が落ちると失敗するか)を把握
  • アプリをローカルで起動し、画面を実際に触ってみる

2〜3日目

  • エントリポイントを一覧化(grep -rEn "@(Get|Post)Mapping" 等)
  • 中心的な操作を1つ選び、縦に1本通す
  • そのトレースをテストコードで裏取りする
  • トレースメモを1枚書く

1週目後半

  • git log でホットスポットを特定し、上位3ファイルをざっと読む
  • git log --first-parent で直近の関心事を把握
  • 担当タスクの影響範囲を grep で確定させる
  • 小さな変更を1つ入れて、テストを通してPRを出す(ここが本当のゴール

2週目

  • 2本目・3本目のトレース(1本目との差分だけ読む)
  • 引っかかった点をまとめて docs/onboarding/ にPRを出す
  • 「まだ理解していない領域」のリストを作り、上長と優先順位をすり合わせる

最後の項目が地味に重要です。 「分かっていないことを、分かっている」状態にして共有すると、周りは安心して仕事を任せられます。


まとめ

  • 「全部読む」は失敗する。 読む量を減らす設計を先に立てる
  • 順序は 外形把握 → エントリポイント → 縦に1本通す → 変更対象の周辺だけ深掘り
  • 外形把握では DBスキーマとテストコードとCI設定 が最も嘘をつかない
  • 横に広く読まない。縦に1本通す。 1本通すと「型」が手に入り、2本目以降は差分だけで済む
  • コードに書いていない「なぜ」は git にある。git log(ホットスポット)、git blame -w -C(意図)、git log -S(出自)、git shortlog(聞く相手)
  • AIは 読む場所を絞り込む装置。出力には必ず ファイル:行 を出させ、grepとテストで裏を取る
  • 読んだことは1操作1枚のトレースメモに残す。引っかかった点こそが次の人への資産
  • リファクタ提案と命名への指摘は、経緯を読むまで我慢する

新しい現場に入るたびに毎回ゼロから消耗するか、手順として持っておくかで、立ち上がりの速度は大きく変わります。この記事の型が、そのまま使えるものになっていれば嬉しいです。


参考


エンジニア採用や開発のご相談は、コーポレートサイトからお気軽にどうぞ。
株式会社Good Lab コーポレートサイト

@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?