IoT M:N 통신 아키텍처 설계기 — AWS IoT Core + Amplify + Cognito
M명의 사용자와 N대의 디바이스가 권한에 따라 통제된 형태로 통신하는 M:N MQTT 아키텍처를 AWS IoT Core, Amplify, Cognito 기반으로 설계하고 구현한 과정을 공유합니다.
OpenIoT

왜 M:N 통신인가
IoT 서비스에서 가장 흔한 통신 구조는 1:1입니다. 하나의 앱이 하나의 디바이스를 제어하는 단순한 모델이죠. 하지만 현실의 IoT 서비스는 훨씬 복잡합니다.
- 한 사용자가 여러 디바이스를 소유하고 제어해야 하고,
- 하나의 디바이스를 여러 사용자가 공유해서 모니터링해야 하며,
- 사용자마다 접근 가능한 디바이스 범위가 달라야 합니다.
이것이 바로 M:N 통신 문제입니다. M명의 사용자와 N대의 디바이스가 서로 자유롭게, 그러나 권한에 따라 통제된 형태로 통신해야 합니다.
이 글에서는 OpenIoT 프로젝트에서 AWS IoT Core 기반의 M:N MQTT 통신 아키텍처를 설계하고, 사용자 프로비저닝을 구현하기까지의 과정을 공유합니다.
서버리스 IoT 아키텍처의 한계들
수백만 대 규모의 IoT 디바이스를 서버리스(AWS Lambda) 기반으로 처리하려 할 때 마주치는 핵심 문제들과 그 대응 전략을 먼저 살펴보겠습니다.
1. Lambda 오류 — 트래픽 집중 시 실행 실패
수천 대의 디바이스가 동시에 메시지를 보내면 IoT Rule Engine이 트리거하는 Lambda 함수에 동시 호출이 몰립니다. 결과는 실행 실패와 타임아웃입니다.
대응 전략으로 IoT Rule Engine의 Error Action을 설정했습니다. 에러 발생 시 임시 저장소에 데이터를 적재한 후 재처리하는 구조로, Dead Letter Queue(DLQ)를 활용하여 데이터 소실을 방지합니다.
2. Lambda 성능 튜닝
비최적화된 Lambda는 비용 증가와 오류를 동시에 유발합니다. Lambda Best Practice를 적용하여 Global Variable 전환을 수행했습니다.
- Warm-start 지연 시간 감소 — 핸들러 외부에서 클라이언트 초기화
- Backend 서비스(DB, FTP 등) 부하 절감 — 연결 재사용
- 파일 핸들 수량 감소 — 리소스 효율화
Concurrency Cap을 100으로 설정한 Lambda 함수 기준으로, 초당 1,600회 호출 / Throttle 4건 수준의 성능을 확보할 수 있었습니다.
3. 계정 단위 Concurrency 한계
AWS Lambda는 계정당 동시 실행 제한이 있습니다. 단일 Lambda 함수가 모든 Concurrency를 소모하면 다른 함수까지 영향을 받습니다.
개별 함수에 Concurrency Cap을 적용하여, 단일 함수가 전체 Concurrency를 소모하는 것을 방지합니다. 대규모 서비스에서는 멀티 계정/리전 분산 배포 전략도 고려해야 합니다.
4. 이벤트 유실 방지 — 큐 기반 처리
실시간 이벤트가 과부하되면 데이터 누락이 발생합니다. SQS와 Kinesis를 도입하여 이벤트를 버퍼링하고 순차 처리하는 구조로 전환했습니다.
| 서비스 | 특징 | 적합한 상황 |
|---|---|---|
| SQS (Standard) | 스케일 관리 불필요, One-to-one 처리 (최대 10개 배치) | 단일 애플리케이션 |
| Kinesis | 샤드별 순서 보장, Many-to-one (최대 250개 배치), 70ms 수준 | 다수 애플리케이션이 동일 스트림 구독 |
중복 처리도 고려해야 합니다. QoS 0에서도 중복 메시지가 발생할 수 있기 때문입니다.
- 시계열 데이터: 시간 값을 Constraint로 활용한 Unique/Primary 기반 중복 처리
- Thing Shadow: timestamp, version, token 정보를 활용한 Upsert 처리
- 동시성 제어: ElastiCache를 잠금 관리자로 활용
근본적 해결책: 제어 권한을 앱에 위임 (Delegation)
위의 대응 전략들을 모두 적용해도, 클라우드가 모든 디바이스를 직접 제어하는 구조는 근본적으로 비용과 성능 병목을 피하기 어렵습니다. 발상의 전환이 필요했습니다.
사용자 앱(모바일/웹)이 제어의 중심 역할을 수행하고, 클라우드는 정책 관리, 인증, 텔레메트리 저장, 커맨드 히스토리 기록 등 보조 역할로 전환합니다.
이 Delegation 모델에서 디바이스는 MQTT, HTTP, WebSocket을 통해 사용자 앱과 직접 통신합니다. 클라우드의 Rule Engine은 텔레메트리 저장과 커맨드 이력 관리만 담당합니다. Lambda 호출 빈도를 크게 줄이면서도 실시간 제어의 반응성을 유지할 수 있었습니다.
X.509에서 WebSocket + IAM으로의 전환
Delegation 모델을 적용하려면 사용자 앱이 AWS IoT Core의 MQTT 브로커에 직접 접속해야 합니다. 여기서 현실적인 벽에 부딪혔습니다.
브라우저 환경의 제약
브라우저는 raw TCP 소켓을 사용할 수 없습니다. MQTT의 기본 포트(1883/8883)에 직접 접근이 불가능하고, WebSocket 브리지를 통한 간접 접근만 가능합니다.
모바일 앱(React Native)의 제약
네이티브 앱은 이론적으로 MQTT over TLS 연결이 가능하지만, 실질적으로 다음 문제들이 있었습니다.
- 인증서 저장소 보안 이슈 — X.509 인증서의 안전한 저장/관리 부담
- 클라이언트 인증서 갱신 문제 — 재발급/로테이션 처리 복잡
- 운영 편의성 문제 — 사용자마다 인증서 발급/설치/관리 필요
무엇보다 React Native 기반이기 때문에 웹 기반 라이브러리가 위주였고, 안정적이고 대중적인 AWS IoT MQTT 통신 라이브러리가 부재했습니다.
해결: AWS Amplify PubSub + IAM 인증
X.509 인증서 대신 WebSocket + IAM 기반의 MQTT 통신으로 전환했습니다. AWS Amplify Gen 2의 PubSub 기능을 활용하면, Cognito 인증을 거친 사용자가 별도의 인증서 없이도 MQTT 브로커에 접속할 수 있습니다.
이 방식의 핵심은 인증의 주체가 인증서(디바이스 레벨)에서 사용자 계정(서비스 레벨)으로 바뀐다는 점입니다. 사용자는 로그인만 하면 MQTT 통신 권한을 자동으로 획득합니다.
진행한 작업:
- Amplify Gen 2 기반 프로젝트 생성
- PubSub 기능 활성화 — MQTT 통신을 위한 구성
- IAM 권한 구성 추가 — 인증된 사용자만 PubSub 사용 가능하도록 설정
사용자 프로비저닝 설계
사용자가 로그인 후 MQTT 통신을 하려면, 해당 사용자에 대한 IoT 리소스 프로비저닝이 선행되어야 합니다.
- AWS IoT Policy 생성 — 사용자별 토픽 접근 권한 정의
- Policy와 Cognito Identity 연결 — 인증된 사용자에게 IoT 권한 부여
- User Group 배정 — PubSub 접근 권한이 있는 그룹에 포함
- DynamoDB에 사용자 정보 저장 — 프로비저닝 상태 추적
기존 계획: 회원가입 시 자동 프로비저닝
처음에는 심플한 구조를 계획했습니다. 회원가입 시 Cognito Lambda Trigger가 자동으로 호출되어 모든 프로비저닝 작업을 한 번에 수행하는 것이었습니다.
문제 발생: identityId의 부재
하지만 AWS Cognito의 Lambda Trigger는 identityId를 반환하지 않는다는 사실을 발견했습니다. IoT Policy를 Cognito Identity에 연결하려면 identityId가 필수인데, 회원가입 시점에는 이 값을 알 수 없었습니다.
변경된 계획: 지연 프로비저닝 (2단계 분리)
회원가입과 프로비저닝을 2단계로 분리하는 방식으로 전환했습니다.
이 방식의 장점은 identityId를 클라이언트에서 직접 가져와 전달할 수 있다는 것입니다. Amplify의 fetchAuthSession()을 호출하면 identityId를 받을 수 있고, 이를 프로비저닝 API에 전달합니다.
프로비저닝 구현 — 코드 레벨
1단계: Cognito Lambda Trigger — 사용자 등록
Cognito의 PreSignUp 트리거에 Lambda를 연결하여, 회원가입 시 DynamoDB에 사용자 기본 정보를 저장합니다.
def handle_pre_signup(event, context):
user_attributes = event.get('request', {}).get('userAttributes', {})
email = user_attributes.get('email')
user_id = event.get('userName') or user_attributes.get('sub')
# 이미 존재하는 사용자인지 확인
if UserTable.user_exists(user_id):
return event
# 사용자 데이터 생성 — provision은 False로 초기화
user_data = {
'email': email,
'username': user_attributes.get('preferred_username', user_id),
'provision': False, # 프로비저닝 미완료 상태
'status': 'pending',
'created_at': datetime.utcnow().isoformat(),
}
UserTable.create_user(user_id, user_data)
return event
PostConfirmation 트리거에서는 이메일 인증이 완료되면 사용자 상태를 confirmed로 업데이트합니다.
2단계: IoT Policy 생성 — 사용자별 토픽 격리
사용자별로 고유한 IoT Policy를 생성합니다. 핵심은 토픽 접근 범위를 사용자 ID 기반으로 제한하는 것입니다.
def create_iot_policy(iot_client, user_id):
policy_name = f"user_policy_{user_id}_{str(uuid.uuid4())[:8]}"
policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "iot:Connect",
"Resource": "arn:aws:iot:ap-northeast-2:*:client/*"
},
{
"Effect": "Allow",
"Action": ["iot:*"],
"Resource": [
f"arn:aws:iot:ap-northeast-2:*:topic/{user_id}/*",
f"arn:aws:iot:ap-northeast-2:*:topicfilter/{user_id}/*"
]
},
{
"Effect": "Allow",
"Action": [
"iot:GetThingShadow",
"iot:UpdateThingShadow",
"iot:DeleteThingShadow"
],
"Resource": [f"arn:aws:iot:*:*:thing/{user_id}"]
}
]
}
return iot_client.create_policy(
policyName=policy_name,
policyDocument=json.dumps(policy_document)
)
이 정책은 사용자가 {user_id}/* 하위 토픽에만 접근할 수 있도록 제한합니다. 다른 사용자의 토픽에는 접근할 수 없으므로, M:N 통신에서도 사용자 간 격리가 보장됩니다.
Policy-Identity 연결 및 그룹 배정
생성된 Policy를 Cognito Identity에 연결하고, 사용자를 PubSub 접근 권한이 있는 그룹에 추가합니다.
# Policy를 Cognito Identity에 연결
iot_client.attach_policy(
policyName=policy_name,
target=identity_id # 클라이언트에서 전달받은 identityId
)
# 사용자를 IoT 권한 그룹에 추가
cognito_client.admin_add_user_to_group(
UserPoolId=user_pool_id,
Username=user_id,
GroupName='pubsubFullAccessGroup'
)
프로비저닝이 완료되면 DynamoDB의 provision 필드를 True로 업데이트하여, 이후 앱 진입 시 중복 프로비저닝이 발생하지 않도록 합니다.
향후 계획: 디바이스 그룹 기반 M:N 연결
프로비저닝의 다음 단계는 사용자와 디바이스를 그룹으로 연결하는 것입니다.
회원가입 시 — 사용자 Device Group 생성
디바이스 커미셔닝 시 — 그룹에 디바이스 추가
Device Group 공유 — M:N의 완성
앱(사용자)과 디바이스 모두 AWS IoT Thing으로 등록되며, Device Group이라는 논리적 단위로 묶여 토픽 기반의 격리된 M:N 통신이 가능해집니다.
아키텍처 결정 요약
| 문제 | 결정 | 이유 |
|---|---|---|
| Lambda 동시 호출 폭주 | Error Action + DLQ 설정 | 데이터 소실 방지 |
| Lambda 성능 병목 | Global Variable 전환 + Concurrency Cap | 비용/성능 최적화 |
| 이벤트 유실 | SQS/Kinesis 버퍼링 | 순차 처리 보장 |
| 클라우드 비용/성능 한계 | Delegation 모델 (앱에 제어 위임) | 근본적 부하 감소 |
| 브라우저/RN의 X.509 제약 | WebSocket + IAM 인증 (Amplify PubSub) | 플랫폼 무관 MQTT 접속 |
| Cognito Trigger에 identityId 부재 | 지연 프로비저닝 (2단계 분리) | 실제 구현 가능한 구조 |
| 사용자 간 토픽 격리 | user_id 기반 IoT Policy | M:N에서도 보안 보장 |
배운 점
설계와 현실의 괴리
"회원가입 시 자동 프로비저닝"이라는 깔끔한 설계가 identityId 하나 때문에 무너졌습니다. AWS 서비스들이 제공하는 값과 타이밍을 사전에 정확히 파악하는 것이 중요합니다. 공식 문서에 명시되지 않은 제약은 실제로 구현해봐야 발견되는 경우가 많습니다.
Delegation의 위력
클라우드가 모든 것을 처리하는 구조에서, 앱에 제어를 위임하는 구조로 바꾸는 것만으로도 Lambda 호출 횟수와 비용이 크게 줄었습니다. IoT 서비스 설계 시 "클라우드가 꼭 해야 하는 일인가?"를 먼저 자문해볼 필요가 있습니다.
토픽 기반 권한의 우아함
MQTT 토픽을 {user_id}/* 패턴으로 설계하고, IoT Policy로 접근 범위를 제한하는 방식은 M:N 통신에서도 사용자 간 격리를 자연스럽게 보장합니다. 별도의 인가 서버 없이 AWS IoT Core의 기본 메커니즘만으로 충분히 구현할 수 있었습니다.
마치며
M:N MQTT 통신은 단순한 pub/sub를 넘어, 인증/인가/프로비저닝/그룹 관리가 유기적으로 결합되어야 하는 복합적인 문제입니다. 이 글에서 다룬 아키텍처가 비슷한 IoT 서비스를 설계하는 분들에게 참고가 되길 바랍니다.
특히 "서버리스의 한계를 어떻게 우회할 것인가"와 "사용자 앱에 얼마만큼의 권한을 위임할 것인가"라는 두 가지 질문은, IoT 서비스의 규모가 커질수록 반드시 마주하게 되는 설계 과제입니다. Delegation 모델과 지연 프로비저닝이라는 해법이 하나의 선택지가 되길 바랍니다.


