본문 바로가기

스프링 부트

S3를 사용하는 환경에서 LocalStack을 통한 통합 테스트

놀토는 이미지 저장소로 AWS의 S3를 사용한다. 
하지만 S3에 대한 쓰기 작업은 우테코 측해에 권한을 부여한 EC2에서만 가능했으며 accesskey와 secretkey도 받을 수 없었기 때문에
로컬 환경에서는 S3를 사용하는 서비스에 대해서 테스트를 진행할 수 없었다.

AWS의 S3를 이용하는 서비스단을 통합 테스트하는 방법으로 LoacalStack을 이용하는 방법을 제시해줬는데, 
이를 사용하는 통합테스트를 진행한 과정들을 기록한다.

Localstack?

LocalStack provides an easy-to-use test/mocking framework for developing Cloud applications. It spins up a testing environment on your local machine that provides the same functionality and APIs as the real AWS cloud environment.

Yes, that's true - you can run your Lambda functions, store data to DynamoDB tables, feed events through Kinesis streams, put your application behind an API Gateway, and much more. And all this happens on your local machine, without ever talking to the cloud.

AWS 클라우드 리소스 등을 사용하는 애플리케이션 개발을 위해 사용하기 쉬운 테스트/Mock 프레임워크를 제공한다. (전부 지원하진 않고 일부는 유료) 실제 AWS 클라우드 환경과 동일한 기능 및 API를 제공하는 테스트 환경을 로컬 머신에서 가동한다. 도커를 사용하여 손쉽게 실행 가능하다.

이 LocalStack을 테스트 환경에서 사용하기 위해 TestContainers를 사용하였다. 

Testcontainers?

JUnit 테스트를 보조하는 자바 라이브러리로, 코드 상에서 여러 도커 컨테이너들을 실행하고 테스트 코드와 연동할 수 있는 방법을 제공한다. (데이터베이스, Selenium 웹브라우저와 같이 도커 컨테이너로 실행될 수 있는 모든 종류의 인스턴스 제공) Testcontainers를 사용하기 위해서는 로컬 도커가 필요하다. 

ImageService 통합 테스트 작성하기

build.gradle 추가

    implementation("com.amazonaws:aws-java-sdk-s3")
    testImplementation "org.testcontainers:testcontainers:1.15.3"
    testImplementation "org.testcontainers:junit-jupiter:1.15.3"
    testImplementation "org.testcontainers:localstack:1.15.3"
    compile group: 'commons-fileupload', name: 'commons-fileupload', version: '1.4'
    compile group: 'commons-io', name: 'commons-io', version: '2.4'

참고한 블로그들의 gradle로는 해결되지 않아서... 
수많은 삽질 끝에 성공한 gradle 파일.

testcontainers, localstack와 aws s3 관련 의존성을 불러온다.

LocalStackS3Config 

@TestConfiguration
public class LocalStackS3Config {
    DockerImageName localstackImage = DockerImageName.parse("localstack/localstack");

    @Bean(initMethod = "start", destroyMethod = "stop")
    public LocalStackContainer localStackContainer() {
        return new LocalStackContainer(localstackImage)
                .withServices(S3);
    }

    @Bean
    public AmazonS3 amazonS3(LocalStackContainer localStackContainer) {
        return AmazonS3ClientBuilder.standard()
                .withEndpointConfiguration(localStackContainer.getEndpointConfiguration(LocalStackContainer.Service.S3))
                .withCredentials(localStackContainer.getDefaultCredentialsProvider())
                .build();
    }
}

 localstack을 사용한 S3 클라이언트를 Bean으로 생성하는 Config 클래스를 테스트 패키지에 추가하고 S3 빈을 오버라이딩 하는 방법을 사용하였다.

initMethod / destroyMethod

@Bean의 옵션으로 Bean이 초기화 될 때, 소멸될 때 수행할 메서드를 지정할 수 있다. 
현재  AmazonS3 빈이 초기화 될 때 해당 localstack 컨테이너가 실행중인 상태여야 한다. 또한 이 빈이 소멸될 때도 localstack 컨테이너는 종료되어야 한다. 

LocalStackContainer가 상속받고 있는 GenericContainer<LocalStackContainer>를 까보면 컨테이너를 실행하는 start와 컨테이너를 종료시키는 stop 메서드가 정의되어 있는 것을 확인할 수 있다.

로컬 환경에서 localstack 도커 실행

docker run --rm -it -p 4566:4566 -p 4571:4571 localstack/localstack

Testcontainers를 위해 로컬에서 localstack 도커를 미리 띄워놓아야 한다.

application.yml에 빈 오버라이드 설정

spring:
  main:
    allow-bean-definition-overriding: true

Spring Boot 2.1이상 버전에서는 빈 오버라이딩이 비활성화 되어 있기 때문에 애플리케이션 Fail이 발생한다. 
(현재 정의한 S3빈이 실제 S3빈과 겹치기 때문이다.)
때문에 yml에서 빈 오버라이딩 설정을 허용해주어야 한다. 

ImageServiceTest

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = LocalStackS3Config.class)
class ImageServiceTest {

    public static final String FILE_PATH = "/src/test/resources/static/";
    public static final String 업로드할_이미지_이름 = "pretty_cat.png";
    public static final String 업데이트할_이미지_이름 = "amazzi.jpeg";

    @Value("${application.bucket.name}")
    private String bucketName;

    @Value("${application.cloudfront.url}")
    private String cloudfrontUrl;

    @Value("${application.default-image}")
    private String 기본_이미지_이름;

    @Autowired
    AmazonS3 amazonS3;

    @Autowired
    ImageService imageService;

    @BeforeEach
    void init() {
        amazonS3.createBucket(bucketName);
        amazonS3.putObject(bucketName, 기본_이미지_이름, new File(new File("").getAbsolutePath() + FILE_PATH + 기본_이미지_이름));
    }

    @DisplayName("이미지를 S3에 업로드한다.")
    @Test
    void upload() throws IOException {
        File 업로드할_이미지_파일 = new File(new File("").getAbsolutePath() + FILE_PATH + 업로드할_이미지_이름);
        MultipartFile 업로드할_멀티파트_파일 = generateMultiPartFile(업로드할_이미지_파일);

        String 업로드된_이미지_주소 = imageService.upload(업로드할_멀티파트_파일, ImageKind.FEED);
        String 업로드된_이미지_이름 = 업로드된_이미지_주소.replace(cloudfrontUrl, "");

        assertAll(
                () -> assertThat(업로드된_이미지_주소).isNotNull(),
                () -> assertThat(amazonS3.doesObjectExist(bucketName, 업로드된_이미지_이름)).isTrue()
        );
    }
    // ...
    
 }

당시 cloudfront의 url과 기본이미지 이름은 yml로 관리하고 있었기에 @Value 로 해당 값들을 초기화해주었다. 
이제 ImageService 통합테스트 시 실제 AWS의 S3를 사용하는 것이 아닌 Testcontainers가 제공하는 S3환경을 사용하기 때문에 로컬에서도 S3를 이용하는 테스트가 가능해졌다. 
실제 배포 환경과 동일한 상태를 만들기 위하여 @BeaforeEach로 S3에 현재 우리가 사용하고 있는 버킷 이름과 동일한 버킷을 만들어 주어 같은 환경을 가지도록 하였다. 

 

참고자료