[AWS + Spring] SNS 활용 : 이벤트 메시지 브로드캐스트 구현
다양한 기능이 유기적으로 연결된 서비스 환경에서 하나의 이벤트가 여러 시스템에 동시에 전달되어야 하는 상황이 자주 발생한다.
AWS SQS는 Point-to-Point 모델을 따르기 때문에, 하나의 메시지를 여러 수신자에게 전달하려면 큐마다 별도로 메시지를 전송하는 방식으로 구현해야 한다.
AWS SNS는 이러한 문제를 해결하기 위한 발행-구독(Pub/Sub) 방식의 메시지 서비스로, 하나의 메시지를 다양한 채널(SQS, Email 등)로 동시에 전파할 수 있다.
이번 글에서는 Spring 기반 애플리케이션에서 SNS를 활용하여 이벤트를 다양한 주체에 분산 전파하는 구조를 구현하는 방법을 정리해본다.
1. AWS 인프라 설정
- 1-1. SNS Topic 생성
- 1-2. SNS Subscription 생성 : SQS 구독 연결 & 이메일 구독 연결
2. SNS Publisher 애플리케이션 작성
- 2-1. 애플리케이션 의존성 및 환경설정
- 2-2. SNS 관련 클래스 작성
3. SQS Consumer 애플리케이션 수정
4. 결과 확인
5. SNS FIFO 사용 시 유의할 점
시나리오

