AI時代のジュニア開発者:何を学べば取って代わられないか

1. 概要

コード生成ツールは今や、コントローラー、エンティティ、またはサンプルテストをわずか数秒で書くことができます。だから、新人開発者の多くは、ジュニアというポジションが消えるのではないかと心配しています。この記事は未来を予測しません。より現実的な質問に焦点を当てます:コードを書くことがもはや難しくない場合、どのスキルを磨くべきでしょうか?

この記事を読むと、優先すべきスキルのリストが得られます。各スキルには、すぐに実行できるJavaの例が付いています。また、コード補助をいつ使用しないべきかも明確に述べます。

2. この質問が今注目されている理由

以前は、ジュニア開発者は通常、CRUD書き込み、DTOマッピング、小さなバグ修正など、反復的な作業を割り当てられました。それは彼らが職業を学んだ方法でもありました。現在、これらのほとんどの作業は自動生成できます。コード作成のコストが急激に低下しました。

間違ったコストは依然として同じです。生成されたコードはコンパイルでき、見た目が良く、単純なテストに合格するかもしれませんが、微妙な場所ではまだ間違っています:通貨、concurrency、トランザクション、セキュリティ。本番環境にコードをデプロイする責任は依然としてあなたです。

したがって、価値は「コードが書ける」から3つのことへシフトします:

  • 評価する - コードが正しいか間違っているか、そしてなぜかを判断する。
  • 検証する - テストを使って動作を検証する。
  • 理解する - コードが動作するランタイム、フレームワーク、システムを理解する。

以下のセクションでは、各スキルを順番に説明します。

3. コード作成よりもコードを読んでレビューする方がはるかに価値がある

最初のスキルは、一見良さそうに見えるコードの一部を見て、それが間違っていることを検出することです。あなたがコード補助に注文合計を計算する関数を書くよう依頼し、以下を受け取ったと仮定します:

import java.util.List;

record OrderLine(String product, double price, int quantity) {}

record Order(List<OrderLine> lines) {
    double total() {
        return lines.stream()
                .mapToDouble(l -> l.price() * l.quantity())
                .sum();
    }
}

public class Main {
    public static void main(String[] args) {
        var order = new Order(List.of(
                new OrderLine("Bút", 0.1, 1),
                new OrderLine("Vở", 0.2, 1)));
        System.out.println(order.total());
    }
}

コードは短くて読みやすいですが、結果は以下の通りです:

0.30000000000000004

doubleは浮動小数点形式で数値を表します。したがって、0.1と0.2は正確に保存されません。通貨では、この誤差は数千のトランザクションに蓄積して、照合エラーになります。BigDecimalを使った修正版:

import java.math.BigDecimal;
import java.util.List;

record OrderLine(String product, BigDecimal price, int quantity) {
    BigDecimal subtotal() {
        return price.multiply(BigDecimal.valueOf(quantity));
    }
}

