← Docs hub

AWS IoT Domain Framework 상세 설계

구조 원칙: AWS IoT Core 연결·인증서·물리 Topic 입출력은 IotAgent 하나가 소유한다. Client Service App은 SkixIotCoreManager를 통해 허용된 domain과 channel만 선택해 subscribe, publish, request를 사용한다. 실제 정책과 상태는 Binder 뒤의 SkixIotCoreService가 소유한다.

이 설계는 기존 MQTT 경로를 교체하는 안이 아니다. 현재 제품 메시지와 sendModuleCommand(Bundle)는 그대로 유지하고, 여러 Client가 AWS IoT Core 기능을 안전하게 공유해야 하는 경우에만 Domain API를 선택적으로 사용한다. 기존 MQTT Task Contract는 Task·Workflow의 응용 메시지 형식을, 이 문서는 Client별 양방향 통신과 그 생명주기를 다룬다.

1. 목표와 책임 경계

제공할 기능

소유권

계층 소유하는 것 소유하지 않는 것
Client Service App 필요한 domain 선택, payload 생성, callback 처리 인증서, endpoint, raw Topic, 재연결
SkixIotCoreManager 호출 API, Service bind·재bind, 앱 친화형 callback 정책, 구독 원장, MQTT 연결
SkixIotCoreService caller 검증, domain 권한, logical handle, 전달 상태와 격리 인증서, 실제 MQTT socket
IotAgent 자격 증명, 단일 MQTT 연결, 물리 subscribe/publish, Shadow·Jobs owner Client별 권한과 callback queue
Domain Adapter 기존 제품 메시지 해석과 domain payload 변환 공통 연결·재시도 정책

Client가 보는 것은 안정된 domain API이고, Topic·인증서·AWS SDK는 Framework 내부 구현이다.

2. 현재 코드에서 확인된 사실

현재 기준은 a1-packages-mr6 작업 폴더의 quber/mr7 브랜치, commit 7695c9a다.

근거 현재 확인된 동작 리팩터링 필요점
apps/IotAgent/.../IIotControl.aidl registerCallback, sendModuleCommand(Bundle) 중심 domain·channel·handle이 공개 계약에 없음
apps/IotAgent/.../IIotControlCallback.aidl 범용 Bundle callback Client별 구독과 correlation 분리가 어려움
AWSIoTMQTTClient.java 하나의 AWSIotMqttManager, 자동 재연결·재구독 단일 연결 owner로 재사용 가능
IotAgent.connectToIoT() 제품 Topic을 코드에서 직접 구독 물리 구독 목록과 domain 선언을 분리해야 함
SubscribeHelper.java Topic 해석과 제품 dispatch가 결합 Broker 수신과 Client별 fan-out을 분리해야 함
IotAgent OTA 경로 AWS IoT Jobs 흐름 보유 Jobs owner를 새 Framework가 중복 소유하면 안 됨

이 표는 현재 구현이다. 이후의 Manager, Service, Registry와 Descriptor는 목표 구조이며 구현 완료 API가 아니다.

3. 목표 설계

Client Service App과 Domain별 양방향 AWS IoT Pub/Sub 구조

그림 1. Domain별 양방향 통신 구조 — Client는 Manager API를 사용하고, SkixIotCoreService가 Client별 관계와 정책을 관리한다. IotAgent는 기존과 같이 단일 물리 연결을 유지한다. 원본 SVG 열기

Android 계층

Client Service App
  -> SkixIotCoreManager              // SDK/AAR의 앱 호출 창구
  -> Binder / AIDL
  -> SkixIotCoreService              // Framework의 상태·정책 owner
       -> DomainCapabilityRegistry   // 공개 가능한 domain 목록
       -> InternalDescriptorRegistry // Topic·schema·caller 정책
       -> ClientSubscriptionRegistry // Client별 logical handle
       -> SubscriptionMultiplexer    // 공유 physical subscription
       -> DeliveryTracker            // publish와 request 결과
       -> LegacyDomainDispatcher     // 기존 제품 처리와 신규 fan-out
  -> IotAgentAdapter
  -> IotAgent                        // 인증서와 단일 AWS 연결 owner
  -> AWS IoT Core

핵심 구성요소

