Java Tips | 日付・時間:日付キャッシュ

Java Java
スポンサーリンク

日付キャッシュユーティリティは何のために必要か

「今日の日付」「今月の営業日一覧」「今年の祝日一覧」など、日付まわりの計算は、業務システムのあちこちで何度も呼ばれます。
しかも、多くの場合「同じ結果」が何度も必要になります。

例えば、1画面の中で「今月の営業日数」を3回計算している、
バッチ処理の中で「今年の祝日一覧」を何千レコード分のループの中で毎回作っている、
こういうコードは、動くけれど無駄が多いです。

そこで出てくるのが「日付キャッシュ」のユーティリティです。
一度計算した結果を覚えておき、同じ条件で再度呼ばれたときは「計算せずに返す」ことで、処理を軽く・安定させるための仕組みです。


まずは「今日の日付」をキャッシュするところから

LocalDate.now() をそのまま多用する危険

初心者がやりがちなのが、あちこちで LocalDate.now() を直接呼ぶことです。

import java.time.LocalDate;

public class TodayExample {

    public void doSomething() {
        LocalDate a = LocalDate.now();
        // 何か処理
        LocalDate b = LocalDate.now();
        // さらに処理
    }
}
Java

一見問題なさそうですが、長い処理の中で何度も now() を呼ぶと、「a と b が違う日付になる」可能性があります。
日付が変わるタイミング(深夜0時)をまたいだ場合などです。

業務ロジックとして「この処理は“同じ日”として扱いたい」のであれば、
「その処理の中で使う“今日”は1回だけ決めて、使い回す」方が安全です。

今日の日付をキャッシュするユーティリティ

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

public class TodayCache {

    private final Clock clock;
    private LocalDate cachedDate;

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

    public LocalDate today() {
        LocalDate now = LocalDate.now(clock);
        if (cachedDate == null || !cachedDate.isEqual(now)) {
            cachedDate = now;
        }
        return cachedDate;
    }
}
Java

使い方の例です。

import java.time.Clock;

public class TodayCacheExample {

    public static void main(String[] args) {
        TodayCache todayCache = new TodayCache(Clock.systemDefaultZone());

        LocalDate d1 = todayCache.today();
        LocalDate d2 = todayCache.today();

        System.out.println(d1 == d2);          // true(同じインスタンス)
        System.out.println(d1.isEqual(d2));    // true(同じ日付)
    }
}
Java

ここで深掘りしたいポイントは二つです。
一つ目は、「処理単位で“今日”を固定する」という発想です。
二つ目は、「Clock を注入しておくことで、テストしやすくしている」ことです。Clock.fixed を使えば、任意の日付を“今日”としてテストできます。


「重い日付計算」をキャッシュする

営業日一覧や祝日一覧は“重い計算”になりがち

例えば、「2025年の全営業日一覧」を作る処理を考えます。
1年分の日付をループし、土日や祝日を判定しながらリストを作るので、それなりにコストがかかります。

これを、画面の表示やバッチ処理の中で何度も計算していると、無駄が積み重なります。
しかも、同じ年の営業日一覧は、1回計算すれば結果は変わりません(祝日マスタが変わらない前提)。

そこで、「年ごとの営業日一覧をキャッシュする」ユーティリティを作ります。

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class BusinessDaysCache {

    private final BusinessDayRules rules;
    private final Map<Integer, List<LocalDate>> cache = new HashMap<>();

    public BusinessDaysCache(BusinessDayRules rules) {
        this.rules = rules;
    }

    public List<LocalDate> businessDaysOfYear(int year) {
        if (cache.containsKey(year)) {
            return cache.get(year);
        }
        List<LocalDate> list = computeBusinessDaysOfYear(year);
        cache.put(year, list);
        return list;
    }

    private List<LocalDate> computeBusinessDaysOfYear(int year) {
        LocalDate start = LocalDate.of(year, 1, 1);
        LocalDate end   = LocalDate.of(year, 12, 31);
        List<LocalDate> result = new ArrayList<>();
        LocalDate d = start;
        while (!d.isAfter(end)) {
            if (rules.isBusinessDay(d)) {
                result.add(d);
            }
            d = d.plusDays(1);
        }
        return result;
    }
}
Java

ここで重要なのは、「Map を使って“年 → 営業日一覧”を覚えている」ことです。
同じ年を再度要求されたときは、計算せずにキャッシュから返します。


キャッシュの“有効期限”をどう考えるか

いつまでキャッシュしてよいかは“業務ルール”次第

日付キャッシュで必ず考えなければいけないのが、「いつまでその結果を信用してよいか」です。

