Application/Spring Framework

로컬에서 임베디드 S3 사용하기

반응형

파일 업로드와 S3

파일 업로드 기능을 구현할 때, 주로 확장성이 좋은 AWS에서 제공하는 S3 서비스를 이용해서 많이 구현할 것입니다.

 

Spring Cloud AWS S3 연동 및 파일 업로드

스프링에서 프로필 사진 업로드 등 파일을 업로드하는 경우 Spring Cloud를 통해 쉽게 개발하실 수 있습니다. 참고로 지난번 AWS S3에 파일 업로드 하는 방법에 비해 좀 더 개선된 방법으로 글 올려보

willseungh0.tistory.com

문제 사항

로컬 혹은 테스트 환경에서 AWS S3을 사용해서 파일 업로드 기능을 테스트가 필요한 상황이 있습니다.

이때마다 실제 S3 버킷에 파일을 업로드하는 것은 비효율적이다. 이를 개선하기 위해서 테스트 환경에서는 런타임 시점에 Stub 객체를 주입하는 방식으로 외부 의존성을 끊어 테스트 환경을 구성하였지만, 테스트 환경이 아닌 메인 로컬 환경에서는 파일 업로드 테스트 시, 실제 S3 버킷으로 테스트할 수밖에 없었습니다.

 

이를 개선하기 위해서 리서치를 진행해봤는데, S3를 Mock 해주는 오픈소스가 있어서 사용해보고 작성한 리뷰입니다.

참고로 로컬에 인메모리 형태로 S3 Mock 서버를 띄워, 그곳에 파일을 업로드하는 방식으로 영속성이 없는 휘발성 개념입니다.

https://github.com/findify/s3mock

 

GitHub - findify/s3mock: Embedded S3 server for easy mocking

Embedded S3 server for easy mocking. Contribute to findify/s3mock development by creating an account on GitHub.

github.com


build.gradle

  • 먼저 해당하는 외부 의존성을 추가해줍니다.
  • 만약 테스트 환경에서만 사용할 거라면 testImplementation 'io.findify:s3mock_2.13:0.2.6' 를 해주면 되지만 저는 실제 메인 환경 (로컬)에서 사용하기 위해서 다음과 같이 구성하였습니다.
dependencies {

	...
    
    implementation 'io.findify:s3mock_2.13:0.2.6'
}

 

EmbeddedS3Config.java

  • 로컬의 인메모리에 S3Mock 서버를 구성하기 위한 Config 설정입니다.
@Slf4j
@Profile({"local"}) // local 프로파일에서만 생성되는 빈.
@Configuration
public class EmbeddedS3Config {

    @Value("${cloud.aws.region.static}")
    private String region;

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

    @Value("${cloud.aws.s3.mock.port}")
    private int port;

    @Bean
    public S3Mock s3Mock() {
        return new S3Mock.Builder()
            .withPort(port) // 해당 포트에 프로세스가 생성된다.
            .withInMemoryBackend() // 인메모리에서 활성화.
            .build();
    }

    @PostConstruct // 의존성 주입이 이루어진 후 임베디드 S3 서버를 가동시킨다.
    public void startS3Mock() throws IOException {
        port = ProcessUtils.isRunningPort(port) ? ProcessUtils.findAvailableRandomPort() : port;
        this.s3Mock().start();
        log.info("인메모리 S3 Mock 서버가 시작됩니다. port: {}", port);
    }

    @PreDestroy
    public void destroyS3Mock() {
        this.s3Mock().shutdown();
        log.info("인메모리 S3 Mock 서버가 종료됩니다. port: {}", port);
    }

    @Bean
    @Primary
    public AmazonS3 amazonS3Client() {
        AwsClientBuilder.EndpointConfiguration endpoint = new AwsClientBuilder.EndpointConfiguration(getUri(), region);
        AmazonS3 client = AmazonS3ClientBuilder
            .standard()
            .withPathStyleAccessEnabled(true)
            .withEndpointConfiguration(endpoint)
            .withCredentials(new AWSStaticCredentialsProvider(new AnonymousAWSCredentials()))
            .build();
        client.createBucket(bucket);
        return client;
    }

    private String getUri() {
        return UriComponentsBuilder.newInstance()
            .scheme("http")
            .host("localhost")
            .port(port)
            .build()
            .toUriString();
    }

}

 

ProcessUtils.java

다음으로 특정 포트가 사용 중인지 확인하는 isRunningPort() 정적 메소드와

만약 포트가 사용 중이라면 사용할 수 있는 랜덤 포트를 찾을 수 있는 findAvailableRandomPort() 메소드로 구성하였다.

 

이때 OS 환경에 따라서 프로세스를 확인하는 명령이 달라서 다음과 같이 구성하였다.

public final class ProcessUtils {

    private ProcessUtils() {
    }

    private static final String OS = System.getProperty("os.name").toLowerCase();

    public static boolean isRunningPort(int port) throws IOException {
        return isRunning(executeGrepProcessCommand(port));
    }

    public static int findAvailableRandomPort() throws IOException {
        for (int port = 10000; port <= 65535; port++) {
            Process process = executeGrepProcessCommand(port);
            if (!isRunning(process)) {
                return port;
            }
        }
        throw new IllegalArgumentException("사용가능한 포트를 찾을 수 없습니다. (10000 ~ 65535)");
    }

    private static Process executeGrepProcessCommand(int port) throws IOException {
        // 윈도우일 경우
        if (isWindows()) {
            String command = String.format("netstat -nao | find \"LISTEN\" | find \"%d\"", port);
            String[] shell = {"cmd.exe", "/y", "/c", command};
            return Runtime.getRuntime().exec(shell);
        }
        String command = String.format("netstat -nat | grep LISTEN|grep %d", port);
        String[] shell = {"/bin/sh", "-c", command};
        return Runtime.getRuntime().exec(shell);
    }

    private static boolean isWindows() {
        return OS.contains("win");
    }

    private static boolean isRunning(Process process) {
        String line;
        StringBuilder pidInfo = new StringBuilder();
        try (BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            while ((line = input.readLine()) != null) {
                pidInfo.append(line);
            }
        } catch (Exception e) {
            throw new IllegalArgumentException("사용가능한 포트를 찾는 중 에러가 발생하였습니다.");
        }
        return !StringUtils.isEmpty(pidInfo.toString());
    }

}

 

application-cloud.yml

spring:
  profiles: local,local-will
cloud:
  aws:
    s3:
      bucket: local-embedded-bucket
      mock:
        port: 5050
    region:
      static: ap-northeast-2
    stack:
      auto: false
---

 

참고로 해당 application-cloud.yml 파일 생성 후 다음과 같이 cloud 프로파일을 포함해줘야 한다.

 

application.yml

spring
  profiles:
    include:
      - cloud

이미지를 업로드하면 실제 S3 버킷이 아닌, 로컬 인메모리에 저장되는 것을 확인하실 수 있습니다.


감사합니다.

반응형