구성요소 책임
SkixIotCoreManager domain 조회, subscribe·publish·request, 연결·전달 상태 관찰
SkixIotCoreService Binder caller 확인, 정책 적용, 원장·queue·복구 생명주기 소유
DomainCapabilityRegistry Client가 사용할 수 있는 domain·channel·방향·schema version 제공
InternalDescriptorRegistry exact Topic template, caller group, quota와 Root Policy 관리
ClientSubscriptionRegistry UID·package·Binder token별 handle, callback, quota와 상태 관리
SubscriptionMultiplexer 여러 logical 구독을 하나의 physical 구독으로 병합
PublishPolicyGateway 방향·schema·크기·rate·offline 정책 검증
DeliveryTracker 접수·queue·전송·PUBACK·응답·실패를 publishId로 추적
LegacyDomainDispatcher 기존 메시지와 신규 Client 전달을 한 번만 분배
IotAgentAdapter Framework 내부 계약을 기존 IotAgent 호출·callback으로 변환

CleaningIotManager, MobilityIotManager 같은 전용 Manager는 선택적인 편의 API다. 공통 SkixIotCoreManager를 우회하지 않으며 별도 MQTT 연결이나 별도 정책을 만들지 않는다.

4. 공개 계약과 내부 계약을 분리한다

Client에 공개하는 DomainCapability

{
  "domainId": "cleaning",
  "displayName": "청정",
  "apiVersion": 2,
  "channels": [
    {"name": "command", "direction": "SUBSCRIBE", "schemaId": "cleaning.command", "maxSchemaVersion": 2},
    {"name": "state", "direction": "PUBLISH", "schemaId": "cleaning.state", "maxSchemaVersion": 3}
  ],
  "features": ["REQUEST_RESPONSE"]
}

Client는 자신에게 허용된 capability만 조회한다. raw Topic, wildcard, caller group, quota 계산식, 인증 정보는 공개하지 않는다.

Framework 내부 DomainDescriptor

DomainDescriptor는 Topic과 정책을 가진 내부 서명 계약이다.

{
  "domainId": "cleaning",
  "descriptorVersion": 7,
  "subscribeChannels": [{
    "channel": "command",
    "topicTemplate": "device/{thingName}/cleaning/command",
    "schema": "cleaning.command.v2",
    "qos": 1,
    "allowedCallerGroups": ["SYSTEM_CLEANING", "SYSTEM_DEVICE_AGENT"]
  }],
  "publishChannels": [{
    "channel": "state",
    "topicTemplate": "device/{thingName}/cleaning/state",
    "schema": "cleaning.state.v3",
    "qos": 1,
    "maxPayloadBytes": 16384,
    "ratePerMinute": 120,
    "offlinePolicy": "LATEST_ONLY",
    "allowedCallerGroups": ["SYSTEM_CLEANING"]
  }],
  "requestResponse": [{"requestChannel": "query", "responseChannel": "query-response"}],
  "shadowNames": ["cleaning"]
}

Framework가 template을 exact Topic으로 컴파일한다. Client가 #, +, $aws/ 또는 임의 Topic을 전달하는 API는 제공하지 않는다.

최종 허용 판단

Root Policy ∩ signed DomainDescriptor ∩ caller grant ∩ operation request를 적용한다. 쉽게 말하면 시스템 상한, domain 선언, 호출 앱 권한, 이번 요청 조건을 모두 통과해야 한다. Client 요청만으로 권한 범위가 넓어지지 않는다.

5. Framework API 계약

Java Manager API

public interface SkixIotCoreManager {
    List<DomainCapability> listAvailableDomains();

    SubscriptionHandle subscribe(
        DomainSubscriptionRequest request,
        DomainMessageCallback callback,
        SubscriptionStatusCallback statusCallback);
    void unsubscribe(String subscriptionId);

    PublishReceipt publish(
        DomainPublishRequest request,
        PublishStatusCallback statusCallback);
    PublishStatus getPublishStatus(String publishId);

    RequestHandle request(
        DomainRequest request,
        DomainResponseCallback callback);

    ConnectionState getConnectionState();
    ObserverHandle observeConnectionState(ConnectionStateCallback callback);
}

PublishReceipt는 최초 접수 결과의 snapshot이다. Binder로 반환된 객체가 이후 저절로 바뀌지 않으므로, 최종 상태는 PublishStatusCallback 또는 getPublishStatus()로 확인한다.

Binder 계약

