はじめに
Spring Boot で application.yaml の設定値を扱う方法として、@Value や @ConfigurationProperties の仕組みはよく知られています。しかし、設定値が増えてくると YAML の構造変更がビジネスロジック側のコードにまで波及してしまった、という経験はないでしょうか。
本記事では @Value・@ConfigurationProperties に加え、@ConfigurationProperties をベースにさらに保守性を重視した設計パターンを紹介します。3 つのアプローチの特性と使いどころを、同じ題材で比較しながら解説します。@Value や @ConfigurationProperties を使ったことがあれば、すぐに読み進められる内容です。
前提
- Spring Boot 3.x / 4.x
- Lombok 1.18.30 以降(Spring Boot 3.x との組み合わせで動作確認済み。
@Data,@RequiredArgsConstructor,@Builderを使用)
3 つのアプローチの全体像
Spring Boot の設定管理には、大きく 3 つのアプローチがあります。
3 つ目の「設定ファサード」は @ConfigurationProperties をベースにさらに層を分離するパターンで、アプローチ 3 で詳しく紹介します。
@Value |
@ConfigurationProperties |
設定ファサード | |
|---|---|---|---|
| 型安全性 | 低い(文字列ベース) | 高い | 高い |
| 構造化 | できない | YAML の階層に対応 | 利用側の関心で再構成 |
| 不変性 | なし | record で対応可 | record で保証 |
| バリデーション | 実行時のみ | Bean Validation 対応 | Properties 層で検証済み |
| 複数ソース集約 | 手動 | 手動 | Factory で一元化 |
| 導入コスト | 最小 | 低い | 中程度 |
これらは「どれが正解」というものではなく、プロジェクトの状況に応じて選択・組み合わせるものです。以降、各アプローチを具体的に見ていきます。
題材とする設定
本記事では、以下の通知サービスの設定を題材にします。メールと Slack の通知に関する設定で、全アプローチで同じ YAML を扱います。
notification:
email:
timeout-seconds: 30
retry-count: 3
from-address: noreply@example.com
slack:
webhook-url: https://hooks.slack.com/services/XXXX/XXXX/XXXX
channel: "#alerts"
aws:
region: ap-northeast-1
sqs:
alert-queue-name: notification-alerts
audit-queue-name: notification-audit
deadletter-queue-name: notification-dlq
アプローチ 1:@Value
最もシンプルな方法です。フィールドに @Value アノテーション を付けるだけで、application.yaml の値を注入できます。YAML のキー階層をドット区切りで指定します。
@Service
public class NotificationService {
@Value("${notification.email.timeout-seconds}")
private int timeoutSeconds;
@Value("${notification.email.retry-count}")
private int retryCount;
@Value("${notification.email.from-address}")
private String fromAddress;
public void sendEmail(String to, String subject, String body) {
Duration timeout = Duration.ofSeconds(timeoutSeconds);
// ...
}
}
手軽さが最大の魅力で、数個の設定値を扱うだけなら十分です。ただし、プロパティ名はただの文字列なので、タイプミスや不正な値があってもコンパイル時には検出できず、実行時に初めてエラーになります。設定値が増えてくると、どのクラスがどのプロパティを使っているかの把握も難しくなり、リネームや削除時の影響範囲を追いにくくなります。
アプローチ 2:@ConfigurationProperties
@ConfigurationProperties アノテーション は、YAML の階層構造をそのまま Java クラスにマッピングする仕組みです。前述の @Value における型安全性の欠如や影響範囲の追いにくさを解消します。先ほどの YAML を Java クラスで表現すると、次のようになります。
// prefix = "notification" → YAML の notification: 配下にマッピング
@ConfigurationProperties(prefix = "notification")
@Validated
@Data
public class NotificationProperties {
@Valid // ネストしたオブジェクトにもバリデーションを適用する(カスケード)
private EmailConfig email = new EmailConfig(); // notification.email:
@Valid
private SlackConfig slack = new SlackConfig(); // notification.slack:
@Data
public static class EmailConfig {
// notification.email.timeout-seconds → 30
@Min(1)
private int timeoutSeconds = 30;
// notification.email.retry-count → 3
@Min(0)
private int retryCount = 3;
// notification.email.from-address → "noreply@example.com"
@NotBlank
private String fromAddress;
}
@Data
public static class SlackConfig {
// notification.slack.webhook-url
private String webhookUrl;
// notification.slack.channel → "#alerts"
private String channel;
}
}
型安全で IDE の補完も効き、@Validated と組み合わせれば起動時にバリデーションが走ります。ネストしたオブジェクトに @Valid を付けることで、内側のクラスの制約(@Min, @NotBlank など)もカスケードで検証されます。コンストラクタインジェクションとも自然に組み合わせられるため、テスタビリティも向上します。多くの Spring Boot プロジェクトではこのアプローチで十分です。
@ConfigurationProperties クラスの Bean 登録方法について
このクラスには @Component を付けていません。Bean への登録は、後述の @EnableConfigurationProperties で利用側から行う方法と、@ConfigurationPropertiesScan でパッケージスキャンする方法があります。本記事では前者を採用しています。
サービスクラスからは、この Properties をインジェクションして利用します。
@Service
@RequiredArgsConstructor
public class NotificationService {
private final NotificationProperties properties;
public void sendEmail(String to, String subject, String body) {
// int → Duration の変換はサービス側で行う
Duration timeout = Duration.ofSeconds(properties.getEmail().getTimeoutSeconds());
// ...
}
}
なお、@ConfigurationProperties クラス自体を record で書くことも可能です(バリデーションアノテーションは簡略化のため省略しています)。
// record を使った書き方(Spring Boot 3.x 以降)
@ConfigurationProperties(prefix = "notification")
public record NotificationProperties(EmailConfig email, SlackConfig slack) {
public record EmailConfig(int timeoutSeconds, int retryCount, String fromAddress) {}
public record SlackConfig(String webhookUrl, String channel) {}
}
record は Java 16 で正式導入されたクラスの一種で、コンストラクタ・アクセサ・equals/hashCode/toString が自動生成されます。生成後にフィールドを変更できない(不変)ため、設定値のように「一度決めたら変わらない」データの表現に適しています。
アプリケーションが大きくなると見えてくる課題
このアプローチにも、規模が大きくなると限界が見えてくる場面があります。
① 型変換がサービス側に散在する。
通知機能に関わるクラスが複数あると、同じ変換ロジックが各クラスに重複します。やがて単位の取り違えといったバグの温床になります。
// EmailSender.java
Duration timeout = Duration.ofSeconds(properties.getEmail().getTimeoutSeconds());
// EmailHealthChecker.java — ミリ秒と秒を間違えている
Duration healthCheckTimeout = Duration.ofMillis(properties.getEmail().getTimeoutSeconds());
② 複数の Properties に依存して、コンストラクタが膨れ上がる。
例えば、メール送信サービスは通知設定だけでなく、AWS サービスの設定も必要になることがあります。実際のプロジェクトでは、さらにリトライポリシーやメトリクス設定など、依存する Properties が増えていく可能性があります。
@Service
@RequiredArgsConstructor
public class EmailDeliveryService {
private final NotificationProperties notificationProperties;
private final AwsProperties awsProperties; // AWS 設定
public void deliver(Email email) {
String queueName = awsProperties.getSqs().getAlertQueueName();
Duration timeout = Duration.ofSeconds(notificationProperties.getEmail().getTimeoutSeconds());
// ... 複数の Properties から値を取り出して使う
}
}
これらの課題が顕在化してきたとき、次のアプローチが選択肢に入ります。
アプローチ 3:設定ファサードパターン
アプローチ 2 の課題の根本には、YAML の構造とサービスクラスが直接結びついているという問題があります。YAML の構造変更という「設定ファイル側の都合」が、ビジネスロジックの修正を引き起こす構造です。
設定ファサードパターンは、この結びつきを断ち切るために設定管理を 3 つの層に分離します。型変換の一元化や複数ソースの集約はその副産物です。GoF の Facade パターン(複雑な複数サブシステムをシンプルなインターフェースで包む)を設定管理に適用した考え方であり、DDD の Anti-Corruption Layer(外部の構造を内部モデルに変換する層)やクリーンアーキテクチャの依存方向の制御にも通じます。
本記事での「設定ファサードパターン」は説明のための命名です。GoF の Facade パターンや DDD の Anti-Corruption Layer に通じる考え方ですが、一般的に確立された用語ではありません。
3 つの層の役割は次のとおりです。
-
Properties:YAML の構造をマッピングするクラス(
@ConfigurationProperties) - Factory:Properties から値を取り出し、型変換・集約して Settings を生成する
-
Settings:アプリケーションが使う不変の設定オブジェクト(
record)
Properties は YAML 側の関心事、Settings はアプリケーション側の関心事です。間に Factory が入ることで、両者が互いの変更に影響されなくなります。
実装例
各層の実装を見ていきます。NotificationProperties(前述)に加え、題材 YAML の aws: セクションに対応する AwsProperties も使って複数 Properties からの集約を示します。
AwsProperties
@ConfigurationProperties(prefix = "aws")
@Validated
@Data
public class AwsProperties {
// aws.region → "ap-northeast-1"
@NotBlank
private String region;
@Valid
private SqsConfig sqs = new SqsConfig();
@Data
public static class SqsConfig {
@NotBlank
private String alertQueueName;
@NotBlank
private String auditQueueName;
@NotBlank
private String deadletterQueueName;
}
}
AwsProperties には region や複数のキュー名が含まれていますが、今回の通知サービスの例では Settings に集約するのは alertQueueName だけです。Settings にはユースケースに必要な設定値だけを含めるのがこのパターンの基本です。region や auditQueueName が必要な別のユースケースがあれば、そちらの Settings に含めます。
Settings クラス
アプリケーションが使いやすい形に整えた不変オブジェクトです。
命名規則について: Settings のフィールド名は YAML のキー名に縛られません。利用側のユースケースに即して自由に命名します。後述の Factory が YAML と Settings の対応を吸収するため、両者の名前は一致している必要はありません。
@Builder
public record NotificationSettings(
// メール送信タイムアウト ← notification.email.timeout-seconds (int → Duration に変換)
Duration emailTimeout,
// メール送信リトライ回数 ← notification.email.retry-count
int emailRetryCount,
// メール送信元アドレス ← notification.email.from-address
String emailFromAddress,
// Slack 通知先 Webhook URL ← notification.slack.webhook-url
String slackWebhookUrl,
// Slack 通知先チャンネル ← notification.slack.channel
String slackChannel,
// アラート通知先キュー名 ← aws.sqs.alert-queue-name(別の Properties から集約)
// AWS の命名規則ではなく、ビジネス上の意味で命名する
String alertQueueName
) {}
int timeoutSeconds が Duration emailTimeout に変わっており、利用側は「秒かミリ秒か」を気にする必要がなくなっています。alertQueueName は AwsProperties 由来ですが、Settings 上は通知に必要な設定として同居し、AWS を意識させない名前になっています。
ConfigurationFactory
複数の Properties から値を取り出し、型変換や集約を行って Settings を生成するクラスです。
@Configuration
// @Component を Properties クラスに付ける方法もあるが、ここで明示登録することで
// 「この Factory がこれらの Properties に依存している」という関係をコードで表現する
@EnableConfigurationProperties({NotificationProperties.class, AwsProperties.class})
@RequiredArgsConstructor
public class ConfigurationFactory {
// notification: 配下の設定(メール・ Slack)
private final NotificationProperties notificationProperties;
// aws: 配下の設定
private final AwsProperties awsProperties;
@Bean
public NotificationSettings notificationSettings() {
var email = notificationProperties.getEmail();
var slack = notificationProperties.getSlack();
var sqs = awsProperties.getSqs();
return NotificationSettings.builder()
// int 秒 → Duration に変換(この変換を一箇所に集約するのがポイント)
.emailTimeout(Duration.ofSeconds(email.getTimeoutSeconds()))
.emailRetryCount(email.getRetryCount())
.emailFromAddress(email.getFromAddress())
.slackWebhookUrl(slack.getWebhookUrl())
.slackChannel(slack.getChannel())
// 異なる Properties からの値も 1 つの Settings にまとめる
.alertQueueName(sqs.getAlertQueueName())
.build();
}
}
@EnableConfigurationProperties を使う側(Factory)に置くことで、「この Factory がこれらの Properties に依存している」という依存関係がコードとして明示されます。型変換と複数 Properties からの集約がここに一元化されます。
サービスクラスでの利用
@Service
@RequiredArgsConstructor
public class NotificationService {
// Settings のみに依存(Properties や YAML の構造を知らない)
private final NotificationSettings settings;
private final EmailClient emailClient;
public void sendEmail(String to, String subject, String body) {
// settings.emailTimeout() はすでに Duration 型。型変換なしでそのまま使える。
emailClient.send(
new EmailRequest(settings.emailFromAddress(), to, subject, body),
settings.emailTimeout()
);
// settings.alertQueueName() は AwsProperties 由来だが、このクラスはそれを知らない。
}
}
サービスクラスが依存するのは NotificationSettings だけです。設定が NotificationProperties と AwsProperties のどちらから来たのか、元が int の秒数なのかといった詳細を一切知りません。
設定変更時の影響範囲
このパターンの核心的な価値は、YAML 構造の変更が Factory 層で吸収されることです。
例えば、notification.email.timeout-seconds のキー名を notification.email.timeout-sec にリネームした場合を考えます。アプローチ 2 では、Properties クラスの修正に加えて、その Properties を参照しているすべてのサービスクラスにも変更が波及します。YAML のキー名をリネームしただけなのに、ビジネスロジック側のコードに手を入れることになります。
一方、設定ファサードパターンでは Properties と Factory の修正だけで完結し、Settings のインターフェースは変わらないため、サービスクラスには一切手を加える必要がありません。
テスト容易性
アプローチ 2 では、複数の Properties を setter で組み立てる必要があり、テストの準備コードが膨れ上がります。設定ファサードパターンでは Settings 1 つを Builder で生成するだけです。
@Test
void メール送信のタイムアウトが設定値で動作する () {
// record の Builder で必要な値だけ指定。Properties の構造や YAML の出所は無関係。
var settings = NotificationSettings.builder()
.emailTimeout(Duration.ofSeconds(5))
.emailRetryCount(1)
.emailFromAddress("test@example.com")
.slackWebhookUrl("https://hooks.slack.com/test")
.slackChannel("#test")
.alertQueueName("alert-queue-test")
.build();
var service = new NotificationService(settings, mockEmailClient);
// テスト実行...
}
Settings が record + @Builder なので、Spring コンテキストを起動せず Builder で簡潔に生成できます。アプローチ 2 で複数の Properties を setter で組み立てていたコードと比べると、準備の手間が大幅に減っています。
デメリット
堅牢さと引き換えに、このパターンには明確なコストがあります。
| デメリット | 内容 | 緩和策 |
|---|---|---|
| 編集コスト | 設定値 1 つの追加に Properties・Settings・Factory の 3 箇所の修正が必要 | 定型的な構造のため自動化しやすい |
| 小規模では過剰 | 設定項目が少ない場合、層を分けた恩恵より同期コストが上回る | 課題を感じてから段階的に適用する |
| チーム共有が必要 | なぜ Properties を直接使わないのか、パターンの目的を共有する必要がある | 判断基準をドキュメント化しておく |
小規模な単体サービスや短期プロジェクトでは過剰になりがちです。課題を感じてから段階的に適用するのがよいでしょう。
※なお、編集コストについては、このパターンの構造が定型的であることが緩和材料になります。Properties・Settings・Factory の 対応関係が明確なため、以前からマクロ等によるコード一括生成との相性が良いパターンでした。近年は AI エージェントに「この設定値を追加して」と指示するだけで整合性を保って更新できるようになり、編集コストの問題は大幅に軽減されています。採用を検討する際は、こうした自動化手段も含めてコストを評価してみてください。
どのアプローチを選ぶか
① @Value
設定項目が数個で手早く動かしたいときに適しています。プロトタイプや小さなバッチ処理など、構造化のコストに見合わない規模のプロジェクトが典型です。
② @ConfigurationProperties
多くの Spring Boot プロジェクトにおけるスタンダードです。型安全で構造化された設定管理が手に入り、バリデーションも使えます。設定項目が増えても YAML の階層をそのまま Java クラスに対応づけられるため、見通しが良く保てます。まずはここを基本に考えるのがよいでしょう。
③ 設定ファサードパターン
@ConfigurationProperties の運用で課題を感じたときの次のステップです。特に以下のようなケースで効果を発揮します。
- 大規模サービスで変更時の影響を抑えたい場合 — 設定を参照するサービスクラスが多いほど、YAML 構造の変更がもたらす波及コストは大きくなります。Factory 層で吸収することで、変更箇所を局所化できます
- 共通ライブラリやフレームワークとして提供する場合 — YAML 側の構造変更がライブラリの公開インターフェース(Settings)に波及しないため、利用者への影響を最小化できます
- 設定の提供元が頻繁に変わる可能性がある場合 — 環境移行(AWS → Azure、ローカル → クラウド)などで Properties の構造が変わっても、Settings のインターフェースを維持できるため、サービスクラスの修正が不要になります
迷ったらまず @ConfigurationProperties を採用し、型変換の散在や Properties 依存の肥大化を感じたら設定ファサードへ、というのが現実的な判断軸となります。
まとめ
Spring Boot の設定管理には、@Value・@ConfigurationProperties・設定ファサードパターンの 3 つのアプローチがあり、プロジェクトの成長に合わせて段階的に採用できます。
設定ファサードパターンの核心は、YAML 構造の変更をサービスクラスに波及させないことです。Factory を緩衝層として挟むことで、型変換の一元化・複数ソースの集約・不変性の保証・テスト容易性は、いずれもこの設計の帰結として得られます。
一方で、過剰な関心の分離は変換レイヤーを増やし、かえってメンテナンス性を下げる要因にもなります。
大切なのは、どのような選択肢があり、それぞれがどんな場面で力を発揮するかを把握して適用することです。
本記事のアプローチ整理が、設定管理の適切な設計判断の一助になれば幸いです。
参考
- Spring Boot 公式ドキュメント - Externalized Configuration
- Spring Boot 公式ドキュメント - Type-safe Configuration Properties
- Guide to @ConfigurationProperties in Spring Boot - Baeldung
We Are Hiring!

