Java Tips | 日付・時間:Clock差し替え

Java Java
スポンサーリンク

なぜ「Clock差し替え」が業務ユーティリティとして重要なのか

日付・時間を扱うコードを書くとき、多くの人が最初にやるのは LocalDate.now()Instant.now() をそのまま呼ぶことです。
小さなサンプルならそれで十分ですが、業務システムになると次のような問題が出てきます。

テストで「特定の日付・時刻」を再現しづらい。
締め処理や有効期限など、「日付に依存するロジック」のテストが不安定になる。
本番と検証環境で「今」が違うせいで、挙動が変わってしまう。

これを一気に解決してくれるのが、Clock を使った「Clock差し替え」です。
「今」を直接聞くのではなく、「今を教えてくれる時計(Clock)」を注入しておき、テストや環境ごとに差し替えられるようにする考え方です。


Clock の基本を押さえる

Clock は「今を教えてくれるオブジェクト」

Java の Clock は、「現在時刻を取得するための戦略」を表すインターフェースのような存在です。
LocalDate.now()Instant.now() は、内部的に「システムのデフォルト Clock」を使っていますが、実は自分で Clock を渡すオーバーロードもあります。

import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;

public class ClockBasic {

    public static void main(String[] args) {
        Clock systemClock = Clock.systemDefaultZone();
        Instant now = Instant.now(systemClock);
        LocalDate today = LocalDate.now(systemClock);

        System.out.println(now);
        System.out.println(today);

        Clock tokyoClock = Clock.system(ZoneId.of("Asia/Tokyo"));
        LocalDate tokyoToday = LocalDate.now(tokyoClock);
        System.out.println(tokyoToday);
    }
}
Java

ここで重要なのは、「now() に直接頼るのではなく、“どの Clock を使うか”を自分で選べる」ということです。
この「Clock を渡す」という一手間が、テストと設計の自由度を一気に上げてくれます。


「Clock差し替え」を前提にした日付ユーティリティの書き方

直接 now() を呼ばず、Clock をコンストラクタで受け取る

例えば、「今日が締め切りを過ぎているかどうか」を判定するユーティリティを考えます。
悪い例はこうです。

import java.time.LocalDate;

public class DeadlineBad {

    public static boolean isExpired(LocalDate deadline) {
        LocalDate today = LocalDate.now(); // 直接 now を呼んでいる
        return today.isAfter(deadline);
    }
}
Java

これだと、「2025-03-31 を今日としてテストしたい」と思っても、どうにもなりません。
そこで、Clock を注入する形に変えます。

import java.time.Clock;
import java.time.LocalDate;

public class DeadlineChecker {

    private final Clock clock;

    public DeadlineChecker(Clock clock) {
        this.clock = clock;
    }

    public boolean isExpired(LocalDate deadline) {
        LocalDate today = LocalDate.now(clock);
        return today.isAfter(deadline);
    }
}
Java

使い方の例です(本番想定)。

DeadlineChecker checker = new DeadlineChecker(Clock.systemDefaultZone());
boolean expired = checker.isExpired(LocalDate.of(2025, 3, 31));
Java

ここで深掘りしたいのは、「時間に依存するクラスは、必ず Clock を持つ」という設計パターンです。
これを徹底するだけで、「時間依存ロジックのテスト可能性」が劇的に上がります。


テストで Clock を差し替える具体例

Clock.fixed を使って「テスト用の今日」を作る

テストでは、「今が 2025-03-31 だと仮定して動かしたい」といったケースがよくあります。
Clock.fixed を使うと、任意の Instant を「固定された現在時刻」として扱えます。

import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;

public class DeadlineCheckerTest {

    public static void main(String[] args) {
        Instant fixedInstant = Instant.parse("2025-03-31T10:00:00Z");
        Clock fixedClock = Clock.fixed(fixedInstant, ZoneId.of("Asia/Tokyo"));

        DeadlineChecker checker = new DeadlineChecker(fixedClock);

        LocalDate deadline = LocalDate.of(2025, 3, 30);
        System.out.println(checker.isExpired(deadline)); // true と期待

        LocalDate deadline2 = LocalDate.of(2025, 3, 31);
        System.out.println(checker.isExpired(deadline2)); // false or true を仕様に合わせて確認
    }
}
Java

ここでのポイントは、「テストコード側で“今”を自由に決められる」ことです。
締め処理、月末処理、有効期限切れなど、「日付が境界になるテスト」を、安定して何度でも再現できます。


アプリ全体で Clock をどう配るか

DI(依存性注入)で Clock を共有するイメージ