record Order(List<OrderLine> lines) {
    BigDecimal total() {
        return lines.stream()
                .map(OrderLine::subtotal)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

注意すべき3つのポイントがあります:

  • priceはBigDecimal型です。new BigDecimal(0.1)ではなく、new BigDecimal("0.1")で値を作成すべきです。
  • subtotal()は、doubleを経由せずに価格に数量を乗算します。
  • reduceはBigDecimal.ZEROから始まるため、空の注文はエラーではなく0を返します。

これはまさにツールがあなたを代替できないスキルです。データ型、境界値、空のケースについて質問を提起することができる必要があります。レビューするときは、自問してください:「どの入力がこの関数を間違わせるか?」

4. テストを仕様書として書く

テストがなければ、受け取ったばかりのコードが正しいかどうかわかりません。テストはまた、あなたが何を望んでいるかを説明する最も明確な方法です。説明が明確なほど、生成されたコードまたは同僚が書いたコードを検証するのが簡単になります。

Spring Bootプロジェクトでは、JUnit 5とAssertJを取得するための単一の依存関係が必要です:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

上記のOrderクラスのテスト:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class OrderTest {

    @Test
    @DisplayName("小数点を正確に合計する")
    void totalIsExactForDecimals() {
        var order = new Order(List.of(
                new OrderLine("Bút", new BigDecimal("0.1"), 1),
                new OrderLine("Vở", new BigDecimal("0.2"), 1)));

        assertThat(order.total()).isEqualByComparingTo("0.3");
    }

    @Test
    @DisplayName("空の注文の合計は0である")
    void emptyOrderTotalIsZero() {
        var order = new Order(List.of());

        assertThat(order.total()).isEqualByComparingTo(BigDecimal.ZERO);
    }

    @Test
    @DisplayName("合計金額は数量で正しく乗算される")
    void totalMultipliesByQuantity() {
        var order = new Order(List.of(
                new OrderLine("Sách", new BigDecimal("12.50"), 3)));

        assertThat(order.total()).isEqualByComparingTo("37.50");
    }
}

学ぶべき詳細がいくつかあります:

  • @DisplayNameはビジネスルールを読み取り可能な言語で記録します。テストレポートを読むことはシステムが何をすべきかを理解することです。
  • isEqualByComparingToは値によって比較します。new BigDecimal("0.3").equals(new BigDecimal("0.30"))はスケールが異なるためfalseを返します。これは非常に一般的なエラーです。
  • 各テストは1つの動作をチェックします:通常のケース、空のケース、数量を乗算するケース。

注意: ツールにコードとテストの両方を生成させてから、単にグリーンを見てから安心することはしないでください。両方が要件を誤って理解すると、テストは依然としてパスします。少なくともビジネスルールについては、自分自身でテストを書くべきです。

5. ランタイムを理解する:concurrencyは推測では予測できない

ほとんどのハードな問題は、複数のスレッドが同時に実行されるときにのみ現れます。Springでは、すべてのbeanはデフォルトではシングルトンです。つまり、すべてのリクエストが同じインスタンスを共有します。以下は無害に見えます:

import java.util.concurrent.Executors;

class ReportService {
    private int processedCount = 0;

    void process() {
        processedCount++;
    }

    int processedCount() {
        return processedCount;
    }
}

public class CounterDemo {
    public static void main(String[] args) {
        var service = new ReportService();

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10_000; i++) {
                executor.submit(service::process);
            }
        }

        System.out.println("Đã xử lý: " + service.processedCount());
    }
}

Java 21のExecutors.newVirtualThreadPerTaskExecutor()は、各タスクごとに1つのvirtual threadを作成します。try-with-resourcesブロックはすべてのタスクが完了するのを待ってから閉じます。10000が出力されることを期待していますが、実際にはより小さい数値が表示され、実行するたびに異なります:

Đã xử lý: 9871

理由はprocessedCount++がアトミック操作ではないためです。読み取り、加算、書き込みの3つのステップで構成されています。2つのスレッドが同じ値を読み取ることができ、1つの加算を失わせます。修正版:

import java.util.concurrent.atomic.AtomicInteger;

class ReportService {
    private final AtomicInteger processedCount = new AtomicInteger();

    void process() {
        processedCount.incrementAndGet();
    }

    int processedCount() {
        return processedCount.get();
    }
}

今回の結果は常に10000です。より広い教訓は:シングルトンbeanはリクエストごとに変わる状態を保持すべきではないです。

@Serviceを読むときは、finalではないフィールドを探すべきです。そこは通常、バグが隠れている場所です。

java.util.concurrentのすべてのAPIを暗記する必要はありません。しかし、race condition、visibility、lockなどの概念を理解して、見たときにバグを認識する必要があります。

6. フレームワークを理解する:「魔法」がどのように動作するかを知る

Springはアノテーションを使ってコードを書くのに役立ちますが、多くのことが舞台裏で起こります。基盤となるメカニズムを理解しないと、驚かされます。@Transactionalでのself-invocationの古典的な例:

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class PaymentService {

    private final PaymentRepository paymentRepository;

    public PaymentService(PaymentRepository paymentRepository) {
        this.paymentRepository = paymentRepository;
    }

    public void payAll(List<Long> paymentIds) {
        paymentIds.forEach(this::pay);
    }

    @Transactional
    public void pay(Long paymentId) {
        var payment = paymentRepository.findById(paymentId).orElseThrow();
        payment.markPaid();
        paymentRepository.save(payment);
    }
}

見た目上、payへの各呼び出しはトランザクション内で実行されます。実際にはそうではありません。Springはbeanを囲むプロキシを通じて@Transactionalを適用します。payAllがthis.pay(...)を呼び出すとき、呼び出しはオリジナルのオブジェクトに直接行き、プロキシをスキップします。したがって、トランザクションは開きません。

2つの一般的な修正方法があります:

  • 外部から呼び出されるメソッド自体に@Transactionalを配置します。つまり、バッチ全体が1つのトランザクション内で実行されるようにしたい場合はpayAllです。
  • payを別のbeanに分割します。例えばSinglePaymentProcessorで、その後PaymentServiceに注入して、呼び出しをプロキシを通すようにします。

