๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
BackEnd๐ŸŒฑ/Spring

LocalStack์„ ํ™œ์šฉํ•œ AWS S3 ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์ถ•ํ•˜๊ธฐ

by ์•ˆ์ฃผํ˜• 2023. 10. 22.

๊ฐœ์š”

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ์ด์ „ ๊ฒŒ์‹œ๊ธ€ '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๋กœ ๊ตฌ์ถ•ํ•ด๋ณด๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ถ€๋ถ„์€ ๊ธฐ์—…๋“ค๋งˆ๋‹ค ๋‹ค์–‘ํ•˜๊ฒŒ ๊ตฌ์ถ•ํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ ๊ฐ™์œผ๋‹ˆ ํ•œ๋ฒˆ ํ™•์ธํ•˜์‹œ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

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 ์„œ๋น„์Šค๋งˆ๋‹ค ๋‹ค๋ฅธ ํฌํŠธ๋กœ ๋งคํ•‘๋˜์–ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

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์™€ ๊ฐ™์€ ๋„๊ตฌ์™€ ๊ฒฐํ•ฉํ•˜๋ฉด ๋”์šฑ ๊ฐ•๋ ฅํ•œ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

ํ…Œ์ŠคํŠธ ๊ด€๋ จ ๊ฐ™์ด ๋ณด๋ฉด ์ข‹์€ ๊ธ€

๋Œ“๊ธ€