๊ฐ์
์ด๋ฒ ๊ธ์์๋ ์ด์ ๊ฒ์๊ธ '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 |
Spring Data Redis์ @Indexed ์ฌ์ฉ ์ ์ฃผ์์ (0) | 2023.08.07 |
WebClient์์ ์๋ฌ ์ฒ๋ฆฌ์ ์ฌ์๋ํ๋ ๋ฐฉ๋ฒ (0) | 2023.08.03 |
๋๊ธ