例えば、「今年の祝日一覧」は、祝日マスタが変われば結果も変わります。
「今日の日付」は、日付が変われば当然変わります。

つまり、「キャッシュの有効期限」を決めておかないと、
「古い結果をいつまでも使い続ける」危険があります。

今日の日付キャッシュなら、「日付が変わったら破棄する」で十分です。
祝日一覧や営業日一覧なら、「マスタ更新時にキャッシュをクリアする」「アプリ再起動時にクリアする」などの運用を決める必要があります。

シンプルな「日単位キャッシュ」の例

「その日だけ有効なキャッシュ」を作るイメージのコードです。

import java.time.Clock;
import java.time.LocalDate;
import java.util.function.Supplier;

public class DailyCache<T> {

    private final Clock clock;
    private final Supplier<T> loader;

    private LocalDate cachedDate;
    private T cachedValue;

    public DailyCache(Clock clock, Supplier<T> loader) {
        this.clock = clock;
        this.loader = loader;
    }

    public T get() {
        LocalDate today = LocalDate.now(clock);
        if (cachedDate == null || !cachedDate.isEqual(today)) {
            cachedValue = loader.get();
            cachedDate = today;
        }
        return cachedValue;
    }
}
Java

使い方の例です。

DailyCache<List<LocalDate>> todayBusinessDaysCache =
        new DailyCache<>(Clock.systemDefaultZone(), () -> {
            // ここで「今日の営業日一覧」などを計算する
            return List.of(LocalDate.now());
        });

List<LocalDate> list1 = todayBusinessDaysCache.get();
List<LocalDate> list2 = todayBusinessDaysCache.get(); // 同じ日なら再計算しない
Java

ここで深掘りしたいのは、「キャッシュの有効期限を“日付”で区切る」という発想です。
「日次バッチ」「日次レポート」など、日単位で動く業務には相性が良いパターンです。


スレッドセーフティとキャッシュ

マルチスレッド環境での注意点

Web アプリやバッチは、複数スレッドから同時にキャッシュにアクセスすることが普通です。
そのとき、単純な HashMap やフィールドの書き換えだけだと、
「同時に更新して壊れる」「二重に計算される」といった問題が起きる可能性があります。

実務では、次のような対策を検討します。

キャッシュの更新部分を synchronized で囲む
ConcurrentHashMap を使う
外部のキャッシュライブラリ(Caffeine, Guava Cache など)を使う

初心者向けの段階では、「キャッシュは“共有資源”なので、同時アクセスに注意が必要」という感覚だけは持っておいてください。
本格的なキャッシュは、専用ライブラリを使う方が安全です。


セキュリティ・運用の観点から見た日付キャッシュ

「キャッシュが古いまま」はそのまま障害になる

日付キャッシュは便利ですが、「古い結果を返し続ける」リスクがあります。

祝日マスタが更新されたのに、古い営業日一覧を使い続ける。
タイムゾーン設定が変わったのに、古いオフセットを使い続ける。
システム日付の変更(運用上の調整など)に追従できない。

これらは、請求・締め処理・SLA などに直結するため、そのまま業務障害になります。

だからこそ、次のようなルールを決めておくことが重要です。

どの値をキャッシュしてよいか(“変わりにくいもの”だけにする)
いつキャッシュを破棄するか(有効期限・マスタ更新・再起動など)
キャッシュを使うかどうかを切り替えられる設定を用意するか

日付キャッシュは「性能のための工夫」であると同時に、「正しさを壊す危険」も持っている、というバランス感覚が大事です。


まとめ:日付キャッシュユーティリティで身につけてほしい感覚

日付キャッシュユーティリティは、「一度計算した日付関連の結果を覚えておき、同じ条件なら再利用する」ための仕組みです。
しかし、その裏には次のようなポイントが隠れています。

処理単位で“今日”を固定し、LocalDate.now() を乱発しない。
重い日付計算(営業日一覧・祝日一覧など)は、キー(年など)を決めてキャッシュする。
キャッシュの有効期限を必ず意識し、「いつ破棄するか」を決める。
マルチスレッド環境では、キャッシュは共有資源であり、スレッドセーフティが必要。

もしあなたのプロジェクトで、
「同じ年の営業日一覧を何度も計算している」「1つの処理の中で LocalDate.now() が何十回も出てくる」ようなコードがあれば、
それを一度「日付キャッシュユーティリティ」に置き換えられないか眺めてみてください。

それだけで、処理は軽くなり、日付の一貫性も上がり、
“業務で長く運用できる”日付・時間まわりの設計に一歩近づきます。

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