interface ISkixIotCoreService {
    List<DomainCapability> listAvailableDomains();
    SubscriptionHandle subscribe(
        in DomainSubscriptionRequest request,
        in IDomainMessageCallback messageCallback,
        in ISubscriptionStatusCallback statusCallback);
    void unsubscribe(String subscriptionId);
    PublishReceipt publish(
        in DomainPublishRequest request,
        in IPublishStatusCallback statusCallback);
    PublishStatus getPublishStatus(String publishId);
    RequestHandle request(in DomainRequest request, in IDomainResponseCallback callback);
    ConnectionState getConnectionState();
    IObserverHandle observeConnectionState(in IConnectionStateCallback callback);
}

Service는 Binder의 실제 UID, package와 signature를 caller identity로 사용한다. payload의 package name이나 role은 권한 근거로 사용하지 않는다.

메시지 봉투

{
  "messageId": "msg-01J...",
  "domainId": "cleaning",
  "channel": "command",
  "schemaId": "cleaning.command",
  "schemaVersion": 2,
  "correlationId": "req-20260901-001",
  "receivedAtMs": 1788220800000,
  "retained": false,
  "duplicate": false,
  "payload": {}
}

DomainMessage의 schemaId와 version으로 codec을 선택한다. QoS 1 중복, retained message, 재연결 후 지연 수신을 구분할 수 있도록 messageId, duplicate, retained, 수신 시각을 보존한다.

6. 양방향 데이터 흐름

AWS → Client: subscribe

  1. Client가 domainId, channel, schema version과 callback을 전달한다.
  2. Service가 caller 권한과 내부 descriptor를 검증한다.
  3. ClientSubscriptionRegistry에 Client별 Logical Subscription을 만든다.
  4. 같은 exact Topic의 physical 구독이 있으면 reference만 증가시킨다.
  5. 없으면 IotAgent의 기존 연결로 한 번 subscribe한다.
  6. 수신 메시지는 LegacyDomainDispatcher에서 기존 제품 handler와 허용된 Client callback으로 각각 한 번만 전달한다.
Client A: cleaning.command -> Handle A1
Client B: cleaning.command -> Handle B1

Physical Subscription(cleaning.command, effective QoS 1)
  -> existing product handler: 1회
  -> Client A queue: 1회
  -> Client B queue: 1회

Physical Subscription의 key는 exact Topic이다. 여러 Client가 서로 다른 QoS를 요구하면 descriptor 상한 안에서 가장 높은 effective QoS로 한 번 구독하고, 각 logical handle에는 실제 적용 QoS를 기록한다.

Client → AWS: publish

  1. Client 요청을 PublishPolicyGateway가 권한·schema·크기·rate·TTL 기준으로 검증한다.
  2. Service가 publishId를 발급하고 LOCAL_ACCEPTED 또는 즉시 실패를 반환한다.
  3. 연결 상태와 channel 정책에 따라 바로 전송하거나 offline queue에 저장한다.
  4. SENT, PUBACKED, RESPONDED, FAILED, EXPIRED를 callback과 조회 API로 전달한다.

Request/Response

request()는 response channel을 먼저 subscribe하고 correlationId를 등록한 뒤 request channel에 publish한다. 응답·timeout·Client 종료 중 하나가 발생하면 임시 handle과 correlation record를 정리한다. 빠른 응답 유실과 영구 구독 누수를 방지하기 위한 순서다.

7. 신규 Domain 등록과 동적 도메인 추가

신규 Domain 등록과 원격 활성화 흐름

그림 2. 신규 Domain 등록 — 기본 경로는 제품 이미지에 포함된 로컬 built-in descriptor다. 원격 서명 catalog는 설치된 기능의 활성화·축소·회수만 수행한다. 원본 SVG 열기

가장 단순한 추가 경로

추가하려는 것 필요한 작업 Framework Core 변경
기존 JSON/CBOR codec을 쓰는 새 channel descriptor·schema·caller grant·계약 테스트 추가 없음
새 domain이지만 공통 봉투와 codec 사용 로컬 built-in descriptor와 domain adapter 등록 없음
typed Manager가 필요한 핵심 domain descriptor에서 wrapper 생성 또는 얇은 AAR facade 추가 없음
새 payload codec codec plugin과 호환성 테스트 추가 Registry 확장만
새 하드웨어 동작·native executor domain 구현과 펌웨어/HAL 배포 별도 제품 개발 필요

로컬 built-in descriptor가 source of truth다. 빌드 시 schema, Topic 충돌, 권한과 version을 검증하고 제품 이미지에 함께 배포한다. 새 domain을 추가하기 위해 SkixIotCoreService의 분기문을 수정하지 않는다.

원격 catalog의 역할

skix-domain-catalog named Device Shadow 또는 서명된 catalog 패키지는 다음만 수행한다.

