SKIX Manager Main 장치 제어 리팩터링 분석
refactoring/skix-manager-main은 main의 851cacb4에서 갈라져 나온 뒤,
df342d4a부터 91a8ae92까지 9개 커밋으로 LLM Agent의 장치 제어 경계를
기존 DeviceControl scalar IPC에서 SKIX SDK의 도메인별 Manager API로 옮겼다.
핵심 성과는 조회·상태·보안·스케줄 호출을 typed DeviceCommunicator port로
노출하고, 앱 코드가 raw Bundle 결과를 직접 해석하던 책임을 adapter 내부로
모은 것이다. 다만 쓰기 명령 큐에는 아직 DeviceCommand(method: String,
inData: Map)가 남아 있으므로 완전한 end-to-end 타입화가 끝난 상태는 아니다.
분석 기준
| 항목 | 기준 |
|---|---|
| 기준 브랜치 | origin/main |
| 기준 커밋 | 851cacb4 (version change v1.20.07 / db 133) |
| 분석 HEAD | 91a8ae92 |
| 커밋 범위 | origin/main..refactoring/skix-manager-main의 9개 커밋 |
| 파일 변화 | 15개 파일, 1,678줄 추가, 1,426줄 삭제 |
| 검증 수준 | Git diff·코드 정적 분석·SDK 경계 검사 |
| 제외 | Android 전체 빌드, APK 설치, DeviceAgent/SKIX Service 실연결, 실기기 음성 시나리오 |
변경량에는 자동 재기준화된 dictionary_positive_test.json 1,171줄과 바이너리
AAR/SO가 포함된다. 따라서 줄 수만으로 리팩터링 복잡도를 평가하면 안 된다.
무엇이 바뀌었나
1. DeviceControlProxy 직접 결합을 SkixManager로 교체
기존 DeviceCommunicator는 DeviceControlProxy에 문자열 method와 Bundle을
전달하고, 반환 Bundle을 Map<String, Any>로 변환했다. 호출자는 키 이름,
문자열 boolean, JSON payload의 형식을 알아야 했다.
변경 후 구현체는 SkixManager.getInstance(context)를 소유하고 다음 SKIX SDK Manager
도메인 API를 호출한다.
| SDK 도메인 | Agent가 사용하는 대표 계약 |
|---|---|
llm() |
상태, STT/TTS text, 날씨·공기질 요청, AI log, upload URL, LlmEvent |
setting() |
언어·음성·볼륨·밝기·privacy·lock·night mode·대기화면 theme |
execution() |
pause/resume/stop과 cleaning·interaction·movement 상태 snapshot |
airCleaner() |
운전 상태, 청정 위치, 운전 request |
security() |
SecurityBasicAvailability, isB2bMode() |
schedule() |
on-device schedule 조회·등록·중지·모드 판정 |
error() |
active error code와 DeviceErrorUpdate listener |
airQuality() |
IndoorAirQuality snapshot |
manage() |
alert, battery, filter, firmware, AWS 연결, 환경·locale |
map() / amr() |
map 존재와 station 상태 |
interaction() |
launcher screen request |
연결 lifecycle도 initializeDeviceControlProxy()/DeviceControlProxy.disconnect()에서
initializeSkixManager()/SkixManager.connectDevice()/disconnectDeviceController()로
바뀌었다. LLM, error, setting mode callback은 각각 SDK listener로 분리됐다.
2. 소비자가 구현체 대신 typed DeviceCommunicator port에 의존
service/api/DeviceCommunicator.kt는 기존의 범용 함수
sendCommand(command, params): Map<String, Any>와 getSingleData()를 제거하고
의미가 드러나는 메서드를 제공한다.
| 기존 호출 | 변경 후 |
|---|---|
sendCommand(GET_ERROR_STATUS) 후 errorCode cast |
getActiveErrorCodes(): List<String> |
sendCommand(GET_DEVICE_STATUS) 후 JSON 역직렬화 |
getDeviceActionStatus(): DeviceActionStatus? |
getSingleData(GET_LOCK_MODE_ENABLE, enable) |
isLockModeEnabled(): Boolean |
sendCommand(GET_SECURITY_BASIC_AVAILABLE) |
getSecurityBasicAvailability() |
sendCommand(GET_SCHEDULE) |
getOnDeviceSchedules(): List<DeviceSchedule> |
sendCommand(ADD_SCHEDULE, Map) |
registerOnDeviceSchedule(AppScheduleRequest) |
sendCommand(SET_DEVICE_STATUS, Map) |
controlDeviceExecution(Action, ExecutionScope, Boolean) |
ApiCallValidator, ForegroundService, FunctionCallHandler도 concrete
service.DeviceCommunicator가 아닌 service.api.DeviceCommunicator를 받도록
정리됐다. 이로써 validator와 handler의 테스트 대역을 구현체 상속 없이 만들 수
있고, raw transport 형식 변경의 영향이 adapter 쪽으로 좁아진다.
3. callback도 도메인 event로 전환
기존 단일 callback은 method string과 Bundle을 해석해 날씨, 공기질, error,
언어, 음성, TTS 요청을 분기했다. 변경 후에는 다음 listener가 역할을 나눈다.
SkixLlmManager.EventListener:LlmEvent.Type별 TTS, 상태, 언어, 날씨, 공기질, AI log, S3 upload URL 처리SkixErrorManager.Listener:DeviceErrorUpdate에 따라 TTS 중지 판단SkixSettingManager.ModeListener: privacy/lock 상태 변경 반영SkixConnectionListener: 연결 완료 시 언어·voice·환경·locale·음성 인식 상태 초기화
이 변화는 callback payload key 누락이나 잘못된 cast를 줄이지만, SDK event type과 Agent store의 대응이 정확한지는 통합시험으로 확인해야 한다.
보안·스케줄 계약 변화
B2B 보안모드
eca4c33f는 GET_B2B_MODE 결과에서 문자열 isB2B를 꺼내던 로직을
security().isB2bMode()로 교체했다. 기본 가능 여부도
SecurityBasicAvailability의 isB2bRestricted(),
isSubscriptionRequired(), isAvailable()로 판정한다.
구버전 보드에서 isB2bMode() API가 없을 수 있다는 기존 호환 전략은 유지한다.
직접 호출이 실패하면 false로 두고 typed availability 판정으로 이어지므로,
API 미지원 하나만으로 보안모드 전체가 unknown error로 종료되지 않는다.
On-device 스케줄
기존 schedule map은 AppScheduleRequest와 DeviceSchedule로 전환됐다.
SDK의 ScheduleRegistrationResult(delivered, accepted, code, message)를 기존
Sk48Schedule.RegisterResult로 바꾸는 overload도 추가됐다. accepted=true 또는
빈 code는 성공, REDUNDANCY_ERROR/중복 메시지는 중복, 나머지는 일반 오류로
정규화한다.
함께 포함된 기능 보정
| 커밋 | 분류 | 내용 |
|---|---|---|
df342d4a |
패키징 보정 | release에서 제외되던 native .so를 포함하고 libGenie.so 추가 |
4c6e9073 |
핵심 리팩터링 | legacy DeviceControl AAR을 SKIX Framework SDK AAR로 교체하고 도메인 Manager 적용 |
eca4c33f |
계약 리팩터링 | B2B 판정을 typed security API로 전환 |
f18fcafe |
경계 정리 | sendCommand()/getSingleData() scalar adapter 제거 |
2dd595d4 |
회귀 방지 | raw DeviceAgent IPC와 legacy AAR 재유입을 막는 검사 스크립트 추가 |
2adcc914 |
오류 분류 수정 | S07_4 주행 불가 기타 오류를 관리용 OtherErrors에 포함 |
7d1ee371 |
matcher 수정 | 지나치게 넓은 영문 <sk_19> 규칙 Who is/Who + current/currently 제거 |
810ecee0 |
의존성 역전 | validator·service·handler가 typed DeviceCommunicator port에 의존 |
91a8ae92 |
test fixture | 현재 matcher 결과에 맞게 positive fixture 재기준화 및 도구 명시 |
남아 있는 구조적 경계
쓰기 큐는 아직 문자열·Map 기반
FunctionCallHandler의 다수 setter는 계속 DeviceCommandMethod와
MutableMap<String, Any>로 DeviceCommand를 만든다. adapter의
executeTypedCommand()가 이 값을 다시 when으로 분기해 SDK request를 만든다.
즉 현재 구조는 다음 두 단계 중 첫 단계가 완료된 상태다.
- 완료: consumer query/state/security/schedule API와 raw IPC 분리
- 미완료: setter command까지 sealed command 또는 SDK request로 완전 타입화
이 잔존 adapter는 기존 명령 큐의 순서·취소 동작을 유지하는 완충층이지만,
잘못된 key나 type은 컴파일 때가 아니라 executeTypedCommand()에서 false로
거절된다. 다음 단계에서는 queue payload 자체를 도메인 command로 바꾸는 것이
가장 큰 구조 개선점이다.
port가 SDK model을 직접 노출
port가 AirCleanerStateSnapshot, AvailableCleaningPosition,
SecurityBasicAvailability, DeviceSchedule 등 SDK model을 반환한다. raw IPC
결합은 제거됐지만 application 계층은 여전히 SDK DTO 변화의 영향을 받는다.
장기적으로 SDK 교체 가능성까지 목표로 한다면 Agent-owned model로 한 번 더
매핑해야 한다.
코드 리뷰에서 발견한 후속 확인 항목
| 우선순위 | 정적 근거 | 필요한 확인 |
|---|---|---|
| 높음 | requestOutdoorAirQualityData()가 requestWeatherForecast()를, requestOutdoorWeatherData()가 requestOutdoorAirQuality()를 호출 |
메서드명과 SDK 호출이 교차된 의도인지 확인하고 날씨·공기질 각각 callback E2E 검증 |
| 높음 | getNightModeScheduleTime()가 SDK list의 [0], [1]을 길이 확인 없이 접근 |
미설정·부분 설정 상태의 반환 계약과 예외 처리 검증 |
| 중간 | 미지원 또는 key/type 불일치 command는 log만 남기고 drop | 모든 DeviceCommandMethod와 executeTypedCommand() coverage 대조 |
| 중간 | version 없는 skix-framework-sdk-release.aar 바이너리를 저장소에 직접 포함 |
SDK 산출 버전·source commit·호환 DeviceAgent 버전을 릴리즈 메타데이터에 기록 |
| 중간 | 여러 getter가 연결 준비 상태를 일관되게 검사하지 않음 | 연결 전·재연결 중 query 호출과 SDK exception contract 검증 |
첫 번째 항목은 코드 이름과 호출 대상이 정반대로 배치된 정적 위험 후보다. 실제 SDK 명세와 실기기 callback을 확인하지 않았으므로 이 문서에서는 확정 장애로 판정하지 않는다.
검증 결과와 경계
| 검증 | 결과 | 의미 |
|---|---|---|
git log origin/main..HEAD |
9개 커밋 확인 | 사용자 지정 분석 범위와 일치 |
git diff --stat origin/main...HEAD |
15개 파일, +1,678/-1,426 |
변경 규모 확인 |
bash tools/verify-skix-sdk-boundary.sh |
통과 | LLM source에 raw DeviceControlProxy/IDeviceControl, 직접 sendCommand, getSingleData, legacy AAR 의존 없음 |
| Android unit/build | 사용자 요청에 따라 미수행 | 컴파일·단위 회귀 결과를 이 문서에서 주장하지 않음 |
| 실기기 SKIX 연동 | 실기기 미검증 | connect, listener, 장치 실행, 날씨·공기질, 보안·스케줄 E2E는 별도 Gate |
SDK 경계 검사는 금지 패턴의 부재를 확인하는 정적 guard다. typed method의 의미가 정확하거나 실제 DeviceAgent와 호환된다는 증거는 아니다.
권장 검증 순서
- SDK/DeviceAgent 호환 버전과 AAR provenance를 확정한다.
- connect/disconnect/reconnect와 listener 중복 등록 여부를 확인한다.
- 날씨와 실외 공기질 요청을 각각 실행해 request/event/store 대응을 확인한다.
- privacy·lock·B2B·subscription 조합별 보안모드 응답을 확인한다.
- map 유무와 중복 schedule별
ScheduleRegistrationResult매핑을 확인한다. - setter queue의 모든 command를 SDK domain request와 일대일 대조한다.
- 마지막으로 음성 발화 → matcher → function call → 장치 상태 → TTS까지 회귀한다.
결론
이번 9개 커밋은 단순 AAR 교체가 아니라 Agent가 DeviceAgent transport 세부사항을 직접 해석하던 구조에서 SKIX SDK의 도메인 계약을 사용하는 구조로 이동한 1단계 리팩터링이다. 조회와 핵심 상태 판단의 타입 안정성, listener 책임 분리, consumer의 port 의존은 분명한 개선이다.
다만 setter queue의 문자열·Map envelope, SDK model 직접 노출, 날씨/공기질 요청 교차 가능성은 남아 있다. 따라서 현재 판정은 정적 SDK 경계 전환 완료, 제품 동작 검증 전이다.