プロキシメカニズムを理解することで、多くの他の現象を説明できます:@Asyncが非同期に実行されない、@Cacheableがキャッシュしない、またはprivateメソッドのアノテーションが無視される。これは、任意の提案ツールよりも迅速にデバッグするのに役立つ基礎知識です。

7. システムを観察する:推測ではなく

ローカルマシンで正しく実行されるコードは、本番環境で正しく実行されるわけではありません。ログ、メトリック、ヘルスチェックを読むことができるジュニアは、機能を書くだけの人より価値があります。Spring Bootでは、Actuatorから始めます:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

次に、必要なエンドポイントを設定します:

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      show-details: when-authorized

logging:
  level:
    root: INFO
    org.springframework.transaction: DEBUG

各部分の意味:

  • exposure.includeは3つの必要なエンドポイントのみを開きます。"*"ですべてを開くことは一般的なセキュリティエラーです。
  • show-details: when-authorizedは、認証されたユーザーのみにヘルス詳細を表示します。
  • org.springframework.transactionのDEBUGをオンにすると、トランザクションがいつ開いてコミットされるかをログで確認できます。これはセクション6のself-invocationエラーを直接検証する方法です。

クイックチェック:

curl http://localhost:8080/actuator/health
{"status":"UP"}

ここで開発すべき習慣は:エラーが発生したときに、修正する前にログ、メトリック、スタックトレースから証拠を収集することです。提案されたコードの各部分をランダムに試すまで、エラーが消えるまで試さないでください。

8. コード補助を正しく使う方法とそれを使うべきでないとき

コード補助は便利なツールです。これを避けることは、あなたがより進歩するのに役立ちません。重要なのは、意思決定者の役割を保つことです。合理的なプロセスは4つのステップで構成されています:

  1. 最初に、セクション4のように要件とメインテストを自分で書きます。
  2. ツールに実装部分の提案をお願いします。
  3. 各行を読みます。1行も説明できない場合は、マージしないでください。
  4. テストを実行し、レビュー中に思いついたエッジケースのテストを追加します。

次の場合には、コード補助にタスクを委任すべきではない、または特に注意が必要です:

  • セキュリティコード:認証、認可、暗号化、トークン処理。ここでの小さなエラーは大きな影響を引き起こし、間違ったコードは正しいコードと非常に似ています。
  • 新しい概念を学んでいるとき。ツールが最初のconcurrencyエクササイズを書く場合、学習の最も重要な部分を見落とします。
  • 機密データ:承認されていないツールにシークレット、顧客情報、または内部コードを貼り付けないでください。
  • 固有のビジネスロジック - ツールが文脈を持たない場合、例えば税務ルールや独自の割引ポリシーなど。最初にビジネス専門家に確認してください。

注意: 危険な兆候は、コードを理解する速度より速くマージしていることです。その場合、エンジニアではなく、コードを転送しているだけです。

9. ジュニア向けの推奨ロードマップ

すべてを一度に学ぶ必要はありません。合理的な順序は次のとおりです:

  • 言語の基礎:データ型、コレクション、equals/hashCode、例外、基本的なconcurrency。
  • テスト:JUnit 5、AssertJ、行動に基づくテスト命名、エッジケーステスト。
  • フレームワークの内部:dependency injection、beanライフサイクル、プロキシ、トランザクション。
  • データ:実際のSQL、インデックス、トランザクション分離、JPAを使用するときのN+1クエリの問題。
  • 運用:ログを読む、メトリック、ヘルスチェック、本番環境に近い環境でのデバッグ。
  • コミュニケーション:曖昧な要件を明確な受け入れ基準に変換し、レビューしやすいpull requestの説明を書く。

最後のスキルはしばしば過小評価されます。正しい質問をできる人、例えば「空の注文はどうなる?」または「2つのリクエストが同時に来たらどうする?」は常に必要です、誰がコードを書いたか、どのツールを使ったかに関わらず。

10. 結論

この記事では、ジュニア開発者の価値がコード入力速度から、評価、検証、システム理解能力にシフトしているのが見られます。例は、「見た目が良い」コードが陥る可能性のあるエラーを示しています:通貨を含むdoubleの誤差、シングルトンbeanのrace condition、self-invocationのため無視される@Transactional。また、推測するのではなく、テストとActuatorを使って自分で検証する方法も見ました。

コード補助を速く書くが間違いやすい同僚として使用してください。その速度を活用しますが、常にレビューし、テストを書き、マージする前に各行をよく理解してください。それがあなたが取って代わられず、チームが頼りになる人になる方法です。

キャリアジュニア開発者aijavaテスト