업데이트:


AWS의 SNS, SQS 등의 서비스를 사용하는 경우 로컬에서 테스트하기가 쉽지 않습니다.

LocalStack을 사용하면 로컬 환경에서 AWS의 SNS, SQS등의 서비스를 클라우드 환경과 동일하게 띄우고, 테스트 할 수 있습니다.

지금부터 LocalStack으로 AWS의 SNS, SQS를 사용한 메세징 테스트 방법을 알아보겠습니다.

예시에 사용된 모든 코드들은 여기에 있습니다.


구현 방향

  • Application -> SNS -> SQS -> Application
  • Application -> SQS -> Application

위와 같이 메세지를 전송하고 수신하는 인프라와 애플리케이션을 만들어 보겠습니다.


LocalStack

Docker

docker-compose.yml

version: '3.0'

services:
  localstack:
    container_name: taeheekim-blog-codes-localstack
    image: localstack/localstack
    healthcheck:
      test: awslocal sns list-topics && awslocal sqs list-queues
      interval: 3s
      timeout: 10s
    environment:
      - SERVICES=sns,sqs
      - AWS_ACCESS_KEY_ID=accesskey
      - AWS_SECRET_ACCESS_KEY=secretkey
      - AWS_DEFAULT_REGION=ap-northeast-2
      - HOSTNAME_EXTERNAL=localhost
      - TMPDIR=/private$TMPDIR
    ports:
      - "4566:4566"

LocalStack으로 SNS, SQS를 사용하기 위한 docker-compose 파일 입니다.

terminal에서 위의 파일이 있는 위치로 가서

$ docker-compose up -d

위의 명령어를 입력해 docker-compose 파일의 컨테이너들을 실행시킵니다.


컨테이너 실행이 완료되었으면,

$ docker ps

를 입력해, LocalStack 컨테이너의 ID를 확인합니다.

image

위의 빨간 네모 부분이 도커 컨테이너의 ID입니다.


$ docker exec it ${Docker 컨테이너 ID} /bin/bash

위의 명령어를 입력해서 LocalStack 도커 컨테이너 안으로 접속합니다.

스크린샷 2022-10-31 오전 2 09 01

여기서 입력하는 ${Docker 컨테이너 ID}는, 위와 같이 다른 도커 컨테이너 ID들과 구분될 수 있는 앞자리 몇 개만 입력해도 됩니다.


이제 LocalStack 도커 컨테이너 안에서, 필요한 SNS/SQS를 만들고 발행-구독 연결이 필요한 SNS/SQS를 연결해 보겠습니다.


SNS 생성

$ aws --endpoint-url=http://localhost:4566 sns create-topic --name LocalMessaging-SNS

스크린샷 2022-10-31 오전 2 12 30

저는 위와 같이 YAML 파일에 설정한 SNS 이름으로 SNS를 만들었습니다.

$ aws --endpoint-url=http://localhost:4566 sns list-topics

image

그리고 위의 명령어를 입력해, 잘 생성되었는지 현재 생성되어있는 SNS의 목록을 확인해 봅니다.


SQS 생성

$ aws --endpoint-url=http://localhost:4566 sqs create-queue --queue-name LocalMessaging-SQS-From-SNS
$ aws --endpoint-url=http://localhost:4566 sqs create-queue --queue-name LocalMessaging-Only-SQS

image

SNS와 똑같이, 위의 명령어들로 SQS를 두 개 생성합니다.

$ aws --endpoint-url=http://localhost:4566 sqs list-queues

image

그리고 위의 명령어를 입력해, 잘 생성되었는지 현재 생성되어있는 SQS들의 목록을 확인해 봅니다.


SNS - SQS 구독 연결

  • LocalMessaging-SQS-From-SNS : Application -> SNS -> SQS -> Application
  • LocalMessaging-Only-SQS : Application -> SQS -> Application

위와 같은 용도로 사용할 것이기 때문에,

앞에서 생성한 LocalMessaging-SNSLocalMessaging-SQS-From-SNS 가 구독하도록 설정해야 합니다.

$ aws --endpoint-url=http://localhost:4566 sns subscribe --topic-arn arn:aws:sns:ap-northeast-2:000000000000:LocalMessaging-SNS --protocol sqs --notification-endpoint http://localhost:4566/000000000000/LocalMessaging-SQS-From-SNS

위의 명령어로 구독을 설정합니다.

$ aws --endpoint-url=http://localhost:4566 sns list-subscriptions

image

위의 명령어로 SNS/SQS의 발행-구독 연결이 잘 되었는지 확인해 봅니다.


Spring Application

먼저, Spring AWS를 사용하기 위해서 필요한 설정들을 해 보겠습니다.

Gradle 설정

dependencies {
    ...
      
    // https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-aws-messaging
    implementation 'org.springframework.cloud:spring-cloud-starter-aws-messaging:2.2.6.RELEASE'
    // https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-aws-autoconfigure
    implementation 'org.springframework.cloud:spring-cloud-aws-autoconfigure:2.2.6.RELEASE'
     
    ...
}