원격 catalog는 설치되지 않은 실행 코드를 만들 수 없다. 새 codec, adapter, native 기능을 임의로 다운로드해 실행하지도 않는다. 따라서 단순 domain 추가 경로와 원격 운영 경로를 분리한다.

8. 전체 생명주기

Client별 Domain 통신 생명주기

그림 3. 연결·구독·발행 생명주기 — Broker 재연결은 handle을 유지하지만 Framework Service 재시작은 기존 Binder handle을 무효화하므로 Client 재등록이 필요하다. 원본 SVG 열기

세 생명주기를 분리하면 연결 장애는 전체 통신 복구로, Client 종료는 해당 구독 정리로, 발행 실패는 해당 메시지 재처리로 제한할 수 있어 정상 Client와 메시지에 장애가 번지는 것을 막는다.

Broker 연결 생명주기

DISCONNECTED -> CONNECTING -> CONNECTED -> RESTORING -> READY
      ^               |              |
      +---- BACKOFF <-+--------------+

복구 순서는 IotAgent control Topic, 제품 필수 Topic, active Client Physical Subscription이다. MQTT persistent session의 sessionPresent가 유효하면 불필요한 재구독을 피하고, 세션이 없으면 현재 logical 원장에서 물리 구독을 복원한다.

Client별 Domain Subscription 생명주기

REQUESTED -> AUTHORIZED -> SUBSCRIBING -> ACTIVE
                                      -> SUSPENDED -> RESTORING -> ACTIVE
ACTIVE -> CLOSED
ACTIVE -> REVOKED -> CLOSED

Publish 추적 생명주기

RECEIVED -> LOCAL_ACCEPTED -> QUEUED_OFFLINE -> SENT -> PUBACKED
    |              |               |            |
    +----------> FAILED <-----------+------------+
                   ^
                 EXPIRED

PUBACKED는 Broker 수신 증거이지 cloud 처리나 실제 기기 동작 완료가 아니다. 응용 응답이 필요한 channel만 RESPONDED 상태를 추가로 사용한다.

Client 프로세스 종료

Binder death 시 죽은 Client의 callbacks, handles, requests와 offline quota만 회수한다. 다른 Client의 logical handle과 제품 필수 구독은 유지한다.

IotAgent 종료·재시작

SkixIotCoreService가 살아 있으면 logical handle을 보존한다. IotAgent 재bind 뒤 descriptor와 caller grant를 다시 검증하고 physical subscription을 복원한다. Client에는 SUSPENDED와 ACTIVE 전환을 알린다.

Framework Service 종료·재시작

기존 Binder handle과 callback은 유효하지 않다. Client의 SkixIotCoreManager는 Service 재bind 후 자신이 보관한 subscription intent를 다시 제출하고 새 handle을 받는다. Framework가 durable subscription을 지원하더라도 이전 Binder 객체를 되살리지 않는다. 앱 재시작도 같은 규칙을 따른다.

부팅·패키지 교체

부팅 후 IotAgent가 CONNECTED를 보고하고 built-in descriptor 검증이 끝나야 READY가 된다. Client package 교체 또는 signature 변경 시 기존 grant와 handle을 폐기하고 재승인한다.

카탈로그 회수

channel 제거 또는 권한 축소 시 신규 operation을 즉시 차단한다. 영향 handle에는 revoke 이유를 전달하고, 보내지 않은 offline publish는 폐기 사유를 기록한다.

9. 전달·순서·오프라인 정책

상태 의미
LOCAL_ACCEPTED 로컬 권한·schema 검증 통과
QUEUED_OFFLINE channel 정책에 따라 암호화 queue 저장
SENT MQTT client에 전달
PUBACKED QoS 1 Broker PUBACK 수신
RESPONDED correlation된 응용 응답 수신
FAILED 정책·schema·queue·transport 실패
EXPIRED TTL 또는 request timeout 만료

10. AWS IoT Core 기능 공개 범위

기능 Client 공개 방식 소유권
MQTT Pub/Sub 공통 Domain API Service 정책, IotAgent 물리 I/O
Request/Response correlation을 포함한 helper API Service
Device Shadow domain별 typed get/update/observe IotAgent 단일 owner
AWS IoT Jobs 상태 조회·승인 등 제한된 typed API IotAgent·UpdateAgent 단일 owner
연결 상태 getConnectionState, observeConnectionState Service가 IotAgent 상태를 정규화
Credential·endpoint·AWS policy 공개하지 않음 IotAgent·제품 보안 영역

