SpringBootでクラウド（AWS）かローカルかによって実装を切り替える方法

## はじめに

Webアプリケーションで画像などのファイルをアップロードしたりメールを送信する機能はよくあります。AWSを使って運用する場合、画像はS3へアップし、メール送信はAmazon SESを使用するなどが考えられます。
その場合、S3のバケットやSESへのアクセスはパブリックにはせず、デプロイされたサービスからのみなど、アクセスを限定するのが一般的でしょう。

デプロイされたサービスからのみアクセスできるようにする場合、セキュリティ面では安心ですが、ローカル環境からはアクセスできないため、ローカルで動作させるとエラーとなってしまい、開発やテストに支障が出る場合があります。

## やりたこと

例えば、以下のような実装にすればローカルでもクラウドでも同じ振る舞いが実現できます。

- 画像アップロード
    - AWS環境：S3にアップロードされる
    - ローカル環境：特定のディレクトリにファイルコピーされる
- メール送信
    - AWS環境：Amazon SESにリクエストを送信する
    - ローカル環境：サーバー認証でメール送信する

設定やコードを変えることなく動的に上記のように実装が切り替われば、ローカル環境でもクラウド環境でも同じ振る舞いをするようになり、開発やローカルでのテストがしやすくなります。

## 実現方法

SpringBootを使っている場合、Javaのインターフェースと、SpringBootで用意されている`@Conditional`アノテーションを組み合わせることで実現可能です。

### Conditionの実装を作成

SprngBootで用意されている`Condition`インターフェースを実装し、環境毎の条件を定義します。

まずはローカル用。
環境の判定方法は色々ありそうですが、ここでは実行時の引数に指定するプロファイル名で判断するようにします。
実行の引数で`--spring.profiles.active=local`と指定している場合にはLocalConditionが有効になります。

```LocalCondition.java
package sample;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class LocalCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // ここではプロファイルで環境を判定
        // application-local.propertiesやapplication-local.yamlなどを使用する場合はローカル判定
        String activeProfile = context.getEnvironment().getProperty("spring.profiles.active");
        return "local".equals(activeProfile);
    }
}

```

次にクラウド用。
ここではローカル以外はクラウドとします。
ここも判定方法色々ありそうですが、とりあえずはローカル以外をクラウド判定にしておきます。

```CloudCondition.java
package jsample;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class CloudCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // プロファイルがローカル以外はクラウド判定にする
        String activeProfile = context.getEnvironment().getProperty("spring.profiles.active");
        return !"local".equals(activeProfile);
    }
}

```

### ファイルアップロードの実装

次にファイルアップロード機能を実装。
まずはインターフェースを定義。

```FileUploadDriver.java
package sample;

import java.io.File;

public interface FileUploadDriver {
    String upload(File file, String uploadFileName) throws Exception;
}

```

次に、実装クラスを定義。詳細は割愛しますが、S3用は例えばこんな感じ。

```AmazonS3UploadDriver.java
package sample;

import jp.constructionreports.condition.CloudCondition;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.io.File;

@Component
@Conditional(CloudCondition.class) // クラウド用の条件
public class AmazonS3UploadDriver implements FileUploadDriver {

    @Value("${aws.s3.bucket}")
    private String bucketName;

    private final S3Client s3Client;

    // s3Client初期化
    public AmazonS3UploadDriver() {
        this.s3Client = S3Client.builder()
                .region(Region.AP_NORTHEAST_1)
                .build();
    }

    @Override
    public String upload(File file, String uploadFileName) throws Exception {
        // S3にアップロードする処理
        // ・・・
        // return オブジェクトのキー
    }
}
```

次にローカル用

```LocalUploadDriver.java
package sample;

import jp.constructionreports.condition.LocalCondition;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;

import java.io.File;

@Component
@Conditional(LocalCondition.class) // ろーかるｙ
public class LocalUploadDriver implements FileUploadDriver {

    @Override
    public String upload(File file, String uploadFileName) throws Exception {
        // ローカルの指定されたディレクトリにファイルをコピー
        // return 絶対パス
    }
}

```

### クラス図イメージ

ざっくりのクラス図イメージは以下のような感じ。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/497228/2ead1ab6-3982-3810-71e7-f196e26904a8.png" width=70% />

サービス、ユースケースなどのクラスがFileUploadDriverを使用する際に、Conditionとして有効な方のオブジェクトがインジェクションされるので、コードを変更しなくても実装が切り替わるようになります。

## まとめ

以下の手順で環境毎の実装切り替えができる。

1. Conditionインターフェースを実装したクラスを定義
1. 実装を切り替えたい処理に対するインターフェースを定義
1. 環境毎の実装を定義し、`@Conditional`アノテーションでConditionクラスを指定する

今回はAWS環境とローカル環境の切り替えを例にしましたが、Conditionの実装クラスをうまく作れば、様々な環境に合わせて動的に実装が切り替わるような仕組みを作れるでしょう。
また、メール送信や通知機能など、クラウドの様々なサービスと連携するようなシステムをローカルでもストレスなく動作させるようにしたい場合にも使えそうです。

### ※補足

AWSのアクセスキーやシークレットキーなどを使うことでローカルからAWSのサービスアクセスできるようにすることも可能。
その場合はわざわざローカル用の実装をする必要はなくなりますが、アクセスキーなどを適切に管理することが必要。
セキュリティにおいて余計なことを考えず、それでいてストレスなく開発をしたい場合などに今回の方法を検討してみても良いかと思います。