Spring Cloud 의존성을 사용해 SNS, SQS를 사용할 것이기 때문에, 위와 같이 gradle 의존성을 추가시켰습니다.


YAML 설정

cloud:
  aws:
    region:
      static: ap-northeast-2
    stack:
      auto: false
    credentials:
      access-key: accesskey
      secret-key: secretkey

aws:
  sns:
    messaging: LocalMessaging-SNS
  sqs:
    messaging:
      from-SNS: LocalMessaging-SQS-From-SNS
      only-SQS: LocalMessaging-Only-SQS

logging:
  level:
    com:
      amazonaws:
        util:
          EC2MetadataUtils: error

YAML 파일 설정입니다.

  1. 기본적인 AWS 설정
  2. SNS, SQS 이름 설정
  3. AWS EC2 환경이 아니라 로컬 환경에서 띄울 때 나는 로그를 보지 않기 위한 로깅 레벨 설정

순 입니다.


JVM 옵션 설정

위의 YAML 로깅 레벨 설정을 하면 로컬 환경에서 띄울 때 AWS EC2 관련 메타데이터에 대한 로그는 나오지 않지만

AWS EC2 메타데이터를 읽어오는 작업을 하기 때문에 Spring 애플리케이션 구동시 약간의 멈춤이 있는데요,

이를 없애기 위해 아래와 같이 JVM 옵션을 추가했습니다.

@SpringBootApplication
public class LocalstackApplication {

    static {
        System.setProperty("com.amazonaws.sdk.disableEc2Metadata", "true");
    }

    public static void main(String[] args) {
        SpringApplication.run(LocalstackApplication.class, args);
    }

}


AwsConfiguration 빈 설정

@Configuration
public class AwsConfiguration {

    @Configuration
    @ConditionalOnMissingAwsCloudEnvironment
    public static class LocalConfiguration {

        private static final AwsClientBuilder.EndpointConfiguration DEFAULT_ENDPOINT_CONFIGURATION = new AwsClientBuilder.EndpointConfiguration(
                "http://localhost:4566",
                "ap-northeast-2"
        );

        @Primary
        @Bean
        public AmazonSNSAsync amazonSNSAsync(AWSCredentialsProvider awsCredentialsProvider) {
            return AmazonSNSAsyncClientBuilder.standard()
                    .withEndpointConfiguration(DEFAULT_ENDPOINT_CONFIGURATION)
                    .withCredentials(awsCredentialsProvider)
                    .build();
        }

        @Primary
        @Bean
        public AmazonSQSAsync amazonSQSAsync(AWSCredentialsProvider awsCredentialsProvider) {
            return AmazonSQSAsyncClientBuilder.standard()
                    .withCredentials(awsCredentialsProvider)
                    .withEndpointConfiguration(DEFAULT_ENDPOINT_CONFIGURATION)
                    .build();
        }
    }

    @Bean
    public NotificationMessagingTemplate notificationMessagingTemplate(AmazonSNSAsync amazonSNSAsync) {
        return new NotificationMessagingTemplate(amazonSNSAsync);
    }

    @Bean
    public QueueMessagingTemplate queueMessagingTemplate(AmazonSQSAsync amazonSQSAsync, MessageConverter messageConverter) {
        return new QueueMessagingTemplate(amazonSQSAsync, (ResourceIdResolver) null, messageConverter);
    }

    @Bean
    public MessageConverter messageConverter() {
        return new MappingJackson2MessageConverter();
    }
}

AWS SNS, SQS를 사용하기 위한 설정들과 메세지 컨버터 설정 입니다.


Message 객체

@ToString
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Message {

    private MessageData data;

    @ToString
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor(access = AccessLevel.PRIVATE)
    private static class MessageData {

        private String name;
        private String nickname;
        private Integer age;
    }
}

위와 같이 전송 및 수신할 Message 객체를 만들었습니다.


Application -> SNS -> SQS -> Application

  1. Spring 애플리케이션에서 메세지를 SNS로 전송
  2. 해당 SNS를 구독중인 SQS가 메세지를 수신
  3. SQS를 컨슈밍하고 있는 Spring 애플리케이션에서 SQS로부터 메세지를 수신

위와 같이 동작하는 애플리케이션을 만들어 보겠습니다.


Controller

@Slf4j
@RestController
@RequiredArgsConstructor
public class MessagingController {

    private final AwsSnsMessagePublisher awsSnsMessagePublisher;

    @PostMapping("/api/v1/aws/sns/messaging/publish")
    public String publishToSns(@RequestBody Message message) {
        awsSnsMessagePublisher.publish(message);
        return "SNS에 메세지 전송을 완료했습니다.";
    }
}

테스트를 위한 API Controller를 위와 같이 만들었습니다.


SNS Publisher

@Slf4j
@Service
@RequiredArgsConstructor
public class AwsSnsMessagePublisher implements InitializingBean {

    private final NotificationMessagingTemplate notificationMessagingTemplate;

    @Value("${aws.sns.messaging}")
    private String destination;