named Device Shadow는 연결 여부와 무관하게 상태를 공유하는 AWS 저장 모델이다. 일반 Pub/Sub로 reserved Topic을 열지 않고, desired/reported 책임과 version 검증이 있는 typed API만 제공한다. AWS IoT Jobs도 firmware update, reboot 같은 원격 작업의 기존 owner를 유지한다.

11. 기존 경로와 동시성 호환

선택적 도입

중복 구독 금지

같은 Topic을 기존 connectToIoT()와 신규 Multiplexer가 각각 subscribe하면 callback 중복, QoS 충돌과 unsubscribe 경쟁이 발생한다. 1단계에서 기존 필수 Topic을 built-in descriptor로 표현하되, LegacyDomainDispatcher가 기존 handler를 그대로 호출하도록 한다. 물리 subscribe는 IotAgent 안에서 한 경로만 소유한다.

기존 메시지 처리

AWS message
  -> IotAgent physical callback 1회
  -> LegacyDomainDispatcher
       -> 기존 제품 handler 1회
       -> 허용된 Client logical queues 각각 1회

기존 직접 호출과 관리형 Task가 같은 기기 기능을 동시에 요청하는 문제는 MQTT Framework가 아니라 TaskManager·Policy·Resource 계층이 조정한다. Domain API는 통신 권한과 전달 수명을 관리하며, 기기 동작 충돌 정책을 중복 구현하지 않는다.

12. 오류와 관측

오류 의미
DOMAIN_NOT_FOUND active capability에 domain 없음
CHANNEL_NOT_FOUND 요청 방향에 맞는 channel 없음
PERMISSION_DENIED 시스템·domain·caller·operation 조건 중 하나가 거부
SCHEMA_MISMATCH schema version 또는 payload 형식 위반
QUOTA_EXCEEDED 구독 수·rate·queue 상한 초과
BROKER_UNAVAILABLE 연결 불가이며 offline queue 미허용
SUBSCRIPTION_REVOKED descriptor 또는 caller 권한 회수
CALLBACK_OVERFLOW 해당 Client callback queue 포화
SERVICE_RESTARTED 이전 Binder handle 무효, Client 재등록 필요
RESPONSE_TIMEOUT 응용 응답 제한 시간 만료

관측 항목은 연결 상태, logical/physical subscription 수, multiplex 비율, Client별 queue와 drop, publish 상태별 건수, reconnect·restore 시간, revoke와 재등록 결과다. payload, 인증서와 개인 정보는 로그에 기록하지 않는다.

13. 단계적 전환

단계 변경 완료 기준
1. 물리 입출력 단일화 기존 Topic을 로컬 built-in descriptor로 표현하고 기존 handler 연결 메시지·QoS·호출 횟수 회귀 동일
2. Service 경계 SkixIotCoreService, caller 검증과 연결 상태 API 추가 bind·재bind·권한 테스트
3. 구독 API 단일 Client에 logical handle 기반 subscribe 제공 Binder death·Broker 복구 검증
4. 발행 API publishId, callback/query와 channel별 offline 정책 추가 접수·PUBACK·실패 구분
5. 다중 Client 같은 Topic fan-out과 독립 queue 적용 한 Client 종료·포화가 타 Client에 무영향
6. Domain 개발 키트 descriptor validator, schema test, wrapper 생성 제공 Core 수정 없이 새 domain 추가
7. 원격 운영 서명 catalog의 enable·축소·revoke·rollback 제공 위변조·만료·권한 확대 거부
8. 기존 경로 축소 전환 완료 domain만 legacy adapter로 이동 중복 subscribe 0건, 회귀 통과

14. 검증 기준

15. 확정안과 구현 전 선택

확정안

구현 전 선택

16. AWS 공식 참고 자료

Keyboard shortcuts

⌘K / Ctrl+KOpen command palette
/Focus search
g hGo to home
g pGo to projects
g sGo to sessions
j / kNext / prev row (tables)
?Show this help
EscClose dialogs

Structured queries

Mix key:value filters with free text in the palette:

type:sessionOnly session pages
project:llm-wikiFilter by project name (substring)
model:claudeFilter by model name (substring)
date:>2026-03-01Sessions after a date
date:<2026-04-01Sessions before a date
tags:rustPages mentioning a tag/topic
sort:dateSort results by date (newest first)

Example: type:session project:llm-wiki date:>2026-04 sort:date