Article

⚙️ Spring Bootの設定値を安全に扱う:@ConfigurationPropertiesと@Valueの使い分け

java, springboot, spring, backend

Spring Bootで外部APIのURLやタイムアウト値などを扱うとき、手軽だからと@Valueを増やしていくと、設定値がアプリケーション全体に散らばりやすくなります。

この記事では、複数の関連する設定値を@ConfigurationPropertiesで型安全にまとめ、起動時に検証する方法を整理します。

まず結論

単発の設定値なら@Valueでも十分ですが、自分たちのアプリケーション用に複数の設定キーを定義するなら、基本的には@ConfigurationPropertiesでグループ化すると扱いやすくなります。

Spring Boot公式ドキュメントでも、独自コンポーネント向けの設定キー群は@ConfigurationPropertiesを付けたPOJOにまとめることが推奨されています。

@Valueで設定するとどうなるか

例えば外部の決済APIを呼ぶサービスがあるとします。

payment:
  api:
    base-url: https://example.com
    connect-timeout: 2s
    read-timeout: 5s

@Valueなら次のように取得できます。

@Service
public class PaymentClient {

    private final String baseUrl;
    private final Duration connectTimeout;
    private final Duration readTimeout;

    public PaymentClient(
            @Value("${payment.api.base-url}") String baseUrl,
            @Value("${payment.api.connect-timeout}") Duration connectTimeout,
            @Value("${payment.api.read-timeout}") Duration readTimeout) {
        this.baseUrl = baseUrl;
        this.connectTimeout = connectTimeout;
        this.readTimeout = readTimeout;
    }
}

数個なら問題ありません。しかし設定が増えると、キー名の文字列が複数クラスに散らばり、どの設定がひとまとまりなのか分かりにくくなります。

@ConfigurationPropertiesでまとめる

設定専用クラスを作ります。

import java.net.URI;
import java.time.Duration;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "payment.api")
public record PaymentApiProperties(
        URI baseUrl,
        Duration connectTimeout,
        Duration readTimeout
) {
}

そして設定クラスを登録します。

import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@ConfigurationPropertiesScan
public class Application {
}

利用側は設定キーそのものを意識しなくて済みます。

@Service
public class PaymentClient {

    private final PaymentApiProperties properties;

    public PaymentClient(PaymentApiProperties properties) {
        this.properties = properties;
    }

    public void execute() {
        URI endpoint = properties.baseUrl();
        Duration timeout = properties.readTimeout();

        // API呼び出し
    }
}

これでpayment.api配下の設定が1つの型としてまとまります。

型変換をSpring Bootに任せられる

@ConfigurationPropertiesの便利な点は、文字列をそのまま受け取る必要がないことです。

例えば次の設定は、

payment:
  api:
    connect-timeout: 2s

Durationとして受け取れます。

Duration connectTimeout

URLもURIなど適切な型にしておけば、アプリケーション内部で毎回パースする必要がありません。

「設定ファイルでは文字列、Javaコードでは意味のある型」という境界を作れるため、設定値の扱いが明確になります。

起動時に設定ミスを検出する

本番で特に避けたいのが、「APIを実際に呼ぶまで設定漏れに気付かない」状態です。

@ConfigurationPropertiesはBean Validationと組み合わせて起動時に検証できます。

import java.net.URI;
import java.time.Duration;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@ConfigurationProperties(prefix = "payment.api")
@Validated
public record PaymentApiProperties(
        @NotNull URI baseUrl,
        @NotNull Duration connectTimeout,
        @NotNull Duration readTimeout
) {
}

さらに数値なら制約を付けられます。

@ConfigurationProperties(prefix = "worker")
@Validated
public record WorkerProperties(
        @Positive int concurrency
) {
}

設定が不正ならアプリケーション起動時に失敗させられます。

これはFail Fast(異常をできるだけ早い段階で検出して停止する考え方)につながります。

環境ごとの差分はProfileだけに頼らない

Spring Bootではapplication-prod.ymlのようなProfile固有ファイルも利用できます。

# application.yml
payment:
  api:
    connect-timeout: 2s
    read-timeout: 5s
# application-prod.yml
payment:
  api:
    base-url: https://payment.example.com

ただし、本番の認証情報などをGit管理する設定ファイルへ直接書くのは避けるべきです。

Spring Bootは環境変数もPropertySourceとして扱います。例えば、

export PAYMENT_API_BASEURL=https://payment.example.com

のように外部から値を渡せます。

Spring Bootには設定値の優先順位があり、後から評価されるPropertySourceが前の値を上書きします。例えばOS環境変数はConfig Dataより後に評価されるため、application.ymlのデフォルト値を環境変数で上書きできます。

ハマりどころ1:設定値がどこで上書きされたか分からない

ローカルでは正しいのに本番だけ値が違う場合、環境変数や起動引数による上書きを疑います。

Spring Boot Actuatorを利用している場合、envやconfigpropsエンドポイントは設定値の調査に役立ちます。

ただし、設定情報には認証情報などが含まれる可能性があります。Actuatorのエンドポイントを外部へ無制限に公開しないようにします。

ハマりどころ2:@Valueと@ConfigurationPropertiesを無秩序に混在させる

次のように同じ設定グループを別々の方法で読むと、保守時に追いづらくなります。

@Value("${payment.api.base-url}")
private String baseUrl;

別クラスでは、

private final PaymentApiProperties properties;

という状態です。

既にPaymentApiPropertiesを作ったなら、そのグループは原則そこを経由して読む、と決めておくと構造が分かりやすくなります。

ハマりどころ3:デフォルト値で設定漏れを隠す

例えば、

@Value("${payment.api.timeout:30s}")

とすると、設定がなくても30秒で起動します。

便利ですが、本番で必須の設定までデフォルト値を付けると、設定漏れに気付きにくくなります。

「安全なデフォルト値を持てる設定」と「環境ごとに必ず明示すべき設定」を分けるのが重要です。

必須設定は@Validatedで起動時に落とす方が安全です。

@Valueを使う場面

@Valueが悪いわけではありません。

例えば一度しか使わない単純な設定なら十分です。

@Value("${app.version:unknown}")
private String version;

また、@ValueはSpEL(Spring Expression Language)を利用できます。一方、@ConfigurationPropertiesは設定値を構造化して扱うことに向いています。

目安としては次のように考えています。

状況 選択
単発の単純な値 @Value
同じprefixの設定が複数ある @ConfigurationProperties
DurationやURIなど型を付けたい @ConfigurationProperties
起動時に設定を検証したい @ConfigurationProperties + @Validated
SpELが必要 @Value

実務でのおすすめ構成

例えば外部APIごとに設定クラスを分けます。

config/
├── PaymentApiProperties.java
├── StorageProperties.java
└── NotificationProperties.java

各クラスでは設定の取得と検証だけを担当させます。

ビジネスロジックを設定クラスに入れないことで、「設定」と「処理」の責務を分離できます。

まとめ

Spring Bootの設定値は、単にapplication.ymlから文字列を取り出すだけではありません。

複数の関連設定は@ConfigurationPropertiesで1つの型にまとめ、DurationやURIなど適切な型へ変換し、必須項目は@Validatedで起動時に検証すると安全です。

設定値は環境変数やコマンドライン引数などでも上書きされるため、「どこから値が来るか」も意識しておく必要があります。

設定が増えてきたタイミングで@Valueを追加し続けるのではなく、設定自体をアプリケーションのインターフェースとして設計すると、環境差分による事故を減らしやすくなります。

参考