    public void publish(Message message) {
        log.info("AWS SNS에 메세지를 전송합니다. destination={}, payload={}", destination, message);
        notificationMessagingTemplate.sendNotification(destination, message, null);
        log.info("AWS SNS에 메세지 전송을 완료했습니다. destination={}, payload={}", destination, message);
    }

    @Override
    public void afterPropertiesSet() {
        Assert.hasText(destination, "AwsSnsMessagePublisher의 destination 값은 빈 문자열일 수 없습니다.");
        log.info("AwsSnsMessagePublisher destination={}", destination);
    }
}

SNS에 메세지를 전송하는 Publisher를 위와 같이 만들었습니다.


SQS Listener

@Slf4j
@Component
public class AwsSqsMessageListener {

    @SqsListener(value = "${aws.sqs.messaging.from-SNS}", deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
    public void listenSnsMessage(@NotificationMessage Message message) {
        log.info("SNS 발행하고, SQS에서 수신한 메세지={}", message);
    }
}

SNS로 발행한 메세지를 구독하는 SQS로부터 메세지를 받는 리스너를 위와 같이 만들었습니다.


Test

### Publish Message To AWS SNS
POST http://localhost:8080/api/v1/aws/sns/messaging/publish
Content-Type: application/json

{
    "data": {
        "name": "SNS",
        "nickname": "발송",
        "age": 11
    }
}

Spring Application을 구동시킨 뒤, 위의 .http 파일의 API를 실행시켜보면,

image

위와같이 메세지 전송/수신이 정상적으로 잘 되는것을 확인할 수 있습니다.


Application -> SQS -> Application

  1. Spring 애플리케이션에서 메세지를 SQS로 전송
  2. 해당 SQS를 컨슈밍하고 있는 Spring 애플리케이션에서 SQS로부터 메세지를 수신

위와 같이 동작하는 애플리케이션을 만들어 보겠습니다.


Controller

@Slf4j
@RestController
@RequiredArgsConstructor
public class MessagingController {

    private final AwsSqsMessagePublisher awsSqsMessagePublisher;

    @PostMapping("/api/v1/aws/sqs/messaging/publish")
    public String publishToSqs(@RequestBody Message message) {
        awsSqsMessagePublisher.publish(message);
        return "SQS에 메세지 전송을 완료했습니다.";
    }
}

테스트를 위한 API Controller를 위와 같이 만들었습니다.


SQS Publisher

@Slf4j
@Service
@RequiredArgsConstructor
public class AwsSqsMessagePublisher implements InitializingBean {

    private final QueueMessagingTemplate queueMessagingTemplate;
    private final ObjectMapper objectMapper;

    @Value("${aws.sqs.messaging.only-SQS}")
    private String destination;

    public void publish(Message message) {
        log.info("AWS SQS에 메세지를 전송합니다. destination={}, payload={}", destination, message);
        String payload = getPayload(message);
        queueMessagingTemplate.send(destination, MessageBuilder.withPayload(payload).build());
        log.info("AWS SQS에 메세지 전송을 완료했습니다. destination={}, payload={}", destination, message);
    }

    private String getPayload(Message message) {
        try {
            return objectMapper.writeValueAsString(message);
        } catch (Exception e) {
            log.error("SQS 메세지 전송에 실패했습니다. message={}", message, e);
            throw new RuntimeException(e);
        }
    }

    @Override
    public void afterPropertiesSet() {
        Assert.hasText(destination, "AwsSqsMessagePublisher의 destination 값은 빈 문자열일 수 없습니다.");
        log.info("AwsSqsMessagePublisher destination={}", destination);
    }
}

SQS에 메세지를 전송하는 Publisher를 위와 같이 만들었습니다.


SQS Listener

@Slf4j
@Component
public class AwsSqsMessageListener {

    @SqsListener(value = "${aws.sqs.messaging.only-SQS}", deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
    public void listenOnlyMessage(@NotificationMessage Message message) {
        log.info("SQS에서 수신한 메세지={}", message);
    }
}

SQS로부터 메세지를 받는 리스너를 위와 같이 만들었습니다.


Test

### Publish Message To AWS SQS
POST http://localhost:8080/api/v1/aws/sqs/messaging/publish
Content-Type: application/json

{
    "data": {
        "name": "SQS",
        "nickname": "발송",
        "age": 22
    }
}

Spring Application을 구동시킨 뒤, 위의 .http 파일의 API를 실행시켜보면,

image

위와같이 메세지 전송/수신이 정상적으로 잘 되는것을 확인할 수 있습니다.


마치며

이처럼 AWS 클라우드 환경에서 제공하는 SNS/SQS 등의 서비스를 LocalStack을 사용하면 로컬에서 테스트 할 수 있습니다.

클라우드 환경에 매번 배포하면서 테스트하면 배포시간이 오래 걸리고, 여러 테스트를 위한 커밋들 때문에 커밋이 더러워 질 수 있는데요.

LocalStack을 활용하면 로컬 환경에서 빠른 테스트 피드백을 받을 수 있고, 충분한 테스트를 한 뒤에 한 번의 깔끔한 커밋을 남길 수 있는 장점이 있습니다.

감사합니다.


참고


댓글남기기