파일 업로드와 S3
파일 업로드 기능을 구현할 때, 주로 확장성이 좋은 AWS에서 제공하는 S3 서비스를 이용해서 많이 구현할 것입니다.
문제 사항
로컬 혹은 테스트 환경에서 AWS S3을 사용해서 파일 업로드 기능을 테스트가 필요한 상황이 있습니다.
이때마다 실제 S3 버킷에 파일을 업로드하는 것은 비효율적이다. 이를 개선하기 위해서 테스트 환경에서는 런타임 시점에 Stub 객체를 주입하는 방식으로 외부 의존성을 끊어 테스트 환경을 구성하였지만, 테스트 환경이 아닌 메인 로컬 환경에서는 파일 업로드 테스트 시, 실제 S3 버킷으로 테스트할 수밖에 없었습니다.
이를 개선하기 위해서 리서치를 진행해봤는데, S3를 Mock 해주는 오픈소스가 있어서 사용해보고 작성한 리뷰입니다.
참고로 로컬에 인메모리 형태로 S3 Mock 서버를 띄워, 그곳에 파일을 업로드하는 방식으로 영속성이 없는 휘발성 개념입니다.
https://github.com/findify/s3mock
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 버킷이 아닌, 로컬 인메모리에 저장되는 것을 확인하실 수 있습니다.
감사합니다.
'Application > Spring Framework' 카테고리의 다른 글
스프링 트랜잭션, Spring AOP (0) | 2021.08.29 |
---|---|
Spring MVC, DispatcherServlet (0) | 2021.08.29 |
스프링 인터셉터와 어노테이션으로 인증 및 권한 관리하기 (0) | 2021.03.08 |
[Spring] IoC 컨테이너와 DI와 빈의 스코프 (0) | 2021.02.16 |
[스프링 인 액션 정리] 11-12 리액티브 API, 데이터 퍼시스턴스 (0) | 2021.01.12 |