본문 바로가기
문제해결

Testcontainers - Localstack 설정 중 생겼던 궁금증

by KKambi 2021. 4. 25.

문제상황

- AWS S3를 사용하는 어플리케이션에서 통합 테스트 코드 작성
- 어플리케이션 외부의 클라우드 서비스인 S3 Mocking 필요
- 이를 위해 Testcontainers의 Localstack 설정하던 중 문제 발생

 

잠깐! 사전 지식

Testcontainers?
- JUnit 테스트를 보조하는 자바 라이브러리
- 데이터베이스, Selenium 웹브라우저와 같이 도커 컨테이너로 실행될 수 있는 모든 종류의 인스턴스 제공
- Data access layer integration tests에선 DB의 컨테이너화된 인스턴스를 제공하여 영속성 계층을 쉽게 테스트할 수 있습니다. 즉, 외부 DB의 상태에 의존하지 않게 됩니다.
- Application integration tests에선 DB, Message Queue, Web Server와 같은 외부 의존성을 모킹할 수 있습니다.
- UI/Acceptance tests에선 Selenium 등을 이용할 수 있습니다.

따라서 테스트컨테이너를 사용하기 위해선 Local Docker가 필요하고, JVM Testing Framework를 사용해야 합니다. 저는 평소에 JUnit 5를 사용하고 있습니다.

 

Localstack?
- AWS 클라우드 리소스를 모킹해주는 라이브러리 (전부 지원하진 않음. 일부는 유료)
- 테스트컨테이너와 마찬가지로 도커 사용
- localstack-utils처럼 단독으로 사용할 수도 있지만, 테스트컨테이너와 연동하여 사용할 수도 있다.

// build.gradle
dependencies {
    implementation("org.testcontainers:localstack")
}

 

 

궁금증

1. Localstack이 왜 필요할까?

부끄럽지만 저는 평소에 통합테스트를 거의 작성하지 않고, Service / Repository / Domain 3개 레이어를 중점으로 테스트를 작성해왔는데요. 그러다 보니 외부 서비스를 사용하는 부분의 메소드는 Mockito를 사용해 모킹하는 방식을 사용해왔습니다.

그런데 Integration Test(통합 테스트)에선 비즈니스 로직의 일부분을 Mocking하지 않습니다. 직접 Mockmvc로 컨트롤러의 메소드를 호출하게 되는데요. 따라서 외부 서비스를 호출하는 로직을 수행하게 됩니다. 이 때 실제 S3에 영향을 끼치고 싶지 않다면 Mocking을 해야겠죠.

AWS 클라우드 리소스를 Mocking해주는 게 바로 Localstack의 역할입니다.

 

2. Integration Test는 왜 필요할까? Service Test로 충분하지 않을까?

- 모든 Bean을 스프링 컨테이너에 생성하며, 운영환경과 가장 유사한 테스트를 진행할 수 있습니다. 다만 외부 API 호출(S3, SQS, 다른 어플리케이션)이 들어갈 경우, 이를 롤백하기 어렵거나 외부 상태에 의존하기 때문에 Testcontainers를 사용하는 것이겠죠?

- 통합테스트는 모듈을 통합(Integrate)하는 단계에서 수행합니다. 단위 테스트를 성공적으로 수행했다고 해서, 각 모듈이 연동/결합되었을 때 정상적으로 작동할 것이라는 보장이 없잖아요? 게다가 모든 요구사항을 항상 Unit test할 수 없는 환경일 수도 있구요.

 

3. Bean overriding 문제 해결

Localstack에서 제공하는 S3 인스턴스와 연결된 S3 Client 객체를 Bean으로 생성해야 합니다. 그런데 통합 테스트이므로 실제 어플리케이션에서 사용하는 S3 Client도 Bean으로 등록되겠죠?

그런데 스프링 컨테이너는 기본적으로 메소드 이름을 빈의 이름으로 사용합니다. 그런데 어플리케이션 소스에서 사용하는 S3Client Bean과 테스트를 위한 S3Client Bean이 겹치게 되면 빈을 오버라이딩할 수 없다는 BeanDefinitionOverrideException를 만나게 됩니다.

// main/AwsConfig.java
@Configuration
public class AwsConfig {
	
    @Bean
    public AmazonS3 s3Client() {
        ...
    }
}

// test/LocalStackS3Config.java
@TestConfiguration
public class LocalStackS3Config {

    DockerImageName localstackImage = DockerImageName.parse("localstack/localstack:0.11.3");

    @Bean
    public AmazonS3 s3Client() {
        LocalStackContainer localStackContainer = new LocalStackContainer(localstackImage)
                .withServices(LocalStackContainer.Service.S3);

        return AmazonS3ClientBuilder.standard()
                .withEndpointConfiguration(
                        localStackContainer.getEndpointConfiguration(LocalStackContainer.Service.S3)
                )
                .withCredentials(localStackContainer.getDefaultCredentialsProvider())
                .build();
    }
}

 

