MQTT 재연결의 네이티브 스레드 누수를 막은 과정
키오스크 MQTT 연결에 Socket 폴백과 재연결 상태 제어를 추가하고 클라이언트 제거로 네이티브 스레드 누수를 막은 과정을 정리합니다.
원격 문 열기 기능은 처음에 Socket.IO 경로를 사용했다. MQTT를 주 경로로 추가하면서 연결 성공 여부, 실패 시 보조 경로, 앱의 백그라운드 복귀, 네이티브 클라이언트 해제까지 하나의 생명주기로 다뤄야 했다.
MQTT 우선·Socket 보조 경로에서 시작했다
초기 Relay 서비스는 MQTT 연결이 있으면 장치별로 구분되는 메시지 경로에 요청을 발행하고, 연결되지 않았거나 publish가 실패하면 Socket.IO 경로를 사용했다.
아래 코드 예시는 설명을 위해 식별자와 운영 설정값을 일반화했습니다.
const primary = await sendThroughPrimaryChannel(targetId, command);
if (primary.ok) return primary;
return sendThroughFallbackChannel(targetId, command);
요청 payload에는 UUID와 시각을 넣고 QoS 1로 발행했다. 후속 통합에서는 Socket 응답에서 최종 문 열림 ACK를 받아 요청 ID와 성공 여부를 로그로 남기는 흐름도 추가됐다.
flowchart LR
A[원격 문 열기 요청] --> B[MQTT Publish]
B --> C{전송 성공?}
C -- 예 --> D[키오스크 처리]
C -- 아니오 --> E[Socket 보조 경로]
D --> F[최종 ACK 기록]
E --> F연결 관리는 화면이 아니라 전역 Layout이 맡았다
연결 관리 컴포넌트는 지점 정보가 준비되면 초기 연결을 시작하고, 주기적으로 연결 상태를 확인했다. 앱이 foreground로 돌아왔을 때도 연결을 확인해 끊겨 있으면 재연결했다. 각 Effect cleanup에서는 interval과 AppState listener를 해제했다.
후속 리팩터링에서는 MqttClient를 싱글톤으로 바꿔 여러 화면이 서로 다른 인스턴스를 만들지 않게 했다. 연결 중에는 isConnecting 플래그로 중복 연결을 막았다.
disconnect()와 네이티브 자원 제거는 달랐다
문제는 재연결 때 새 네이티브 MQTT client를 만드는 경로였다. disconnect()는 연결을 끊지만 네이티브 client와 관련 스레드를 완전히 제거하지 않을 수 있었다. 개선 코드에는 새 client를 만들기 전에 MQTT.removeClient()를 호출하는 destroyClient()가 추가됐다.
private async releaseConnection() {
if (!this.connection) return;
try { this.connection.disconnect(); } catch {}
transport.remove(this.connection);
this.connection = null;
}
재연결은 client가 있으면 reconnect()로 인스턴스를 재사용하고, 없을 때만 connect()로 새로 만들었다.
if (this.connection) this.connection.reconnect();
else await this.connect();
앱에서 MQTT 연결은 계속 유지돼야 하므로 Layout 언마운트 때 무조건 disconnect하는 cleanup도 제거했다. 대신 중복 연결 방지와 명시적 destroy 경로를 client가 책임지게 했다.
스레드 수를 운영 로그로 관찰했다
Android의 활성 스레드와 Java·Native heap을 주기적으로 수집하는 모니터를 두고 초기 대비 증가율을 로그에 포함했다. 수집은 일정한 간격으로 수행하고 별도의 리포트 timer는 제거했다. 모니터 자체가 불필요한 timer를 늘리지 않게 하기 위한 조정이었다.
당시 재연결 시나리오의 프로파일링에서는 스레드가 약 100개에서 500개로 증가했고, 수정 후에는 약 120개 수준을 유지했다. 이는 해당 시나리오에서 관찰한 값이다. 변경에는 client 재사용, 중복 연결 방지, 네이티브 removeClient, timer·listener cleanup을 함께 적용했다.
연결형 라이브러리의 안정성은 connected Boolean 하나로 판단할 수 없다. 생성·연결·구독·재연결·파괴가 어느 객체의 책임인지 명확히 해야 장시간 실행되는 키오스크에서 누수를 막을 수 있다.