# MQTT 입력 중지 또는 결함

> MQTT 입력 데이터소스가 스트리밍 도중 유휴 상태로 전환되거나, 예측값이 `NaN`으로 나오거나, MQTT 출력 싱크가 "transport not connected"를 기록합니다 — 다섯 가지 뚜렷한 근본 원인과 이를 구별하는 방법.

## 증상

MQTT 기반 데이터소스가 스트리밍 중이었는데, 무언가 깨집니다:

- **추론이 스스로 실행 중에서 유휴/장애 상태로 전환됨** — 아무도 Stop을 누르지 않았습니다.
- **예측값이 `NaN`으로 돌아옴**, 또는 대시보드에 그럴듯한 값이 전혀 보이지 않음.
- **MQTT 출력 싱크가 쓰기 실패를 보고함**, 사유가 `transport: not connected`와 같음.
- **태그 매핑이 저장 시점에 거부됨**, `tag_outside_subscription_*` 메시지와 함께.
- **Enable 또는 Test Connection이 아예 실패함** — 아직 아무것도 스트리밍되지 않은 상태에서.

MQTT는 **푸시(push)** 프로토콜입니다 — 추론은 어댑터가 메시지 도착 시 채우는 캐시에서
매 틱마다 샘플 하나를 **가져옵니다(pull)**. 아래의 각 원인은 사실 다른 각도에서 본
같은 질문입니다: *캐시가 충분히 최근에 사용 가능한 값을 받았는가?*

## 확인

이 동일한 증상 집합을 다섯 가지 뚜렷한 장애 모드가 공유합니다. 백엔드 로그를 grep하여
구분하세요 — 아래 예시는 모두 오류 코드/사유만 보여주며, 페이로드 내용이나 자격 증명은
결코 보여주지 않습니다.

```bash
docker compose -f docker-compose.release.yml logs backend | grep -iE "MqttAdapter|ChannelReadException|channel .* read failed|tag stale|disconnected|not connected"
```

1. **정체된(stale) 토픽 (느리거나 죽은 퍼블리셔).** 장애 전에 경고가 발생합니다:

   ```
   MqttAdapter tag stale: tag=<topic> age=<age> staleAfter=<window>
   Channel <index> (<tag>) read failed — stopping inference
   Inference fault: <reason>
   ```

   캐시 만료 윈도우는 `clamp(50 / SamplingHz, 5s, 60s)`입니다 — 기본값인 10 Hz에서는
   **5초**입니다. 이 윈도우보다 느리게 발행되는 토픽은 만료되어 `ChannelReadException`을
   일으키며, 런타임은 이를 죽은 센서와 동일하게 취급합니다: 모델에 정지된(고정된) 값을
   공급하는 대신 추론을 중지합니다.

2. **`NaN` / `Infinity` 센티널.** MQTT 페이로드 파서는 오직 *유한한(finite)* 숫자만
   허용합니다 — `NaN`, `Infinity`, `-Infinity`, 그리고 `1e400`처럼 오버플로하는 리터럴은
   캐시에 도달하기 전에 모두 거부됩니다. 이런 방식으로 "측정값 없음" 센티널을 발행하는
   장치는 캐시된 값을 결코 갱신하지 못하므로, 위의 원인 1과 **정확히 동일한 방식으로**
   만료되어 실패합니다 — 동일한 로그 줄, 동일한 `ChannelReadException`. 설계상 "NaN이
   모델에 도달했다"는 별도의 장애 모드는 존재하지 않습니다. 그렇게 되면 이후 모든
   다운스트림 예측을 조용히 오염시키게 될 것이기 때문입니다.

3. **클라이언트 ID 축출(eviction).** 다음을 찾으세요:

   ```
   MqttAdapter disconnected (SessionTakenOver); reconnecting
   ```

   또는 출력 측에서, 쓰기가 다음과 함께 실패합니다:

   ```
   transport: not connected
   ```

   MQTT 브로커는 두 번째 클라이언트가 동일한 클라이언트 ID로 연결하면 *기존* 연결을
   축출합니다. 현재 빌드에서는 다음 경우에만 이런 일이 발생합니다: **동일한 역할의
   데이터소스 두 개**(입력 두 개, 또는 출력 두 개)가 하나의 명시적 **Client ID**를
   공유하거나, **외부 MQTT 클라이언트**가 데이터소스에 구성된 id를 재사용하는 경우입니다.

   
**하나의 구성에 있는 입력/출력 쌍은 더 이상 충돌하지 않습니다 (v1.28.0)**

   구성된 **Client ID**는 연결 시점에 자동으로 역할 접미사가 붙습니다(입력 어댑터는
   `<id>-in`, 출력 싱크는 `<id>-out`) — 이는 예전에 공유 기본값이던 `aiboard`를 그대로
   가지고 있는 구성을 포함해 저장된 모든 구성에 적용되므로, *동일한* 데이터소스 구성으로
   만들어진 MQTT 입력과 출력은 더 이상 서로를 축출할 수 없습니다. 입력/출력 쌍에서 여전히
   이 문제가 보인다면 v1.28.0보다 오래된 빌드를 사용 중인 것입니다 — id를 직접 편집하는
   대신 업그레이드하세요.
   

4. **구독한 Base Topic 밖의 태그.** 태그 매핑을 저장(또는 데이터소스 활성화)하면 즉시
   다음과 함께 실패합니다:

   ```
   tag_outside_subscription_<tagRef>
   ```

   입력 어댑터는 매핑된 태그(또는 구성된 **Base Topic** 와일드카드)에만 구독합니다 —
   그 범위 밖의 토픽 경로는 조용히 결코 값을 생성하지 않는 대신, 저장 시점에 거부됩니다.

