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
SENSOR012

Topic:

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/gate01

Payload:

{
  "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/state

Payload:

{
  "occupied": true
}

과거에 몇 번 바뀌었는지보다 지금:

occupied = true

인지가 중요합니다.

이런 Topic은 Retained Message와 잘 맞을 수 있습니다.

Event#

과거에 발생한 하나의 사건입니다.

예:

parking/site01/events/vehicle-entry

Payload:

{
  "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/status

Payload:

{
  "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 System

Topic을 한 번에 바꾸면 모든 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/state

Payload:

{
  "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/command

Control Server:

Publish

parking/site01/gates/+/command

Monitoring Server:

Subscribe

parking/site01/+/+/health
parking/site01/+/+/alarm

처럼 역할별 ACL을 구성할 수 있습니다.

이것이 Topic 계층과 Security Design이 함께 가야 하는 이유입니다.

20장 Topic 설계 오류는 어떤 장애를 만들까#

같은 장비가 서로 다른 Topic으로 Publish한다#

예:

parking/site01/gate01/status

parking/site1/gate01/status

Dashboard는 한쪽만 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 구조만으로 일관되게 관리할 수 있도록 만드는 것이 핵심입니다.

이 페이지의 목차