MQTT Topic 설계 방법: 계층 구조·Wildcard·Naming Rule·ACL까지 이해하기
MQTT Topic 설계 방법: 계층 구조·Wildcard·Naming Rule·ACL까지 이해하기#
1장 MQTT Topic은 단순한 주소가 아니다#
MQTT를 처음 접하면 Topic을 단순한 Message 주소라고 생각하기 쉽습니다.
예:
parking/site01/slot05/state하지만 실제 운영에서는 Topic이 훨씬 더 많은 역할을 합니다.
Topic을 보면 운영자가 다음 정보를 파악할 수 있어야 합니다.
어느 Site인가?
어느 장비인가?
무슨 데이터인가?
상태인가 Event인가?
Command인가?
누가 Subscribe할 수 있는가?
누가 Publish할 수 있는가?따라서 Topic 설계는 단순한 문자열 Naming 작업이 아니라 MQTT 시스템의 정보 구조와 권한 구조를 설계하는 작업에 가깝습니다.
초기에는 Topic이 몇 개밖에 없어 아무렇게나 만들어도 문제가 없어 보입니다.
하지만 Device가:
10대
→ 100대
→ 1,000대
→ 10,000대로 늘어나면 Topic Naming Rule이 없는 시스템은 빠르게 관리하기 어려워집니다.
2장 좋은 Topic은 계층 구조부터 명확하다#
MQTT Topic은 /를 이용해 Level을 구분합니다.
예:
parking/site01/floor03/slot012/status이를 계층적으로 보면:
parking
└─ site01
└─ floor03
└─ slot012
└─ status이 구조는 사람이 읽기도 쉽고 Subscriber가 필요한 범위만 구독하기도 좋습니다.
예를 들어 Site 하나의 모든 상태를 받고 싶다면:
parking/site01/#특정 층의 주차면 상태만 받고 싶다면:
parking/site01/floor03/+/status처럼 Subscription Filter를 만들 수 있습니다.
좋은 Topic 구조는 Publish를 편하게 만드는 것보다 Subscribe와 운영을 편하게 만드는 구조라고 보는 것이 좋습니다.
3장 Topic을 설계할 때 어떤 순서로 나눌까#
주차 IoT 시스템이라면 다음과 같은 기준으로 계층을 만들 수 있습니다.
서비스
↓
Site
↓
위치
↓
장비 또는 Resource
↓
데이터 종류예:
parking/site01/floor03/slot012/status조금 더 Device 중심으로 만들면:
parking/site01/devices/sensor012/status또는 Device Type을 포함할 수도 있습니다.
parking/site01/sensors/sensor012/status어떤 방식이 무조건 옳은 것은 아닙니다.
중요한 것은 어떤 기준을 선택하든 전체 시스템에서 일관되게 사용하는 것입니다.
4장 주차장 ID·층 ID·장비 ID를 표준화한다#
Topic 구조보다 먼저 ID 체계를 정하는 것이 좋습니다.
예를 들어 다음과 같이 정의할 수 있습니다.
Site
SITE001
Floor
F03
Slot
S012
Gate
GATE01
Sensor
SENSOR012Topic:
parking/SITE001/F03/S012/status또는:
parking/SITE001/sensors/SENSOR012/status처럼 사용할 수 있습니다.
중요한 것은 사람이 임의로:
site1
Site01
SITE-1
parkingA
A주차장처럼 제각각 입력하지 못하도록 하는 것입니다.
ID는 가능하면:
고유해야 한다.
변하지 않아야 한다.
사람이 읽을 수 있어야 한다.
너무 길지 않아야 한다.
자동 생성할 수 있어야 한다.는 조건을 만족하도록 설계하는 것이 좋습니다.
장비 Serial Number를 그대로 Topic ID로 써야 할까#
가능은 하지만 신중해야 합니다.
제조사 Serial Number는:
장비 교체
제조사 변경
자산 이전때문에 업무상 Device Identity와 맞지 않을 수 있습니다.
예를 들어 물리 Device는 교체됐지만 업무적으로는 계속:
gate01이라는 동일한 위치의 Gate일 수 있습니다.
그래서:
업무 Resource ID
물리 Device ID를 분리하는 것도 좋은 방법입니다.
5장 상태·이벤트·명령을 같은 Topic에 섞지 않는다#
다음 Topic 하나에 모든 Message를 Publish한다고 하겠습니다.
parking/site01/gate01Payload:
{
"type": "status",
"value": "online"
}또 다른 Message:
{
"type": "alarm",
"value": "motor-error"
}또:
{
"type": "command",
"value": "..."
}기술적으로 가능하지만 운영과 권한 관리가 어려워집니다.
Topic 자체에서 의미를 분리하는 편이 좋습니다.
예:
parking/site01/gate01/status
parking/site01/gate01/events
parking/site01/gate01/alarms
parking/site01/gate01/commands더 명확하게 Presence와 Health를 나눌 수도 있습니다.
parking/site01/gate01/presence
parking/site01/gate01/health
parking/site01/gate01/alarm이렇게 하면 Subscriber도 필요한 데이터만 받을 수 있습니다.
6장 State와 Event의 Topic도 구분한다#
MQTT 시스템에서는 State와 Event를 구분하는 것이 중요합니다.
State#
현재 값이 중요합니다.
예:
parking/site01/slot012/statePayload:
{
"occupied": true
}과거에 몇 번 바뀌었는지보다 지금:
occupied = true인지가 중요합니다.
이런 Topic은 Retained Message와 잘 맞을 수 있습니다.
Event#
과거에 발생한 하나의 사건입니다.
예:
parking/site01/events/vehicle-entryPayload:
{
"eventId": "evt-1001",
"vehicleNumber": "12가3456",
"gateId": "gate01"
}Event는 History와 중복·순서 관리가 중요합니다.
따라서:
State
→ 현재 값
Event
→ 발생한 사건을 Topic 설계 단계부터 구분하는 것이 좋습니다.
7장 Command Topic은 특히 별도로 분리한다#
장비 제어 Message는 상태 Message보다 훨씬 민감합니다.
예를 들어:
parking/site01/gate01/command이라는 Topic을 사용할 수 있습니다.
하지만 중요한 것은 이름 자체보다 권한 분리입니다.
구조적으로:
Device
→ status Publish 허용
Device
→ command Subscribe 허용
Control Server
→ command Publish 허용
일반 Monitoring Client
→ command Publish 금지처럼 만들 수 있습니다.
즉 Topic 구조가 ACL과 자연스럽게 연결되어야 합니다.
상태와 명령이 같은 Topic이면 문제가 생긴다#
예:
parking/site01/gate01하나에서 상태와 Command를 모두 처리하면 ACL을 세밀하게 만들기 어려워집니다.
반면:
parking/site01/gate01/status
parking/site01/gate01/command으로 나누면 권한을 별도로 설정하기 쉽습니다.
8장 +와 # Wildcard를 이해한다#
MQTT Subscription에서는 대표적으로 두 종류의 Wildcard를 사용합니다.
+는 한 단계#
예:
parking/site01/+/status다음과 같은 Topic을 받을 수 있습니다.
parking/site01/gate01/status
parking/site01/gate02/status
parking/site01/gate03/status하지만:
parking/site01/floor03/gate01/status처럼 중간 Level이 하나 더 있다면 맞지 않습니다.
#는 이후 모든 단계#
예:
parking/site01/#는:
parking/site01/gate01/status
parking/site01/gate01/alarm
parking/site01/floor03/slot012/status등 Site01 아래의 여러 Topic을 구독할 수 있습니다.
Wildcard를 기준으로 Topic 구조를 설계할 수도 있다#
운영 Dashboard가 항상:
특정 Site 전체를 구독해야 한다면:
parking/{site}/...구조가 편합니다.
특정 Device Type만 구독해야 한다면:
parking/{site}/sensors/{device}/...처럼 Device Type을 중간에 배치하는 것이 유리할 수도 있습니다.
즉 Topic 계층은 향후 Subscription Pattern을 먼저 생각하고 설계하는 것이 중요합니다.
9장 지나치게 넓은 Wildcard는 신중하게 사용한다#
다음 Subscription:
#은 Broker에서 볼 수 있는 거의 모든 Topic을 구독하는 매우 넓은 Filter입니다.
운영 Monitoring이나 관리 Tool에서는 필요할 수도 있습니다.
하지만 일반 Application Client에서 무분별하게 사용하면:
불필요한 Message 수신
Network 사용량 증가
Client 처리량 증가
권한 범위 확대
개인정보 노출 가능성등이 생길 수 있습니다.
예를 들어 Site01 Dashboard라면:
parking/site01/#정도로 범위를 제한하는 것이 더 나을 수 있습니다.
더 좁게:
parking/site01/+/status처럼 설계할 수도 있습니다.
핵심은 필요한 범위만 Subscribe하는 것입니다.
10장 Topic에 너무 많은 정보를 넣는 것도 문제다#
다음과 같은 Topic을 생각해보겠습니다.
parking/companyA/seoul/site01/buildingA/floor03/
slot012/sensor/ultrasonic/vendorA/model33/device123/
firmware120/status정보는 풍부하지만 너무 복잡합니다.
Topic이 길어질수록:
Traffic 증가
운영 실수 증가
Naming 변경 비용 증가
ACL 복잡도 증가가 커집니다.
변경될 가능성이 높은 Metadata는 Payload나 Device Registry에 두는 것이 더 나을 수 있습니다.
예:
Topic:
parking/site01/sensors/sensor012/statusPayload:
{
"floor": "F03",
"slotId": "S012",
"model": "US-100",
"status": "normal"
}Topic에는 Routing과 Subscription에 실제 필요한 정보만 넣는 것이 좋습니다.
11장 Topic에 개인정보를 넣지 않는 것이 좋다#
다음 Topic:
parking/site01/vehicles/12가3456/entry처럼 차량번호를 Topic에 직접 넣는 것은 피하는 편이 좋습니다.
Topic 이름은:
Broker Log
Monitoring
Metrics
ACL
Debugging Tool등 여러 곳에 노출될 수 있기 때문입니다.
대신:
parking/site01/events/vehicle-entry로 만들고 Payload에 필요한 데이터를 넣을 수 있습니다.
{
"eventId": "evt-1001",
"vehicleId": "veh-8f291"
}개인정보가 반드시 필요하다면 별도의 보안·접근 통제 정책을 적용해야 합니다.
12장 Multi-Tenant 환경에서는 Tenant 경계를 먼저 잡는다#
하나의 MQTT Broker Cluster에서 여러 고객이나 사업장을 처리한다고 하겠습니다.
그렇다면:
tenant/companyA/parking/site01/...
tenant/companyB/parking/site01/...처럼 Tenant를 상위 Level에 둘 수 있습니다.
장점은 ACL을 다음처럼 구성하기 쉬워진다는 것입니다.
Company A
→ tenant/companyA/#
Company B
→ tenant/companyB/#다른 Tenant의 Message에 접근하지 못하도록 Namespace를 분리할 수 있습니다.
다만 Topic만으로 Tenant Isolation을 해결했다고 생각해서는 안 됩니다.
실제 Broker ACL과 인증 정책도 반드시 함께 적용해야 합니다.
13장 Topic Naming Rule을 문서로 만든다#
Topic 설계는 구두 규칙으로 관리하면 시간이 지나면서 무너집니다.
예를 들어 다음과 같은 Convention을 만들 수 있습니다.
Root
parking
Site
lowercase 영문·숫자
예: site01
Device Type
gates
sensors
cameras
Device ID
gate01
sensor012
Resource
status
health
alarm
command예:
parking/site01/gates/gate01/status
parking/site01/gates/gate01/health
parking/site01/sensors/sensor012/state추가 규칙:
공백 사용 금지
불필요한 특수문자 사용 금지
대소문자 규칙 통일
ID 자리수 규칙 통일
Topic Version 정책 정의등을 문서화할 수 있습니다.
14장 대소문자는 처음부터 통일한다#
MQTT Topic은 대소문자를 구분해서 다루는 것이 중요합니다.
다음은 서로 다른 Topic으로 취급됩니다.
parking/site01/status
Parking/site01/status
parking/SITE01/status따라서:
모두 소문자
ID만 대문자등 명확한 규칙을 미리 정하는 것이 좋습니다.
저라면 운영 편의성을 위해 가능하면:
parking/site01/gates/gate01/status처럼 소문자를 중심으로 통일하는 것을 추천합니다.
15장 Client ID와 Topic을 같은 것으로 생각하면 안 된다#
Device가:
clientId = gate01이라고 해서 반드시 Topic도:
gate01로 시작해야 하는 것은 아닙니다.
Client ID는 Broker Connection에서 Client를 식별하는 값이고 Topic은 Message Routing에 사용하는 문자열입니다.
예:
Client ID
site01-gate01-01
Topic
parking/site01/gates/gate01/status처럼 역할이 다릅니다.
특히 한 Client가 여러 Topic을 Publish하거나 Subscribe할 수 있으므로 두 개념을 분리해 설계해야 합니다.
16장 Topic Versioning도 생각해야 한다#
초기에는 다음 구조를 사용했다고 하겠습니다.
parking/site01/gate01/status그런데 시스템이 커지면서:
parking/site01/gates/gate01/status로 바꾸고 싶어졌습니다.
이미 다음 시스템이 기존 Topic을 사용하고 있을 수 있습니다.
Dashboard
Monitoring
Database Consumer
Mobile Service
Alert SystemTopic을 한 번에 바꾸면 모든 Consumer가 동시에 장애를 일으킬 수 있습니다.
따라서 Migration 기간을 둘 수 있습니다.
기존 Topic Publish
+
신규 Topic Publish
↓
Subscriber 점진적 변경
↓
구 Topic 제거또는 명시적으로:
parking/v1/...
parking/v2/...같은 Version Namespace를 도입할 수도 있습니다.
단 Version을 무조건 넣어야 한다는 뜻은 아닙니다.
향후 Breaking Change 가능성과 운영 방식에 따라 결정합니다.
17장 Topic과 Payload의 역할을 구분하자#
Topic:
parking/site01/slots/slot012/statePayload:
{
"occupied": true,
"timestamp": "2026-09-24T16:00:00+09:00"
}이 구조에서 Topic은:
어떤 Resource의 어떤 종류의 데이터인가를 표현합니다.
Payload는:
구체적인 값과 Metadata를 표현합니다.
모든 정보를 Topic에 몰아넣거나 반대로 모든 정보를 Payload에 넣으면 Subscription과 Routing이 어려워질 수 있습니다.
따라서:
Topic
→ Routing에 필요한 정보
Payload
→ 업무 데이터로 역할을 나누는 것이 좋습니다.
18장 주차 IoT Topic 구조 예시#
하나의 예시 구조를 만들면 다음과 같습니다.
parking
└─ site01
├─ gates
│ └─ gate01
│ ├─ presence
│ ├─ health
│ ├─ alarm
│ └─ command
│
├─ slots
│ └─ slot012
│ ├─ state
│ └─ health
│
└─ cameras
└─ cam01
├─ health
└─ events실제 Topic:
parking/site01/gates/gate01/presence
parking/site01/gates/gate01/health
parking/site01/gates/gate01/alarm
parking/site01/gates/gate01/command
parking/site01/slots/slot012/state
parking/site01/cameras/cam01/events이 정도만으로도 운영자가 Topic을 읽고 의미를 파악하기 쉬워집니다.
19장 ACL도 Topic 구조를 기준으로 만든다#
잘 설계된 Topic은 권한 정책도 단순하게 만듭니다.
예를 들어 Gate01 Device:
Publish 허용
parking/site01/gates/gate01/presence
parking/site01/gates/gate01/health
parking/site01/gates/gate01/alarm그리고:
Subscribe 허용
parking/site01/gates/gate01/commandControl Server:
Publish
parking/site01/gates/+/commandMonitoring Server:
Subscribe
parking/site01/+/+/health
parking/site01/+/+/alarm처럼 역할별 ACL을 구성할 수 있습니다.
이것이 Topic 계층과 Security Design이 함께 가야 하는 이유입니다.
20장 Topic 설계 오류는 어떤 장애를 만들까#
같은 장비가 서로 다른 Topic으로 Publish한다#
예:
parking/site01/gate01/status
parking/site1/gate01/statusDashboard는 한쪽만 Subscribe하고 있다면 일부 Message가 사라진 것처럼 보일 수 있습니다.
여러 장비가 같은 Topic을 공유한다#
parking/site01/sensor/status에 Sensor 100개가 Publish하면 어떤 Sensor의 값인지 구분하기 어려워질 수 있습니다.
Command와 Status Topic을 혼용한다#
ACL 설정이 복잡해지고 잘못된 Client가 Command를 Publish할 위험이 커집니다.
너무 넓은 Wildcard를 사용한다#
Client가 필요하지 않은 Message까지 대량으로 수신할 수 있습니다.
Topic이 너무 자주 변경된다#
Consumer와 Monitoring Rule, ACL을 모두 수정해야 합니다.
따라서 Topic Naming은 API Endpoint와 마찬가지로 외부 계약처럼 다루는 것이 좋습니다.
21장 실습으로 Topic 구조를 검증해보자#
시험 Broker에서 Subscriber를 실행합니다.
mosquitto_sub \
-h broker-test.local \
-t "parking/site01/+/+/status" \
-q 0다음 Topic에 Publish해봅니다.
mosquitto_pub \
-h broker-test.local \
-t "parking/site01/gates/gate01/status" \
-m '{"status":"online"}'이번에는 다른 Topic으로 보내봅니다.
mosquitto_pub \
-h broker-test.local \
-t "parking/site02/gates/gate01/status" \
-m '{"status":"online"}'첫 번째 Subscription은 site01만 대상으로 하므로 site02 Message는 들어오지 않아야 합니다.
이런 식으로:
정확한 Topic
한 단계 Wildcard
다단계 Wildcard
잘못된 Topic
권한 없는 Topic을 각각 시험해보면 Topic 설계를 검증하기 쉽습니다.
22장 Topic 설계 체크리스트#
□ Root Namespace가 명확한가?
□ Site ID 규칙이 있는가?
□ Device ID 규칙이 있는가?
□ 업무 Resource ID와 물리 Device ID를 구분했는가?
□ Topic의 대소문자 규칙이 통일됐는가?
□ 상태와 Event를 구분했는가?
□ Presence와 Health를 구분할 필요가 있는가?
□ Command Topic을 별도로 분리했는가?
□ Topic에 개인정보를 넣지 않았는가?
□ Wildcard Subscription Pattern을 검토했는가?
□ 너무 넓은 # 구독을 사용하지 않는가?
□ Topic이 지나치게 길지 않은가?
□ 변경 가능성이 높은 Metadata를 Topic에서 제거했는가?
□ Multi-Tenant Namespace가 필요한가?
□ ACL을 Topic 구조에 맞게 설계했는가?
□ Topic 변경 Migration 방법이 있는가?
□ Naming Convention을 문서화했는가?
□ Test Topic과 Production Topic이 분리돼 있는가?23장 자기 점검#
Topic을 계층적으로 설계하는 가장 큰 이유는 무엇인가#
장비와 데이터 종류를 명확하게 식별하고 Subscriber가 Wildcard를 이용해 필요한 범위만 효율적으로 구독할 수 있도록 하기 위해서입니다.
+와 #의 차이는 무엇인가#
+는 하나의 Topic Level을 대신하고 #는 이후 여러 Level을 포함할 수 있습니다.
차량번호를 Topic에 직접 넣어도 되는가#
가능하더라도 운영 Log와 Broker Monitoring 등에 노출될 수 있으므로 개인정보나 민감정보를 Topic에 직접 넣는 것은 피하는 편이 좋습니다.
Command와 Status를 왜 다른 Topic으로 나누는가#
Message의 의미가 명확해지고 Publish·Subscribe ACL을 역할별로 분리하기 쉬워집니다.
Topic을 변경할 때 왜 Migration 계획이 필요한가#
기존 Dashboard·Consumer·Monitoring·ACL이 이전 Topic을 사용하고 있을 수 있기 때문입니다.
24장 이 글을 마치며#
좋은 MQTT Topic은 단순히 보기 좋은 문자열이 아닙니다.
전체 구조는:
Namespace
↓
Site
↓
Resource / Device
↓
Data Type처럼 읽을 수 있어야 합니다.
예:
parking/site01/gates/gate01/presence
parking/site01/gates/gate01/health
parking/site01/gates/gate01/alarm
parking/site01/gates/gate01/command이런 구조를 사용하면:
Routing
Subscription
Monitoring
ACL
Troubleshooting을 같은 체계로 관리할 수 있습니다.
특히 다음 다섯 가지를 기억하면 됩니다.
Topic에는 Routing과 Subscription에 필요한 정보만 넣고, 변경 가능성이 높은 Metadata는 Payload나 Device Registry에 두는 편이 좋습니다.
상태·Event·Command를 분리하면 Subscriber 구조뿐 아니라 ACL과 장애 분석도 단순해집니다.
+와 # Wildcard를 어떻게 사용할 것인지 먼저 생각하면 실제 운영에 적합한 계층 구조를 만들기 쉽습니다.
차량번호 같은 개인정보를 Topic에 직접 넣으면 Broker Log나 Monitoring을 통해 노출될 수 있으므로 피하는 것이 좋습니다.
Topic Naming은 시스템이 커질수록 변경하기 어려워지므로 API Endpoint처럼 장기적으로 유지되는 계약으로 보고 처음부터 Convention과 Migration 정책을 정해야 합니다.
결국 MQTT Topic을 잘 설계한다는 것은 parking/site01/... 같은 문자열을 보기 좋게 만드는 것이 아닙니다.
장비가 수천 대로 늘어나도 어느 Message가 어디에서 왔고 누가 받아야 하며 누가 Publish할 수 있는지를 Topic 구조만으로 일관되게 관리할 수 있도록 만드는 것이 핵심입니다.