現実のアプリでは、日付・時間を使うクラスがたくさんあります。
それぞれがバラバラに Clock.systemDefaultZone() を new していると、
テスト時に差し替えるのが大変になります。

そこで、「アプリケーション全体で使う Clock を一つ決めて、それを注入して回る」という設計が有効です。

シンプルな例として、「Clock を提供するファクトリ」を用意します。

import java.time.Clock;

public class AppClock {

    private static Clock clock = Clock.systemDefaultZone();

    public static Clock get() {
        return clock;
    }

    public static void set(Clock newClock) {
        clock = newClock;
    }
}
Java

業務クラス側では、こう使います。

import java.time.LocalDate;

public class SomeService {

    public LocalDate today() {
        return LocalDate.now(AppClock.get());
    }
}
Java

テスト時には、次のように差し替えられます。

AppClock.set(Clock.fixed(
        Instant.parse("2025-03-31T00:00:00Z"),
        ZoneId.of("Asia/Tokyo")
));
Java

本格的な DI コンテナ(Spring など)を使う場合は、Clock を Bean として定義し、コンストラクタインジェクションで配るのが王道です。
大事なのは、「Clock.systemDefaultZone() をあちこちで new しない」というルールです。


Clock 差し替えとタイムゾーン・テストの関係

タイムゾーンを変えたテストも簡単になる

Clock は「今」と同時に「どのタイムゾーンか」も持っています。
Clock.system(ZoneId.of("America/New_York")) のように作れば、「ニューヨーク時間の今」を基準にしたロジックをテストできます。

例えば、「ユーザーごとにタイムゾーンが違うシステム」で、
「ユーザーのタイムゾーンで日付が変わるタイミング」をテストしたい場合、
ユーザーごとに異なる Clock を渡す設計にしておくと、とても扱いやすくなります。

import java.time.Clock;
import java.time.LocalDate;
import java.time.ZoneId;

public class UserDateService {

    private final Clock clock;

    public UserDateService(Clock clock) {
        this.clock = clock;
    }

    public LocalDate todayForUser() {
        return LocalDate.now(clock);
    }

    public static void main(String[] args) {
        UserDateService tokyoUser = new UserDateService(Clock.system(ZoneId.of("Asia/Tokyo")));
        UserDateService nyUser    = new UserDateService(Clock.system(ZoneId.of("America/New_York")));

        System.out.println(tokyoUser.todayForUser());
        System.out.println(nyUser.todayForUser());
    }
}
Java

ここで深掘りしたいのは、「Clock を差し替える設計は、そのまま“タイムゾーンを差し替える設計”にもなる」ということです。
グローバル対応のシステムでは、ほぼ必須の考え方になります。


セキュリティ・運用の観点から見た Clock 差し替え

「システム時刻の変更」に振り回されない設計

運用現場では、サーバの時刻を調整することがあります。
NTP の再設定、誤設定の修正、仮想環境のスナップショット復元などです。

now() を直接使っていると、こうした「時刻のジャンプ」にアプリがそのまま影響を受けます。
一方、Clock を経由していれば、「アプリとしてどの Clock を信じるか」を制御できます。

例えば、「アプリ内で独自の“論理時間”を進める Clock」を用意し、
外部の時刻変更の影響を受けにくくする、といった設計も可能です。

テスト環境で「未来日」「過去日」を安全に再現できる

セキュリティや監査の観点では、「有効期限切れ」「長期運用後の状態」などをテストすることが重要です。
Clock 差し替えを前提にしておけば、「10年後の状態」を簡単に再現できます。

これは、「本番でしか起きない日付バグ」を事前に潰すための強力な武器になります。
日付・時間に依存するロジックほど、Clock 差し替えを前提に設計しておく価値が高いです。


まとめ:Clock差し替えユーティリティで身につけてほしい感覚

Clock 差し替えは、「now() を直接呼ばず、“今を教えてくれるオブジェクト”を注入する」という、たった一つの習慣です。
しかし、その効果は非常に大きく、次のようなメリットをもたらします。

時間依存ロジックを、任意の日時で安定してテストできる。
タイムゾーンごとの挙動を、Clock の差し替えだけで再現できる。
システム時刻の変更や環境差に振り回されにくくなる。

もしあなたのコードの中に、LocalDate.now()Instant.now() があちこちに散らばっているなら、
それを一度「Clock を受け取るコンストラクタ」「アプリ共通の Clock プロバイダ」に置き換えられないか眺めてみてください。

それだけで、日付・時間まわりのテストが一気に楽になり、
“業務で長く運用できる”設計にぐっと近づきます。

タイトルとURLをコピーしました