지난 포스팅에서 주문 시스템과 배송 시스템 간의 비동기 통신을 위해 AWS SQS를 활용한 구조를 구현했다.
주문 시스템(Producer)은 주문 처리 이후 메시지를 SQS에 전송하고, 배송 시스템(Consumer)은 해당 메시지를 Polling 방식으로 수신하여 배송 전산 처리를 수행하는 구조였다.
이전에 구현했던 SQS 활용 애플리케이션을 SNS에 연동할 예정이기 때문에 SQS 설정과 관련해서 먼저 해당 포스팅을 참고하면 좋을 것 같다.
<함께 보기> [AWS + Spring] SQS 활용 : 메시지 큐 기반 비동기 통신 구현 (Standard & FIFO)
https://sjh9708.tistory.com/271
[AWS + Spring] SQS 활용 : 메시지 큐 기반 비동기 통신 구현 (Standard & FIFO)
현대의 시스템 아키텍쳐에서 시스템 간 데이터를 주고받을 때, 처리를 즉시 완료하지 않아도 되는 작업이라면 비동기 메시징 방식이 효과적으로 사용된다.특히 마이크로소프트 아키텍쳐를 위
sjh9708.tistory.com
이번에는 AWS SNS를 도입하여, 하나의 주문 이벤트를 여러 수신자에게 브로드캐스트하는 구조를 설계했다. Producer(주문 시스템)는 주문 처리 후 메시지를 SNS에 발행하고, SNS는 이 메시지를 구독하고 있는 SQS(배송 시스템)와 Email(운영자 알림) 구독자에게 동시에 전달한다.
SQS에 전달된 메시지는 Consumer(배송 시스템)에서 Polling 방식으로 수신되어 기존과 동일하게 배송 처리에 활용되며, 동시에 운영자에게는 신규 주문 발생 알림이 이메일로 전송된다.
1. AWS 인프라 설정
먼저 SNS의 주제 생성과 구독 설정을 비롯하여 필요한 AWS 인프라들을 구성하자.
1-1. SNS Topic 생성
SNS Topic를 만들어주었다. Topic은 SNS의 채널 단위다. 해당 토픽에 Message를 Publish하면 Subscribe한 모든 대상에게 메시지를 발송한다. 주제에 설정할 수 있는 주요 추가 기능들은 아래와 같다.
- 암호화 : SNS 메시지를 저장할 때 AWS KMS를 사용해 암호화하는 기능
- 액세스 정책 : 이 주제에 대해 누가 메시지를 게시하거나 구독할 수 있는지를 설정할 수 있다. 디폴트값은 같은 AWS 계정의 리소스들과 사용자들은 모두 접근이 가능하다.
- 데이터 보호 정책 : 민감 정보가 주제를 통해 전달되는 것을 제한할 수 있다.
1-2. SNS Subscription 생성
1-2-1. SQS 구독 연결
먼저 전송할 채널 중 하나인 SQS를 먼저 연동해보자. 메시지를 수신할 대상 큐를 이전 포스팅에서 만들었던 SQS Standard Queue를 지정하여 첫 번째 구독을 생성해주었다.
- 필터 정책 : 특정 조건을 만족하는 메시지만 수신하도록 세밀하게 제어 가능하다. 해당 포스팅에서는 필터링 설정을 별도로 하지는 않을 것이지만 이 기능을 통해 하나의 SNS 주제로부터 채널마다 서로 다른 메시지 조건을 설정해 효율적인 메시지 분배가 가능하다.
1-2-2. 이메일 구독 연결
두 번째로 전송할 채널인 이메일도 연동해주었다. 시나리오에 따라 운영자의 이메일을 넣어주었다.
- 필자가 SNS를 다루면서 해깔렸던 부분이 있었는데, SNS의 경우 이메일을 고정된 주소만 지정 가능하고 동적으로 지정할 수 없다는 사실이다. 즉 Message의 고객 이메일 필드에 따라 이메일을 전송하는 요구사항이 있다면 Lambda나 Application 단으로 연결시켜서 구현해야 한다.
<함께 보기> SNS의 개념과 구성 요소
https://sjh9708.tistory.com/270
[AWS] 메시지 서비스의 이해 : SQS & SNS & Kinesis
현대의 애플리케이션 아키텍처에서는 서비스 간 결합도를 낮추고 유연한 처리를 가능하게 하기 위해 메시지 큐(Message Queue)를 활용하는 경우가 많다. 특히 마이크로서비스 환경에서는 각 서비스
sjh9708.tistory.com
2. SNS Publisher 애플리케이션 작성
주문 시스템에 해당하는 Producer측의 Spring Application을 작성해 볼 예정이다. SQS Producer Application과 유사한 형태의 요구사항을 구현할 예정이다.
- 주문 완료 처리를 하는 API 엔드포인트 생성
- API 호출 시 SNS Client를 통해서 SNS에 Message를 전달
- Spring Application은 3.4.5를 사용
2-1. 애플리케이션 의존성 및 환경 설정
▶ build.gradle
implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1")
implementation 'io.awspring.cloud:spring-cloud-aws-starter-sns'
▶ application.yml
cloud:
aws:
region: ap-northeast-2
profile: default
sns:
topic-name: my-topic
- 사용할 AWS 리전, 프로파일, SNS Topic을 지정하였다.
2-2. SNS 관련 클래스 작성
@Configuration
public class SNSConfig {
@Value("${cloud.aws.region}")
private String region;
@Value("${cloud.aws.profile}")
private String profile;
@Bean
public SnsClient snsClient() {
return SnsClient.builder()
.region(Region.of(region))
.credentialsProvider(ProfileCredentialsProvider.create(profile))
.build();
}
@Bean
public SnsTemplate snsTemplate(SnsClient snsClient) {
return new SnsTemplate(snsClient);
}
}
- Spring 애플리케이션에서 AWS SNS와 통신하기 위한 클라이언트를 구성하였다.
- 환경 변수에서 AWS Region과 Profile 값을 주입받아 사용하였으며, 지정한 프로파일을 통해 인증된 SNS 클라이언트를 생성하였다.
- SnsClient는 AWS SDK v2의 클라이언트로, 이를 Bean으로 등록하여 다른 컴포넌트에서 주입받아 사용할 수 있도록 하였다.
- 또한, SNS 메시지를 보다 간편하게 발행할 수 있도록 SnsTemplate도 함께 Bean으로 등록하였다.
@Service
@RequiredArgsConstructor
public class SNSSendService {
private final SnsTemplate snsTemplate;
@Value("${cloud.aws.sns.topic-name}")
private String topicArn;
public void sendMessage(String name, String item) {
try {
String orderId = UUID.randomUUID().toString();
ObjectMapper objectMapper = new ObjectMapper();
Map<String, String> messageMap = new HashMap<>();
messageMap.put("orderId", orderId);
messageMap.put("name", name);
messageMap.put("item", item);
String message = objectMapper.writeValueAsString(messageMap);
System.out.printf("Send: 주문번호 %s - 상품: %s, 주문자: %s\n", orderId, item, name);
snsTemplate.sendNotification(topicArn, message, "새 주문 도착");
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON 직렬화 실패", e);
}
}
}
- SNS로 메시지를 전송하는 서비스 로직을 담당하는 클래스를 작성해주었다.
- 입력된 주문자 이름과 상품명을 기반으로 주문 번호를 생성하고, 이를 JSON 형태로 변환한 뒤 메시지 본문으로 사용하였다.
- sendNotification() 메서드를 통해 SNS 주제에 메시지를 발행하며, 이메일 구독자에게는 Subject에 해당하는 제목을 함께 지정해줄 수 있다.
- 이렇게 구성된 메시지는 SNS를 통해 여러 구독자(SQS, Email 등)에게 동시에 전파된다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/sns")
public class SNSSendController {
private final SNSSendService snsSendService;
@PostMapping("/send")
public ResponseEntity<String> sendOrder(@RequestParam String name,
@RequestParam String item) {
snsSendService.sendMessage(name, item);
return ResponseEntity.ok("전송 완료");
}
}
- POST 요청을 통해 주문 요청을 하는 API Endpoint를 작성해주었다.
3. SQS Consumer 애플리케이션 수정
▶ 이전 포스팅에서의 SQS Listener
@SqsListener("${cloud.aws.sqs.queue-name}")
public void listenOrderQueue(@Headers Map<String, Object> headers, String message) {
ObjectMapper mapper = new ObjectMapper();
try {
OrderMessage order = mapper.readValue(message, OrderMessage.class);
System.out.println(
String.format("Receive : %s 주문 정보가 도착했습니다. 배송현황 데이터베이스에 저장합니다. : (주문상품 : %s & 주문자 : %s)",
order.getOrderId(), order.getItem(), order.getName()));
// SqsListener는 Exception이 나지 않으면 자동으로 삭제 처리를 한다!
} catch (Exception e) {
throw new RuntimeException(e);
}
}
- 이전 포스팅에서 Message를 수신하는 Listener를 구현한 내용이다.
- SNS Publisher -> SNS -> SQS -> SQS Consumer의 구조를 사용하기 때문에 수신자는 기존 Application을 사용할 것이다.
- 그렇지만 SQS Producer가 보낸 메시지 포멧과 SNS Publisher가 보낸 메시지의 포멧이 다르다는 점을 유의해야 한다. 따라서 해당 포메팅에 맞춰서 파싱하는 작업을 수행해야 한다.
SQS와 SNS의 메시지 구조 파악
{"item":"item","orderId":"66d37ebb-a2b3-454f-8551-6f7c4cc00ad4","name":"test"}
- SQS에서 발행한 메시지의 원본 payload는 다음과 같은 형식으로 Listener의 "String message"에 들어온다.
{
"Type" : "Notification",
"MessageId" : "5cbcfcc1-fb2f-5b52-96ab-9c4d6949b69c",
"TopicArn" : "arn:aws:sns:ap-northeast-2:476114146023:my-topic",
"Subject" : "새 주문 도착",
"Message" : "{\"item\":\"item\",\"orderId\":\"201fe99c-6ba1-421e-81dd-10f63734eb48\",\"name\":\"test\"}",
"Timestamp" : "2025-04-30T21:03:34.764Z",
"SignatureVersion" : "1",
"Signature" : "WsDM4HmIrnR3P/P+9OdL7M4ooxjUfRRQ8aQgtxmUO8ZyNVvQKr4ipi6fewffjfDtsfJG2Bsl95lsq5AzeSEo9atdu9bp3MbqVHNCtxen7bGOYggI79b3G2LxJG2rB1ZA6dRwbF2aGSX/eNfcYO59iNZgZU/jSsUqzAgWCebRh2Aq998wAzF06lBsBIjBvs9DUqwzN8qyih7KOr4n9YfNXT//mniiVBp0XHxyC9Jkrq5Wv4+5dWqIbfdJvTXL7q6H78gJ820zsVjwOnRMDvUAynQJ1/T3Dl62ahAKQbQddU6ZnanVtY3xXwNzRtccKlFESbDgShGis6CistcjThG7VA==",
"SigningCertURL" : "https://sns.ap-northeast-2.amazonaws.com/SimpleNotificationService-9c6465fa7f48f5cacd23014631ec1136.pem",
"UnsubscribeURL" : "https://sns.ap-northeast-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ap-northeast-2:476114146023:my-topic:5acc649c-6f9c-49f3-bccb-2e38dc6bd323",
"MessageAttributes" : {
"id" : {"Type":"String","Value":"d0239464-d598-6817-cd7e-093c16b664a3"},
"timestamp" : {"Type":"Number","Value":"1746047014587"}
}
}
- 그렇지만 SNS에서 발행한 메시지의 원본 payload는 Wrapper 구조로 SNS 시스템이 자동으로 붙이는 메타데이터와 함께 발행한 메시지를 "Message" 필드 안에 문자열로 넣어 전달하게 된다.
▶ SQS Listener JSON 파싱 수정
@SqsListener("${cloud.aws.sqs.queue-name}")
public void listenOrderQueue(@Headers Map<String, Object> headers, String message) {
ObjectMapper mapper = new ObjectMapper();
try {
JsonNode root = mapper.readTree(message);
String innerMessage = root.path("Message").asText(); // 실제 JSON 문자열
OrderMessage order = mapper.readValue(innerMessage, OrderMessage.class);
System.out.println(
String.format("Receive : %s 주문 정보가 도착했습니다. 배송현황 데이터베이스에 저장합니다. : (주문상품 : %s & 주문자 : %s)",
order.getOrderId(), order.getItem(), order.getName()));
// SqsListener는 Exception이 나지 않으면 자동으로 삭제 처리를 한다!
} catch (Exception e) {
throw new RuntimeException(e);
}
}
- SNS Publisher가 보낸 메시지의 포멧에 맞추어 파싱하는 작업을 수정해주었다.
4. 결과 확인
- 우리가 목표했던 것처럼 주문 시스템에서의 Message Publish 이후 두 개의 채널 (운영자의 이메일 & 배송 시스템)에 메시지가 잘 전달된 것을 확인할 수 있다.
5. SNS FIFO 사용 시 유의할 점
마지막으로 SQS와 마찬가지로 SNS는 표준(Standard) 주제뿐 아니라 FIFO 주제도 지원하고 있다.
SNS FIFO를 사용하면 메시지의 전송 순서 보장과 중복 제거가 가능해져, 주문 처리, 결제 순서, 재고 차감 등 처리 순서가 중요한 이벤트 흐름을 구현할 때 특히 유용하다.
단, SNS FIFO 주제는 SQS FIFO 큐와만 연동할 수 있으며, Standard SQS나 이메일, Lambda, SMS와 같은 다른 프로토콜과는 호환되지 않는 제약이 있다.
따라서 아키텍처 설계 시 메시지 순서 보장과 멀티 수신자 구독의 필요성 사이에서 트레이드오프를 고려해야 하며, 상황에 따라 SNS Standard와 FIFO를 적절히 병행 사용하는 전략이 유효할 수 있다.
References
https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/javav2/example_code
aws-doc-sdk-examples/javav2/example_code at main · awsdocs/aws-doc-sdk-examples
Welcome to the AWS Code Examples Repository. This repo contains code examples used in the AWS documentation, AWS SDK Developer Guides, and more. For more information, see the Readme.md file below....
github.com
https://docs.aws.amazon.com/
docs.aws.amazon.com
'Backend > AWS' 카테고리의 다른 글
[AWS] Route53 : 라우팅 정책 설정 (+ Failover 웹 호스팅 설정하기) (2) | 2025.05.29 |
---|---|
[AWS] Route53 : DNS 서비스의 이해 (+ 도메인 호스팅 설정하기) (1) | 2025.05.29 |
[AWS + Spring] SQS 활용 : 메시지 큐 기반 비동기 통신 구현 (Standard & FIFO) (1) | 2025.04.30 |
[AWS] 메시지 서비스의 이해 : SQS & SNS & Kinesis (1) | 2025.04.28 |
[AWS] CloudTrail : AWS 리소스 활동 기록 및 감사 로그 추적 (0) | 2025.04.06 |