스프링 부트 2.1 이상부터 기본적으로 Bean Overridng이 비활성화되기 때문에 발생하는 문제입니다. 이는 해당 설정을 테스트 프로퍼티 파일에 추가시켜주면 됩니다.

// application.yml
spring:
  main:
    allow-bean-definition-overriding: true

 

4. initMethod / destroyMethod

@Bean 어노테이션에는 Bean이 초기화될 때, 소멸될 때 수행할 메소드를 지정할 수 있습니다. Localstack은 AWS 클라우드 리소스의 이미지를 도커 컨테이너로 제공하는 라이브러리라고 했었죠? 그렇다면 S3 Client가 초기화될 때 S3 컨테이너가 생성된 상태여야 합니다. S3 Client가 소멸될 땐 컨테이너를 종료시켜야하구요.

이 때 필요한게 @Bean 어노테이션의 initMethod, destoryMethod 프로퍼티입니다. 처음에 다른 팀원분의 테스트 코드를 보면서 따라했는데, initMethod / destoryMethod를 빼먹은 탓에 테스트가 계속 실패했었습니다. LocalStackContainer은 GenericContainer<LocalStackContainer>를 extends하고 있는데, 제네릭 컨테이너에는 start, stop 메소드가 정의되어 있습니다.

@TestConfiguration
public class LocalStackS3Config {

    @Bean(initMethod = "start", destroyMethod = "stop")
    public LocalStackContainer localStackContainer() {
        DockerImageName localstackImage = DockerImageName.parse("localstack/localstack:0.11.3");

        return new LocalStackContainer(localstackImage)
                .withServices(LocalStackContainer.Service.S3);
    }
}

 

5. @Import와 @ContextConfiguration

@DisplayName("공지사항 컨트롤러")
@Transactional
// @Import(LocalStackS3Config.class)
// @ContextConfiguration(classes = LocalStackS3Config.class)
@SpringBootTest
public class NoticeControllerTest {

	...
}

저는 해당 TestConfiguration을 적용하기 위해 @Import를 사용했었습니다. 그런데 팀원 분은 @ContextConfiguration을 사용하셨더라구요. 둘 중 무엇을 사용해도 테스트가 정상적으로 수행되었는데, 둘의 차이가 뭔지 궁금해졌습니다.

 

 

@Import vs @ContextConfiguration in Spring

Is there any difference in usage of the annotations? Both the annotations allow to use multiple @Configuration classes to create an ApplicationContext. From their docs @ContextConfiguration seems t...

stackoverflow.com

@Import
- Indicates one or more component classes to import, typically @Configuration class
- Spring XML의 <import />와 동일
- Import된 Bean은 @Autowired로 필드 주입받을 수 있다.

 

@ContextConfiguration
- Defines class-level metadata that is used to determine how to load and configure an ApplicationContext for integeration tests.

이 설명만 읽고선 이해가 안되더라구요. 둘 다 Configuration을 로딩하는 것은 똑같지 않나 싶었습니다.

 

 

@Import vs @ContextConfiguration in Spring

Is there any difference in usage of the annotations? Both the annotations allow to use multiple @Configuration classes to create an ApplicationContext. From their docs @ContextConfiguration seems t...

stackoverflow.com

그래서 더 찾아보니 둘의 use case가 다르다고 설명하고 있었습니다 (@Import and @ContextConfiguration are for different use cases and cannot be used interchangeability)

@Import
-
The @Import is only useful for importing other @Configuration files and is only useful (and afaik) and functional on @Configuration classes.
- @Import는 컴포넌트 스캐닝되지 않는 @Configuration 클래스에 대해 효과적일 수 있다.
- @Import는 기존 ApplicationContext에 해당 Component들을 추가하겠다는 뜻

 

@ContextConfiguration
- Import와 유사하지만, javadoc에도 나와있듯이 통합 테스트 환경에서만 유효하다.

 

여러 문서를 찾아봐도 기능적 차이는 명확하게 모르겠지만, 주 용도가 다른 것 같습니다. 아직도 정확하게 이해하지 못했으니 알고 계신 분은 댓글을 달아주세요 😀

 

 

 

 

추가학습

Testcontainers 공식 문서

 

Testcontainers

 Testcontainers About Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container. Testcontainers make the followi

www.testcontainers.org

 

Testcontainers - Localstack 모듈 설명 문서

 

Localstack Module - Testcontainers

 Localstack Module Testcontainers module for the Atlassian's LocalStack, 'a fully functional local AWS cloud stack'. Usage example Running LocalStack as a stand-in for AWS S3 during a test: DockerImageName localstackImage = DockerImageName.parse("locals

www.testcontainers.org

 

우아한형제들 기술 블로그 - Localstack 활용기

 

LocalStack을 활용한 Integration Test 환경 만들기 - 우아한형제들 기술 블로그

안녕하세요. 주문마케팅서비스팀 송정훈입니다. 이 글에서는 AWS 서비스를 활용하는 웹 어플리케이션이 클라우드 환경이 아닌, 로컬개발환경에서 쉽게 실행하고 테스트할 수 있는 방법에 대해

woowabros.github.io

 

댓글