๊ฐ์
์ด๋ฒ ๊ธ์์๋ ์ด์ ๊ฒ์๊ธ 'TestContainer๋ก ํตํฉ ํ ์คํธ ํ๊ฒฝ ๊ตฌ์ถํ๊ธฐ'์ ์ฐ์ฅ์ ์ผ๋ก, LocalStack์ ์ด์ฉํ์ฌ AWS์ S3 ํ ์คํธ ํ๊ฒฝ ๊ตฌ์ถ์ ๋ํด ์์๋ณด๊ฒ ์ต๋๋ค. ๊ด๋ จ ์ฝ๋๋ ๊นํ๋ธ์์ ํ์ธํ์ค ์ ์์ต๋๋ค.
LocalStack์ด๋?
LocalStack์ AWS์ ๋ค์ํ ์๋น์ค๋ค์ ๋ก์ปฌ ๊ฐ๋ฐ ํ๊ฒฝ์์ ๋ชจ๋ฐฉํ์ฌ ์ฌ์ฉํ ์ ์๊ฒ ํด์ฃผ๋ ์คํ์์ค์ ๋๋ค. ์ด๋ฅผ ํ์ฉํ๋ฉด, AWS์ ์ฃผ์ ์๋น์ค๋ค์ ์ค์ ํด๋ผ์ฐ๋ ํ๊ฒฝ์ด ์๋ ๋ก์ปฌ์์ ์๋ฎฌ๋ ์ด์ ํ๋ฉฐ ๊ฐ๋ฐ ๋ฐ ํ ์คํธ๋ฅผ ํ ์ ์์ต๋๋ค.
LocalStack์ AWS์ ๋ค์ํ ์๋น์ค, ํนํ S3, Lambda, DynamoDB, API Gateway, Kinesis, SQS ๋ฑ์ ์ฃผ์ ์๋น์ค๋ค์ ์ง์ํ๋ฉฐ, Docker ํ๊ฒฝ์์ ์คํ๋๊ธฐ ๋๋ฌธ์ ์๋น์ค๋ฅผ ์์ํ๊ฑฐ๋ ์ข ๋ฃํ๋ ๊ฒ์ด ๋งค์ฐ ๊ฐํธํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ๋ชจ๋ํ ๋ ๊ตฌ์กฐ๋ก ์ค๊ณ๋์ด ์์ด, S3๋ Lambda์ ๊ฐ์ ํน์ ์๋น์ค๋ง ์ ํ์ ์ผ๋ก ์คํํ ์ ์์ต๋๋ค.
๋ณดํต ๋ก์ปฌ ๊ฐ๋ฐ ํ๊ฒฝ์์ AWS ์๋น์ค๋ฅผ ์ฌ์ฉํ๋ฉด ์๋์ ๊ฐ์ ๋ฌธ์ ์ ์ด ์ข ์ข ๋ฐ์ํ๊ณค ํ๋๋ฐ, ์ด๋ด ๋ LocalStack์ ์ฌ์ฉํ๋ค๋ฉด ์ข์ ์ ํ์ด ๋ ์ ์์ต๋๋ค.
- ๋ณด์ ๋ฌธ์ : ๋ก์ปฌ์์ AWS ์๋น์ค๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด accesskey์ secretkey๋ฅผ ์ ์ธ ๋ฐ ๊ด๋ฆฌํด์ผ ํ๋ค. ์ด๋ ํค ์ ๋ณด๊ฐ ์ฝ๋์ ํฌํจ๋๋ฉด ๋ณด์์ ๋ฌธ์ ๊ฐ ๋ ์ ์๋ค.
- ํ ์คํธ ํ๊ฒฝ์ ํ๊ณ: ํน์ ๋ณด์ ์กฐ์น๋ก ์ธํด ๋ก์ปฌ์์ accesskey์ secretkey ์ฌ์ฉ์ด ์ ํ๋ ๊ฒฝ์ฐ, ์ค์ AWS ํ๊ฒฝ์ ๋ฐฐํฌํ๊ธฐ ์ ๊น์ง๋ ํ ์คํธ๋ฅผ ์งํํ๊ธฐ ์ด๋ ต๋ค.
- ํ ์คํธ ๊ฒฉ๋ฆฌ์ ๋ถ์ฌ: AWS์์์ ์๋น์ค๋ค์ ๋ค์ํ ์ธ์คํด์ค์์ ๋์ ์ ๊ทผ ๋ฐ ์ฌ์ฉ๋ ์ ์๊ธฐ ๋๋ฌธ์, ์์ ํ ๊ฒฉ๋ฆฌ๋ ํ ์คํธ ํ๊ฒฝ์ ๊ตฌ์ถํ๋ ๊ฒ์ด ์ด๋ ต๋ค.
- ๋น์ฉ ๋ฌธ์ : ๋ก์ปฌ ๊ฐ๋ฐ ๊ณผ์ ์์ AWS ์๋น์ค๋ฅผ ๋ฐ์กฑ์ ์ผ๋ก ์์ฑ ๋ฐ ์ญ์ ํ๋ ํ์๋ ์ถ๊ฐ์ ์ธ ๋น์ฉ ๋ถ๋ด์ ์ด๋ํ ์ ์๋ค.
TestContainers๋ก LocalStack ๊ตฌ์ถํ๊ธฐ
LocalStack ์ปจํ ์ด๋๋ฅผ ์คํํ๋ ๋ฐฉ๋ฒ์ ์ฌ๋ฌ ๊ฐ์ง๊ฐ ์์ง๋ง, ์ ๋ ์ด์ ๊ฒ์๊ธ์ ์ฐ์ฅ์ ์ผ๋ก TestContainers๋ก ๊ตฌ์ถํด๋ณด๊ณ ์ ํฉ๋๋ค. ์ด ๋ถ๋ถ์ ๊ธฐ์ ๋ค๋ง๋ค ๋ค์ํ๊ฒ ๊ตฌ์ถํ๊ณ ์๋ ๊ฒ ๊ฐ์ผ๋ ํ๋ฒ ํ์ธํ์๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
- ๋ฐฐ๋ฏผ ์ฃผ๋ฌธ๋ง์ผํ ์๋น์คํ - TestContainers ์ฌ์ฉ: https://techblog.woowahan.com/2638
- ์ธํ๋ฉ - docker-compose ์ฌ์ฉ: https://tech.inflab.com/202202-integration-test-with-localstack
build.gradle
dependencies {
implementation("commons-io:commons-io:2.13.0")
implementation(platform("software.amazon.awssdk:bom:2.20.136"))
implementation("software.amazon.awssdk:aws-core")
implementation("software.amazon.awssdk:sdk-core")
implementation("software.amazon.awssdk:sts")
implementation("software.amazon.awssdk:s3")
testImplementation("org.testcontainers:testcontainers:1.19.0")
testImplementation("org.testcontainers:localstack:1.19.0")
}
application.yml
cloud:
aws:
endpoint: localhost:4566
LocalStack 0.11.0 ์ด์ ๋ฒ์ ์์๋ ๊ฐ AWS ์๋น์ค๋ง๋ค ๋ณ๋์ ํฌํธ๊ฐ ํ ๋น๋์ด ์์์ต๋๋ค. ํ์ง๋ง 0.11.0 ๋ฒ์ ๋ถํฐ๋ ๋ชจ๋ ์๋น์ค์ ์๋ํฌ์ธํธ๋ฅผ 'http://localhost:4566'์ผ๋ก ํต์ผํ์ฌ ๋ณต์ก์ฑ์ ์ค์์ต๋๋ค. ๋ฐ๋ผ์ AWS์ ์๋ ํฌ์ธํธ๋ฅผ localhost:4566์ผ๋ก ์ง์ ํฉ๋๋ค.
์ด์ ๋ฒ์ ์์๋ ์๋์ฒ๋ผ ๊ฐ AWS ์๋น์ค๋ง๋ค ๋ค๋ฅธ ํฌํธ๋ก ๋งคํ๋์ด ์์์ต๋๋ค.
- API Gateway at http://localhost:4567
- Kinesis at http://localhost:4568
- DynamoDB at http://localhost:4569
- DynamoDB Streams at http://localhost:4570
- S3 at http://localhost:4572
- Firehose at http://localhost:4573
- Lambda at http://localhost:4574
- SNS at http://localhost:4575
- SQS at http://localhost:4576
- Redshift at http://localhost:4577
- Elasticsearch Service at http://localhost:4578
- SES at http://localhost:4579
- Route53 at http://localhost:4580
- CloudFormation at http://localhost:4581
- CloudWatch at http://localhost:4582
- SSM at http://localhost:4583
- SecretsManager at http://localhost:4584
- StepFunctions at http://localhost:4585
- CloudWatch Logs at http://localhost:4586
- EventBridge (CloudWatch Events) at http://localhost:4587
- STS at http://localhost:4592
- IAM at http://localhost:4593
- EC2 at http://localhost:4597
- KMS at http://localhost:4599
- ACM at http://localhost:4619
S3Config.java
@Configuration
public class S3Config {
@Value("${cloud.aws.endpoint}")
private String awsEndpoint;
private static final String AWS_ACCESS_KEY = "foo";
private static final String AWS_SECRET_KEY = "bar";
@Bean
public AwsCredentialsProvider awsCredentialsProvider() {
return AwsCredentialsProviderChain.builder()
.reuseLastProviderEnabled(true)
.credentialsProviders(List.of(
DefaultCredentialsProvider.create(),
StaticCredentialsProvider.create(AwsBasicCredentials.create(AWS_ACCESS_KEY, AWS_SECRET_KEY))
))
.build();
}
@Bean
public S3Client s3Client() {
return S3Client.builder()
.credentialsProvider(awsCredentialsProvider())
.region(Region.AP_NORTHEAST_2)
.endpointOverride(URI.create(awsEndpoint))
.build();
}
}
AWS_ACCESS_KEY์ AWS_SECRET_KEY๋ LocalStack์ ์ฌ์ฉํ๋ ๋ก์ปฌ ํ๊ฒฝ์์์ ์ธ์ฆ ์ ๋ณด๋ก, ์์ ๊ฐ(foo์ bar)์ผ๋ก ์ค์ ํ์ต๋๋ค. ์ค์ ์ด์ ํ๊ฒฝ์์๋ ์ค์ AWS์ ์ธ์ฆ ์ ๋ณด๋ฅผ ์ฌ์ฉํด์ผ ํ์ง๋ง, LocalStack์์๋ ์ค์ ์ธ์ฆ ๊ณผ์ ์ด ํ์ํ์ง ์๊ธฐ ๋๋ฌธ์ ์์์ ๊ฐ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
S3Service.java
@Service
public class S3Service {
private final S3Client s3Client;
@Autowired
public S3Service(S3Client s3Client) {
this.s3Client = s3Client;
}
public void putFile(String bucket, String key, File file) {
s3Client.putObject(builder -> builder.bucket(bucket).key(key), RequestBody.fromFile(file));
}
public File getFile(String bucket, String key) {
var file = new File("build/output/getFile.txt");
var res = s3Client.getObject(builder -> builder.bucket(bucket).key(key));
try {
FileUtils.writeByteArrayToFile(file, res.readAllBytes());
} catch (Exception e) {
System.err.println("Error writing to file: " + e.getMessage());
}
return file;
}
}
IntegrationTest.java
@Disabled
@SpringBootTest
@ContextConfiguration(initializers = IntegrationTest.IntegrationTestInitializer.class)
public class IntegrationTest {
private static final LocalStackContainer AWS_CONTAINER = new LocalStackContainer(DockerImageName.parse("localstack/localstack:0.11.2"))
.withServices(LocalStackContainer.Service.S3);
@BeforeAll
public static void setupContainers() {
AWS_CONTAINER.start();
}
static class IntegrationTestInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(@NotNull ConfigurableApplicationContext applicationContext) {
Map<String, String> properties = new HashMap<>();
setAwsProperties(properties);
TestPropertyValues.of(properties).applyTo(applicationContext);
}
private void setAwsProperties(Map<String, String> properties) {
try {
AWS_CONTAINER.execInContainer(
"awslocal",
"s3api",
"create-bucket",
"--bucket",
"test-bucket");
properties.put("cloud.aws.endpoint", AWS_CONTAINER.getEndpoint().toString());
} catch (Exception e) {
// nothing
}
}
}
}
S3ServiceTest.java
public class S3ServiceTest extends IntegrationTest {
private static final String TEST_BUCKET = "test-bucket";
private static final String SAMPLE_KEY = "sampleObject.txt";
@Autowired
private S3Service s3Service;
@Test
public void s3PutAndGetTest() throws Exception {
// given
File sampleFile = createSampleFile("hello world");
// when
s3Service.putFile(TEST_BUCKET, SAMPLE_KEY, sampleFile);
// then
File resultFile = s3Service.getFile(TEST_BUCKET, SAMPLE_KEY);
Assertions.assertIterableEquals(
FileUtils.readLines(sampleFile, StandardCharsets.UTF_8),
FileUtils.readLines(resultFile, StandardCharsets.UTF_8)
);
// finish
sampleFile.delete();
}
private File createSampleFile(String content) throws IOException {
File tempFile = File.createTempFile("temp", ".txt");
try (FileWriter writer = new FileWriter(tempFile)) {
writer.write(content);
}
return tempFile;
}
}
ํ ์คํธ ์ํ
๊ฒฐ๋ก
LocalStack์ ํ์ฉํ๋ฉด AWS์ ๋ค์ํ ์๋น์ค๋ฅผ ๋ก์ปฌ ํ๊ฒฝ์์ ์์ ํ๊ณ ํจ๊ณผ์ ์ผ๋ก ์๋ฎฌ๋ ์ด์ ํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๊ฐ๋ฐ์๋ ์ค์ ํด๋ผ์ฐ๋ ํ๊ฒฝ์์ ๋ฐ์ํ ์ ์๋ ๋ค์ํ ๋ฌธ์ ์ ์ ๋ฏธ๋ฆฌ ์๋ฐฉํ๊ณ , ๋ ๋น ๋ฅด๊ณ ์์ ์ ์ธ ๊ฐ๋ฐ ๋ฐ ํ ์คํธ ํ๊ฒฝ์ ๊ตฌ์ถํ ์ ์์ต๋๋ค. ํนํ TestContainer์ ๊ฐ์ ๋๊ตฌ์ ๊ฒฐํฉํ๋ฉด ๋์ฑ ๊ฐ๋ ฅํ ํตํฉ ํ ์คํธ ํ๊ฒฝ์ ๋ง๋ค ์ ์์ต๋๋ค.
ํ ์คํธ ๊ด๋ จ ๊ฐ์ด ๋ณด๋ฉด ์ข์ ๊ธ
'BackEnd๐ฑ > Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
default method๋ก JpaRepository ์ข ๋ ์ฐ์ํ๊ฒ ์จ๋ณด๊ธฐ (0) | 2023.11.16 |
---|---|
@Scheduled ์ฌ์ฉํ ๋ ์ค๋ ๋ ์ค์ (0) | 2023.11.09 |
ArchUnit์ผ๋ก ์ํคํ ์ฒ ๊ฒ์ฌํ๊ธฐ (0) | 2023.10.28 |
TestContainer๋ก ํตํฉ ํ ์คํธ ํ๊ฒฝ ๊ตฌ์ถํ๊ธฐ (3) | 2023.10.15 |
๋ถ์ฐ๋ฝ์ผ๋ก ์ ์ฐฉ์ ์ด๋ฒคํธ ๊ตฌํํ๊ธฐ (3) | 2023.08.31 |
Spring Data Redis์ @Indexed ์ฌ์ฉ ์ ์ฃผ์์ (0) | 2023.08.07 |
๋๊ธ