5. **소켓 수준 연결 실패.** 스트리밍이 시작되기도 전에 Test Connection 또는 Enable이
   다음 중 하나와 함께 실패합니다:

   connect_failed 도달 불가 / 거부됨 / DNS 실패 &nbsp;
   timeout 연결 시도가 완료되기 전에 취소됨

   백엔드 로그는 경과 시간과 예외 유형으로 이를 뒷받침합니다:

   ```
   MqttAdapter connect failed: broker=<broker> elapsedMs=<ms> errorType=<type>
   ```

   
**Docker localhost 함정**

   컨테이너 내부에서 `localhost`는 **백엔드 컨테이너 자신**을 의미하며, 데스크톱이나
   Docker 호스트를 의미하지 않습니다. 데스크톱 MQTT 클라이언트가 `localhost:1883`에서
   문제없이 도달하는 브로커라도, AIBOARD에서는 동일한 주소로 도달할 수 없습니다 —
   대신 브로커의 서비스/컨테이너 이름(예: Docker 네트워크에서 `mqtt://mosquitto:8884`)
   이나 `host.docker.internal`을 사용하세요.
   

## 해결

1. **정체된 토픽 / NaN 센티널 (원인 1–2).** 퍼블리셔의 주기를 고치거나, 유한하지 않은
   센티널을 발행하지 않도록 한 뒤 데이터소스를 다시 활성화하세요 — 장애는 런타임을
   유휴 상태로 무너뜨리므로, 복구는 자동이 아닙니다:
   - 매핑된 토픽의 실제 발행 간격을 확인하고, 데이터소스에 구성된 **Sampling Hz**의
     만료 윈도우와 비교하세요.
   - 토픽이 정당하게 "측정값 없음"을 보고한다면, 장치/게이트웨이가 그 기간 동안
     `NaN`/`Infinity`를 보내는 대신 단순히 **발행하지 않도록** 전환하세요 — 둘 다
     동일하게(조용한 토픽) 취급되므로, 센티널을 보내는 데는 아무런 이점이 없습니다.
   - 데이터소스를 다시 **활성화**한 뒤(모델을 다시 로드하고 어댑터를 재연결), **Start**를
     누르세요.

2. **클라이언트 ID 축출 (원인 3).** 명시적 **Client ID**를 공유하는 동일한 역할의
   데이터소스(입력 두 개, 또는 출력 두 개) 각각에 서로 다른 값을 부여하거나, 필드를
   비워 MQTTnet이 각 쪽에 고유 id를 자동 생성하도록 하세요. 외부 클라이언트가 여러분의
   데이터소스 id를 재사용하고 있다면, 둘 중 하나를 변경하세요. 이는 동일한 구성으로
   만들어진 입력/출력 쌍에는 **적용되지 않습니다** — v1.28.0의 역할 접미사(`-in`/`-out`)가
   이미 그 둘이 충돌하지 않도록 하므로 편집이 필요 없습니다.

3. **구독 밖의 태그 (원인 4).** 태그의 토픽 경로를 포함하도록 데이터소스의
   **Base Topic**을 넓히거나, 태그의 토픽 경로를 현재 Base Topic 안으로 들어오도록
   수정하세요. 매핑을 다시 저장하세요.

4. **소켓 실패 (원인 5).** 데스크톱이 아니라 백엔드 컨테이너 *내부에서* 호스트, 포트,
   네트워크 도달 가능성을 확인하세요. `localhost`를 브로커의 컨테이너/서비스 이름으로
   교체하거나(또는 컨테이너에서 Docker 호스트로는 `host.docker.internal`), **Test
   Connection**을 재시도하세요.

## 예방

- 새 MQTT 입력을 활성화하기 전에 **매핑된 모든 토픽의 발행 간격을 만료 윈도우와 대조해서
  확인하세요** — 토픽이 정당하게 느리게 발행한다면 **Sampling Hz**를 낮춰 허용 범위를
  넓히세요(1 Hz → 50초).
- **동일한 역할의 데이터소스 두 개 사이에 하나의 명시적 Client ID를 재사용하지 마세요**
  (입력 두 개, 또는 출력 두 개), 그리고 데이터소스의 Client ID를 외부 MQTT 클라이언트에
  재사용하지 마세요. 동일한 구성의 입력/출력 쌍은 이미 안전합니다 — v1.28.0이 각 쪽에
  자동으로 접미사(`-in`/`-out`)를 붙입니다.
- **Base Topic을 실제로 매핑하는 태그에 맞게 좁게 설정**하고, 이미 그 범위 안에 있는
  토픽 경로를 가진 태그만 매핑하세요.
- **"데이터 없음"을 `NaN`/`Infinity`로 회선에 인코딩하지 마세요.** 보고할 것이 없을 때
  조용해지는 장치는 동일하게 동작하며, 센티널 값보다 추론하기 쉽습니다.
- **워크스테이션이 아니라 박스의 관점에서 Test Connection을 실행하세요** — 데스크톱에서
  `localhost`로 도달 가능한 브로커라도 백엔드 컨테이너에서는 전혀 도달하지 못할 수
  있습니다.

## 관련 항목

- [알림](/ko/operate/notifications/) — 이런 장애를 UI에 표시하는 비자발적 중지 알림과
  출력 쓰기 실패 알림.
- [입력 데이터소스 연결](/ko/configure/input-datasources/) — 만료 윈도우, 재연결 동작,
  유한하지 않은 값 거부에 대한 전체 내용.
- [출력 데이터소스](/ko/configure/output-datasources/) — MQTT 게시 QoS, 클라이언트 ID
  접미사, Test Connection 진단.
- [데이터소스 다운 또는 장애](/ko/troubleshooting/datasource-down-or-faulted/) — 이
  페이지가 MQTT용으로 특화한 일반 Enable/장애 런북.
