LocalStack SNS/SQS
업데이트:
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를 확인합니다.
위의 빨간 네모 부분이 도커 컨테이너의 ID입니다.
$ docker exec it ${Docker 컨테이너 ID} /bin/bash
위의 명령어를 입력해서 LocalStack 도커 컨테이너 안으로 접속합니다.
여기서 입력하는 ${Docker 컨테이너 ID}
는, 위와 같이 다른 도커 컨테이너 ID들과 구분될 수 있는 앞자리 몇 개만 입력해도 됩니다.
이제 LocalStack 도커 컨테이너 안에서, 필요한 SNS/SQS를 만들고 발행-구독 연결이 필요한 SNS/SQS를 연결해 보겠습니다.
SNS 생성
$ aws --endpoint-url=http://localhost:4566 sns create-topic --name LocalMessaging-SNS
저는 위와 같이 YAML 파일에 설정한 SNS 이름으로 SNS를 만들었습니다.
$ aws --endpoint-url=http://localhost:4566 sns list-topics
그리고 위의 명령어를 입력해, 잘 생성되었는지 현재 생성되어있는 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
SNS와 똑같이, 위의 명령어들로 SQS를 두 개 생성합니다.
$ aws --endpoint-url=http://localhost:4566 sqs list-queues
그리고 위의 명령어를 입력해, 잘 생성되었는지 현재 생성되어있는 SQS들의 목록을 확인해 봅니다.
SNS - SQS 구독 연결
- LocalMessaging-SQS-From-SNS : Application -> SNS -> SQS -> Application
- LocalMessaging-Only-SQS : Application -> SQS -> Application
위와 같은 용도로 사용할 것이기 때문에,
앞에서 생성한 LocalMessaging-SNS
를 LocalMessaging-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
위의 명령어로 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 파일 설정입니다.
- 기본적인 AWS 설정
- SNS, SQS 이름 설정
- 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
- Spring 애플리케이션에서 메세지를 SNS로 전송
- 해당 SNS를 구독중인 SQS가 메세지를 수신
- 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를 실행시켜보면,
위와같이 메세지 전송/수신이 정상적으로 잘 되는것을 확인할 수 있습니다.
Application -> SQS -> Application
- Spring 애플리케이션에서 메세지를 SQS로 전송
- 해당 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를 실행시켜보면,
위와같이 메세지 전송/수신이 정상적으로 잘 되는것을 확인할 수 있습니다.
마치며
이처럼 AWS 클라우드 환경에서 제공하는 SNS/SQS 등의 서비스를 LocalStack을 사용하면 로컬에서 테스트 할 수 있습니다.
클라우드 환경에 매번 배포하면서 테스트하면 배포시간이 오래 걸리고, 여러 테스트를 위한 커밋들 때문에 커밋이 더러워 질 수 있는데요.
LocalStack을 활용하면 로컬 환경에서 빠른 테스트 피드백을 받을 수 있고, 충분한 테스트를 한 뒤에 한 번의 깔끔한 커밋을 남길 수 있는 장점이 있습니다.
감사합니다.
댓글남기기