SKIX 응용 프로그램 아키텍처 표준
먼저 이해할 내용
이 문서는 PUI와 시스템 앱을 같은 구조와 규칙으로 개발하기 위한 기준이다. 핵심은 세 영역의 역할을 섞지 않는 것이다.
| 표기 | 이 문서에서의 의미 |
|---|---|
| 현재 구현 | 현재 MR7 계열 앱에서 확인되는 직접 Binder 호출, 주기 조회와 화면 이벤트 처리 |
| 병합 후보 | task_manager 브랜치의 TaskManager, 완료 근거 저장소, Task callback과 호환 Bridge |
| 목표 계약 | 공개 Manager API, 정규화된 Task 상태, Repository Flow와 상태 기반 화면 갱신 |
아래 PUI 흐름은 목표 UX와 목표 계약이다. 현재 앱이 이미 전 과정을 구현했다는 의미가 아니다.
| 영역 | 쉬운 설명 | 대표 책임 |
|---|---|---|
| 화면이 하는 일 | 현재 상태를 보여주고 사용자의 입력을 전달한다. | 청정 상태 표시, 정지 버튼 입력, 오류 안내 |
| 앱 내부가 하는 일 | 입력의 의미를 정리하고 화면에 필요한 상태를 만든다. | ViewModel, 선택형 UseCase, Repository |
| 제품 실행 서비스가 하는 일 | 요청이 가능한지 판단하고 실제 기기 동작을 끝까지 관리한다. | 상태·정책·Task·실제 완료 근거 |
사용자 입력
→ 화면
→ 앱 내부
→ 제품 실행 서비스
→ 실제 기기 기능
→ 실행 결과
→ 앱 상태
→ 화면 갱신
화면은 기기 동작이 성공했다고 임의로 판단하지 않는다. 제품 실행 서비스가 실제 결과를 확인하고, 앱은 그 결과를 받아 화면을 바꾼다.
PUI 예시 · 청정 진행 화면
사용자가 보게 되는 일
청정 중인 화면에서 사용자가 정지 버튼을 누른다.
- 화면이 멈추는 중으로 바뀐다. 사용자는 버튼 입력이 전달됐다는 것을 바로 알 수 있다.
- 앱이 정지 요청을 제품 실행 서비스에 전달한다.
- 기기가 실제 청정을 멈춘다.
- 멈춘 결과가 앱으로 돌아오면 화면이 청정 종료 상태로 바뀐다.
정지 버튼 누름
→ 화면에 멈추는 중 표시
→ 기기가 실제 청정을 멈춤
→ 청정 종료 화면으로 변경
화면은 버튼이 눌렸다는 이유만으로 청정이 끝났다고 표시하지 않는다. 실제 기기가 멈췄다는 결과를 받은 뒤에 종료 화면으로 바꾼다.
그림 1. 정지 버튼 입력부터 실제 청정 정지와 화면 갱신까지
원본 SVG 열기
- 이 그림이 답하는 질문: 정지 버튼을 누른 뒤 화면은 언제
청정 종료로 바뀌는가? - 읽는 순서: 정지 버튼, 멈추는 중, 실제 청정 정지, 청정 종료 화면 순서로 읽는다.
- 현재 검증 범위: 사용자가 보아야 할 상태 계약이다. 기존 PUI 전체 화면 이관과 실기기 청정 정지는 검증 전이다.
개발자 참고 · 내부에서 연결되는 구조
| 사용자가 보는 단계 | 내부에서 맡는 곳 | 하는 일 |
|---|---|---|
| 정지 버튼 누름 | AirClearWorkFragment |
버튼 입력을 StopClicked라는 UiAction으로 전달 |
| 멈추는 중 표시 | AirClearViewModel · AirClearUiState |
정지 요청이 진행 중인 화면 상태 작성 |
| 실제 청정 정지 | AirCleanerRepository → SkixTaskManager → DeviceAgent 청정 실행기 |
실행 가능 여부를 확인하고 실제 기기 기능 호출 |
| 청정 종료 화면 | 완료 근거 → TaskState → Repository → ViewModel |
실제 정지 결과를 새 화면 상태로 변환 |
- 이 그림이 답하는 질문: 청정 화면, 앱 내부, Task 추적과 실제 청정 실행은 무엇을 나눠 맡는가?
- 읽는 순서: 화면 상태에서 앱 내부, 작업 접수·추적, CleaningManagerService 순서로 읽고 아래 결과 경로를 확인한다.
- 현재 검증 범위: 현재 클래스와 목표 ManagerService를 함께 표시한 이행 구조다. DeviceAgent 청정 실행 책임 전환은 아직 완료되지 않았다.
여러 정책이나 기능을 묶어야 할 때만 UseCase를 사이에 둔다. 단순 정지는 ViewModel이 Repository를
바로 호출할 수 있다. 실패 안내, 사용자 확인, 완료 뒤 이동처럼 유실되면 안 되는 결과는 UiState로
표현한다. UiEffect는 업무 결과와 무관한 짧은 애니메이션·포커스·스크롤 같은 반응에만 제한한다.
완료 결과는 어디서 만들어지는가
AirCleanerRepository가 TaskState를 새로 만드는 것은 아니다. Repository가
setAirCleanerOperation 전용 완료 listener를 붙여 청정 완료를 추측하는 구조도 아니다. 청정 기능이
남긴 물리 완료 근거를 TaskManager가 공통 Task 상태로 확정하고, 앱은 공통 Task 이벤트에서 자신이
제출한 taskId의 결과만 선택해 화면 상태로 바꾼다.
그림 2. 요청 접수와 실제 완료를 분리한 PUI 청정 Task 왕복 구조
원본 SVG 열기
- 이 그림이 답하는 질문: 청정 정지 요청은 어떤 여섯 단계를 거쳐 같은 taskId의 실제 완료로 돌아오는가?
- 읽는 순서: 사용자 입력부터 Task 제출, 실제 정지, 완료 근거, Task 완료, 화면 갱신 순서로 읽는다.
- 현재 검증 범위: MR6 후보 코드와 목표 완료 계약을 연결한 구조다. 실제 완료 producer 연결과 PUI APK 검증은 남아 있다.
1. 정지 요청은 Task 제출 형식으로 바뀐다
현재 task_manager 브랜치에서 AirCleanerRepository.stopAirClear()는 기존과 같이 다음 요청을 만든다.
method = setAirCleanerOperation
action = STOP
mode = 현재 청정 모드
DeviceControlResolver.resolve()가 반환한 DeviceControl은 TaskManagerCommandBridge로 감싸져 있다.
Bridge는 상태 변경 명령을 다음과 같이 변환한다.
method = submitTask
taskMethod = setAirCleanerOperation
source = pui
traceId = pui-<uuid>
correlationId = traceId
action = STOP
mode = 현재 청정 모드
이 변환은 기존 PUI 코드가 setAirCleanerOperation을 호출하는 계약을 유지하면서, DeviceAgent 내부에서는
작업을 Task 생명주기로 관리하기 위한 호환 경계다. 조회·상태 확인 명령은 그대로 유지하고, 실제 상태를
바꾸는 명령만 TaskManager 경로로 보낸다.
TaskManagerCommandBridge의 현재 분류는 메서드 이름에 move, station, clean, schedule 등이
포함됐는지 확인하는 전환기 호환 규칙이다. 모든 상태 변경을 의미적으로 판별하는 최종 구조가 아니다.
신규 공개 API는 기능 명세와 명확한 operation ID로 Task 여부를 결정하고, 메서드 이름 추정은 기존 호출을
옮기는 기간에만 사용한다.
2. 접수 성공과 청정 완료는 다른 상태다
submitTask의 동기 응답은 작업을 접수했는지를 알려준다. TaskResult 기준으로는 accepted,
taskId, state, errorCode, reason을 읽을 수 있다. 이 시점에 앱이 표현할 수 있는 상태는
요청 전달됨 또는 멈추는 중이며, 아직 청정 종료가 아니다.
| 구분 | 의미 | 대표 데이터 | 화면이 해도 되는 표현 |
|---|---|---|---|
| 요청 생성 | 사용자가 정지를 요청함 | StopClicked |
버튼 중복 입력 방지 |
| Task 접수 | TaskManager가 요청을 받음 | accepted, taskId, QUEUED/RUNNING |
멈추는 중 |
| 기능 호출 반환 | 기존 청정 API 호출이 반환됨 | executor 반환값 | 완료로 표시하지 않음 |
| 물리 완료 근거 | 청정 Domain이 실제 정지를 관찰함 | cleaning.stopped |
Task 결과 대기 |
| Task 최종 상태 | Platform Adapter가 내부 결과를 공개 상태로 정규화함 | SUCCEEDED, FAILED, CANCELLED, TIMED_OUT |
최종 화면으로 전환 |
API가 예외 없이 반환됐다는 사실은 명령 전달 성공에 가깝다. 모터·팬·청정 세션이 실제로 멈췄다는
뜻은 아니다. 따라서 sendModuleCommand()가 반환된 직후 청정 종료로 바꾸면 실제 기기 상태와 화면이
어긋날 수 있다.
3. 청정 Domain이 물리 완료 근거를 기록한다
TaskManager는 기존 청정 동작을 다시 구현하지 않는다. CleaningAmpTaskExecutor가 기존
setAirCleanerOperation 실행 경로를 호출하고, 완료 목표가 cleaning.stopped인 Task는
TaskCompletionWatcher를 통해 실제 완료 근거를 기다린다.
기본 청정과 이동 청정의 정지 callback인 BaseCleaningManager.onStop()에서
TaskCompletionStateStore.mark()가 cleaning.stopped를 기록한다. 이 지점은 단순 API 반환이 아니라
청정 Domain이 실제 정지를 확인한 뒤 도달하는 근거 지점이다.
BaseCleaningManager.onStop()
→ TaskCompletionStateStore.mark(
target = cleaning.stopped,
areaId = 현재 청정 영역,
action = STOP
)
→ TaskCompletionWatcher 대기 해제
→ CleaningAmpTaskExecutor 완료
완료 근거의 소유자는 청정 Domain이고, TaskManager는 그 근거를 기다려 Task 생명주기를 확정한다. PUI와 Repository는 기기 상태를 추정하거나 가짜 완료 이벤트를 만들지 않는다.
4. TaskManager가 공통 완료 이벤트를 보낸다
완료 근거가 확인되면 병합 후보 TaskManager는 내부 Task를 COMPLETED로 바꾸고 공통
onTaskEvent를 보낸다. 현재 후보 구현은 시간 초과도 FAILED와 errorCode=timeout으로 전달한다.
앱의 Platform Adapter는 이 내부 차이를 아래 공개 계약으로 정규화한다.
method = onTaskEvent
eventName = TASK_STATE_CHANGED
taskId = 제출 응답에서 받은 Task 식별자
state = SUCCEEDED | FAILED | CANCELLED | TIMED_OUT
reasonCode = timeout을 포함한 안정된 실패 사유
result = 실행 결과
앱은 TaskManagerClientCallback.onTaskEvent()에서 이 이벤트를 받는다. 기능별 Binder listener를 계속
추가하는 대신, 앱 프로세스에 하나의 공통 Task callback을 연결하고 Platform Adapter 또는 Task
Repository에서 taskId별 상태 흐름으로 나누는 편이 안전하다.
5. 앱은 Task 상태를 화면 상태로 변환한다
Task 상태와 화면 상태는 같은 객체가 아니다. Task Repository는 기기 실행 상태를 앱 계약으로 변환하고,
ViewModel은 화면에 필요한 값만 AirClearUiState로 만든다.
TaskManagerClientCallback.onTaskEvent()
→ Task Platform Adapter
→ taskId로 해당 Task만 선택
→ Task Repository의 Flow<TaskState>
→ AirClearViewModel
→ AirClearUiState
→ AirClearWorkFragment
| 공개 Task 상태 | Domain/App 해석 | 화면 상태 |
|---|---|---|
QUEUED 또는 RUNNING |
정지 Task가 실행 대기 중이거나 실행 중 | stopping=true, 멈추는 중 |
SUCCEEDED |
cleaning.stopped 근거까지 확인됨 |
running=false, 청정 종료 |
FAILED |
실행 실패 | 기존 청정 상태 유지, 실패 사유와 다음 행동을 UiState에 표시 |
CANCELLED |
정지 요청 자체가 취소됨 | 상태 재조회 뒤 취소 결과를 UiState에 표시 |
TIMED_OUT |
제한 시간 내 완료 근거 없음 | 종료로 단정하지 않고 상태 확인 안내를 UiState에 표시 |
ViewModel은 SUCCEEDED를 받은 뒤에만 청정 종료 화면을 만든다. FAILED나 TIMED_OUT에서는 현재 기기
상태를 다시 조회하고, 화면에는 사용자가 이해할 수 있는 실패 이유와 다음 행동을 표시한다.
현재 AirCleanerRepository가 보강할 부분
현재 AirCleanerRepository.stopAirClear()는 sendModuleCommand()의 outdata를 로그로만 남기고,
예외가 없으면 true를 반환한다. 따라서 TaskManager가 내려준 접수 결과와 taskId를 버리지 않고 반환하는
보강이 필요하다.
| 현재 | 보강 방향 | 이유 |
|---|---|---|
Boolean로 호출 예외 유무만 반환 |
TaskSubmission(accepted, taskId, state, error) 반환 |
접수와 완료를 구분하고 후속 이벤트를 연결하기 위해 필요 |
| 정지 직후 상태 재조회 | 공통 onTaskEvent를 Flow<TaskState>로 변환 |
polling 간격 없이 실행 상태를 받기 위해 필요 |
| 기능 Repository가 Binder 세부사항까지 처리 | TaskManagerClient 연결·재연결은 Platform Adapter에 격리 |
Feature 코드와 Service 생명주기를 분리하기 위해 필요 |
| 호출 직후 종료 이벤트 발생 가능 | 공개 상태 SUCCEEDED를 받은 뒤에만 청정 종료 화면 |
물리 완료 근거와 화면을 일치시키기 위해 필요 |
| 프로세스 재생성 시 진행 Task 연결 정보 소실 | 저장한 taskId로 getTaskStatus(taskId) 조회 후 관찰 재개 |
callback 유실 구간을 복구하기 위해 필요 |
AirCleanerRepository는 청정 요청과 청정 상태 접근을 제공하고, 공통 Task 관찰은 별도 Task
Repository 또는 Platform Adapter가 맡는다. 두 결과는 ViewModel에서 결합한다. 이렇게 해야 청정 외의
이동·화면·TTS·측정 Task도 동일한 완료 계약을 재사용할 수 있다.
실제 코드와 목표 역할
MR7의 청정 화면은 이미 View, ViewModel, 상태, Repository로 나뉘어 있다. 목표는 새 구조로 전면 교체하는 것이 아니라 섞여 있는 책임을 단계적으로 분리하는 것이다.
| 현재 코드 | 현재 역할 | 목표 구조에서 보강할 점 |
|---|---|---|
AirClearWorkFragment |
청정 상태 표시, 버튼 입력, 일부 화면 전환 | 사용자 입력은 UiAction, 화면에 남아야 하는 결과는 UiState로 구분 |
AirClearViewModel |
여러 Repository 조회, AirClearUiState와 이벤트 갱신 |
화면 상태 작성에 집중하고 복잡한 규칙만 UseCase로 분리 |
AirClearUiState |
청정·공기질·이동 상태 보관 | 불변 상태로 만들고 ViewModel만 새 상태를 작성 |
AirCleanerRepository |
Bundle 생성과 Binder 호출, 응답 변환 |
Binder 생명주기와 데이터 변환을 Platform Adapter로 격리 |
DeviceControlResolver·sendModuleCommand() |
DeviceAgent 연결과 명령 전달 | 공개 Manager·Task 계약 뒤로 감추고 앱에서 직접 다루지 않음 |
현재 구현은 1~4초 주기의 상태 조회가 일부 남아 있다. 가능한 영역부터 callback 또는 Flow 기반
상태 관찰로 전환하되, 실제 DeviceAgent가 제공하지 않는 이벤트를 앱이 임의로 만들어서는 안 된다.
전체 구조
그림 3. 앱이 요청을 만들고 제품 실행 계층이 실제 동작을 관리하는 흐름
원본 SVG 열기
| 계층 | 하는 일 | 배포 형태 | 하지 않는 일 |
|---|---|---|---|
SKIX Application Framework |
앱의 상태·입력·오류·동시성·테스트 규칙을 통일 | AAR, 템플릿, 정적 검사, 테스트 도구 | 전역 기기 상태와 제품 정책을 소유하지 않음 |
SKIX Product Runtime Framework |
기기 상태·정책·자원·Task·제품 UI 세션 관리 | Manager API, Binder/AIDL, 플랫폼 서명 Service | 화면별 렌더링과 Feature 업무 규칙을 소유하지 않음 |
Android / Jetpack / AOSP |
UI, 생명주기, 비동기 처리, 저장, IPC 제공 | Android API와 Jetpack 라이브러리 | SKIX 제품 의미와 물리 완료 판단을 정의하지 않음 |
앱은 Android/AOSP와 Jetpack의 표준 컴포넌트를 그대로 사용한다. ViewModel · StateFlow · Coroutine · Lifecycle
등을 SKIX 이름으로 다시 만들지 않는다. SKIX는 이 기술을 사용하는 방법과 계층 간 책임만 통일한다.
구성요소별 역할
화면 상태 계약
| 이름 | 쉬운 설명 | 예시 | 지켜야 할 기준 |
|---|---|---|---|
UiAction |
사용자가 화면에 전달한 입력이다. | 버튼 클릭, 텍스트 입력, 다시 시도 선택 | 화면이 상태를 직접 바꾸거나 Binder를 호출하지 않고 ViewModel 함수로 전달 |
UiState |
화면을 다시 그리는 데 필요한 현재 정보이며, 현재 화면 전체를 표현하는 지속 상태다. | 청정 상태, 위치, 풍량, 오류 표시 | 화면 회전·재구독 뒤에도 필요한 값만 포함 |
DomainEvent |
Task·센서·Network 등 제품 실행 계층에서 들어온 사실이다. | Task 진행, 도킹 완료, 연결 변경 | Repository Flow로 수신하고 화면 입력과 같은 타입으로 섞지 않음 |
UiEffect |
유실돼도 업무 결과가 달라지지 않는 짧은 화면 반응이다. | 포커스 이동, 스크롤, 강조 애니메이션 | 실패 안내·Dialog·권한 결정·업무 결과 Navigation을 넣지 않음 |
기본 흐름은 UiAction → ViewModel → UiState → View다. 외부 callback은
DomainEvent → Repository Flow → ViewModel → UiState로 흐른다. ViewModel에서 시작된 실패 안내나
Navigation 조건은 UiState에 남겨 화면 재생성 뒤에도 복구할 수 있게 한다.
앱 모듈
| 모듈 | 역할 | 언제 사용하는가 |
|---|---|---|
| View | UiState를 화면에 표현하고 사용자 입력을 UiAction으로 전달 |
모든 화면 |
| ViewModel | UiAction과 Repository 상태를 결합해 화면에 공개할 UiState를 관리 |
모든 화면의 상태 경계 |
| Reducer | 복잡한 상태 전이를 순수 함수로 계산 | 다단계 입력과 장기 진행 화면에만 선택 |
| UseCase | 하나의 사용자·제품 행동을 이름 있는 실행 단위로 묶음 | 여러 Repository나 업무 규칙을 한 행동으로 조합할 때 선택 |
| StateMachine | 앱 내부 Feature 상태와 허용 전이를 관리 | Vital Signs 화면처럼 사용자 확인·입력·표시 상태가 복잡할 때. Task 생명주기는 복제하지 않음 |
| Coordinator | 여러 UseCase·Repository의 앱 세션과 사용자 확인 흐름을 조정 | 여러 화면·확인·권한 단계가 이어질 때. 기기 동작 순서와 재시도는 TaskManager에 위임 |
| Repository | 데이터와 기기 기능에 접근하는 안정된 경계 | 화면이나 Domain이 기기·DB·Network에 접근할 때 |
| DataSource | DB·Network·Cache·Device 중 실제 원본 하나를 처리 | Repository 구현 내부 |
| Platform Client Adapter | Binder 연결과 callback을 앱 계약으로 변환 | Binder 재연결, listener 해제, callback-to-Flow 변환이 필요할 때 |
제품 실행 모듈
| 모듈 | 역할 | 책임 경계 |
|---|---|---|
Skix*Manager |
제품 실행 API의 공개 호출 창구 | 요청·상태 관찰·취소·결과 callback 제공. 실제 상태와 정책은 소유하지 않음 |
| Binder Service | 호출자 권한을 확인하고 요청을 내부 책임 모듈로 전달하며 연결 생명주기를 관리 | 상태·정책·Task의 최종 판단은 각각의 내부 책임 모듈에 맡기고 Binder 진입점에 집중시키지 않음 |
| 상태·정책·자원 계층 | 현재 상태에서 요청이 가능한지와 자원 충돌을 판단 | 실행 허용·거부 이유를 정하지만 기기 기능은 직접 수행하지 않음 |
| TaskManager | Task와 Workflow의 접수·대기·순서·진행·취소·timeout·최종 상태 관리 | 물리 기능은 Domain Executor에 위임 |
| Domain Executor | 이동·청정·화면·TTS·보안·측정 기능 실행 | Task 생명주기와 앱 화면 상태는 소유하지 않음 |
| Evidence Adapter | 실제 시작·진행·완료·실패 근거를 TaskState와 제품 이벤트로 변환 |
API 접수 성공을 물리 완료로 간주하지 않음 |
예시 · 청정 시작부터 화면 갱신까지
1. View CleaningStartClicked를 UiAction으로 전달
2. ViewModel StartCleaningUseCase 실행
3. StartCleaningUseCase 공간·모드 규칙을 확인하고 CleaningRepository 호출
4. CleaningRepository Platform Adapter를 통해 SkixTaskManager에 요청
5. Product Runtime 정책·상태·자원을 확인하고 Domain Executor 실행
6. Evidence Adapter 청정 시작·진행·종료 근거를 TaskState로 변환
7. CleaningRepository TaskState를 Flow로 앱에 반환
8. ViewModel 새 CleaningUiState를 만들고 View가 화면 갱신
9. 실패하면 failureMessage와 nextAction을 CleaningUiState에 기록
UseCase와 StateMachine은 필수 통과 지점이 아니다. 단순한 청정 시작 화면은 ViewModel이
CleaningRepository를 직접 호출할 수 있다.
MVVM과 단방향 상태 흐름
MVVM을 버리는 것이 아니다
MVVM은 View와 ViewModel을 나누지만, ViewModel이 어디까지 맡고 상태와 일회성 동작을 어떻게 구분할지는 충분히 정하지 않는다. SKIX는 MVVM + UDF를 기본으로 삼아 이 빈 부분을 채운다.
- MVVM: 화면과 화면 상태 로직을 나눈다.
- UDF: 입력은 안쪽으로 흐르고, 새 상태는 화면 쪽으로 돌아오는 방향을 일정하게 유지한다.
- 불변 상태: 기존 상태를 여기저기 수정하지 않고 새 상태를 만들어 교체한다.
그림 4. MVVM·단방향 상태 흐름과 선택형 Reducer
원본 SVG 열기
- 이 그림이 답하는 질문: 사용자 입력과 UiState는 앱 내부에서 어떤 한 방향으로 흘러야 하는가?
- 읽는 순서: 화면, ViewModel, Repository, SkixUiManager 순서로 요청을 읽고 아래 상태 반환선을 따라 화면으로 돌아온다.
- 현재 검증 범위: 권장 앱 구조와 선택형 Reducer 기준이다. 모든 기존 Activity·Fragment 전환 완료를 뜻하지 않는다.
interface UiState
interface UiAction
interface UiEffect
enum class CleaningNextAction {
RETRY,
CHECK_DEVICE_STATE,
DISMISS,
}
data class CleaningUiState(
val running: Boolean = false,
val roomName: String = "",
val progress: Int = 0,
val failureMessage: String? = null,
val nextAction: CleaningNextAction? = null,
) : UiState
sealed interface CleaningUiAction : UiAction {
data object StopClicked : CleaningUiAction
}
sealed interface CleaningUiEffect : UiEffect {
data object FocusRetryButton : CleaningUiEffect
data object ScrollToCurrentStep : CleaningUiEffect
}
UiState, UiAction, 제한적인 UiEffect는 의미를 통일하는 계약이다. Android ViewModel, StateFlow,
Coroutine과 Lifecycle은 그대로 사용하며, 얇은 선택형 BaseViewModel은 반복 코드만 줄인다.
복잡도에 따라 확장
모든 화면에 같은 수의 계층을 강제하지 않는다.
그림 5. Feature 단계별 구성과 다중 Event 수렴
원본 SVG 열기
| 단계 | 호출 구조 | 적용 조건 | 예시 |
|---|---|---|---|
| 기본 Feature | ViewModel → Repository |
단일 조회·설정·상태 표시 | 설정값, 정보 표시 |
| 표준 Feature | ViewModel → UseCase → Repository |
여러 데이터나 규칙을 한 행동으로 묶음 | 공간 선택과 실행 가능 여부 확인 |
| 복잡한 Feature | ViewModel → Reducer/UseCase → StateMachine · Coordinator → Repository |
앱 화면·확인·권한의 상태 전이가 복잡함 | Vital Signs, Security, Welcome UI 세션 |
| 장기 실행 진행 | ViewModel → Reducer → Task Repository |
단계·진행률·취소·실패 이유 표시 | Task 진행, Workflow 상태 |
Sensor Event · Voice Event · Task Event · Network Event는 사용자 화면 입력이 아니라 제품 실행
계층에서 들어오는 DomainEvent다. 이 입력은 Repository Flow와 Product Runtime으로 모이고
ViewModel은 화면에 필요한 값만 UiState로 바꾼다.
UI는 시스템 상태의 Consumer이며 시스템 상태의 주인이 아니다. 화면이 없어도 기기 작업은
유지될 수 있어야 한다.
앱 상태, 제품 UI 세션을 나타내는 UiSessionState, 기기 작업 상태인 TaskState는 서로 연결하지만
같은 상태를 중복 소유하지 않는다.
Application Framework 구성
| 영역 | 제공 요소 | 쉬운 역할 설명 |
|---|---|---|
| Presentation | UiState · UiAction, 제한적 UiEffect, UDF helper |
화면 상태와 사용자 입력 처리 규칙 |
| Domain | UseCase · StateMachine · Coordinator |
복잡한 업무 규칙과 상태 전이 |
| Data | Repository 규칙 · DataSource 규칙 · Mapper · Cache | 구현 기술을 상위 계층에서 감춤 |
| Common | AppResult · AppError, 구조화 로그 · Coroutine 정책 · 테스트 도구 |
오류·비동기·로그·검증 방식 통일 |
| Platform Client Adapter | Manager callback 변환, Binder 재연결, 상태 투영 | 앱에서 Binder 생명주기 격리 |
| UI 자산 | UI Resource Kit AAR, UI Foundation AAR | 공통 색상·글꼴·아이콘·화면 구성요소 제공 |
| Testing | Fake Repository, 상태·Effect 검증, 계약 테스트 | 로컬 테스트와 실제 Service 통합 테스트 분리 |
권장 모듈
skix-app-framework/
├─ architecture-contract UiState · UiAction · UiEffect · AppResult · AppError
├─ presentation 선택형 UDF helper · 상태 기반 결과 처리 · UI 테스트 지원
├─ domain StateMachine · Coordinator 계약과 공통 모델
├─ common 로그 · dispatcher 주입 · 민감정보 제거 규칙
├─ platform-client Skix Manager adapter · Binder 재연결 · 상태 변환
├─ ui-resources 색상 · 글꼴 · 간격 · 아이콘
├─ ui-foundation 공통 View·Compose 구성요소
└─ testing Fake · 상태 전이 · 계약 테스트 도구
Data 구현은 Feature별로 둔다. 필요하지 않은 앱이 Binder나 Room 의존성을 함께 받지 않도록 AAR을 분리한다. HAL은 앱에서 직접 호출하지 않는다.
Repository 계약
Repository 인터페이스는 상위 계층이 필요한 기능과 상태를 표현하고, Repository 구현은 Binder·Network·Room·Cache를 조합한다.
interface RobotRepository {
val state: Flow<RobotState>
suspend fun start(): AppResult<Unit>
suspend fun stop(): AppResult<Unit>
}
구현은 BinderRobotRepository, NetworkRobotRepository, FakeRobotRepository처럼 교체할 수 있다.
ViewModel은 Binder를 직접 호출하지 않는다.
오류·동시성·로그·테스트
| 항목 | 표준 |
|---|---|
| 결과 | AppResult.Success<T> 또는 AppResult.Failure(AppError) |
| 오류 | Network·Timeout·Permission·Device·InvalidState·Validation·Unknown과 안정된 오류 코드 |
| Coroutine | scope 소유권, dispatcher 주입, 취소 전파, timeout과 예외 경계를 문서화 |
| 로그 | traceId·sessionId·taskId 연결, 개인정보·원문·내부 예외 제거 정책 적용 |
| 단위 테스트 | ViewModel의 State/Action 처리, 제한적 Effect, Reducer 전이, UseCase와 StateMachine 검증 |
| 계약 테스트 | Fake Repository와 실제 Manager Adapter가 같은 결과·오류 계약을 만족하는지 확인 |
| 통합 테스트 | Binder death·재연결·프로세스 복원·Task 완료 근거·UI 세션 종료 확인 |
AppError는 호출자가 대응할 수 있는 코드만 공개한다. 원본 Binder 예외와 기기 내부 진단 정보는
제한된 로그에 남기고 UI 문구를 오류 객체에 직접 넣지 않는다.
현재 코드에서의 전환
전면 재작성하지 않고 Feature 단위로 전환한다.
- 변경 가능한
UiState를 불변 상태와 새 인스턴스 갱신으로 바꾼다. - Activity와 Fragment에서 Repository·Manager 직접 호출 금지를 적용한다.
- 사용자 입력은
UiAction, Task·Sensor·Network 입력은 Repository의DomainEvent흐름으로 분리한다. - Binder callback·재연결·listener 해제를 Repository와 Platform Adapter로 이동한다.
- 실패 안내·Dialog·권한·Navigation 조건은
UiState로 표현하고, 손실 허용 UI 반응만UiEffect로 둔다. - 복잡한 화면만 선택형 Reducer·StateMachine·Coordinator를 도입하되 기기 Task 순서와 복구는 TaskManager에 맡긴다.
- 앱 간 화면 소유권이 필요한 기능부터
SkixUiManager와UiSession계약에 연결한다.
Feature 전환 완료 기준
- UI는 상태를 표시하고 사용자
UiAction만 전달한다. - ViewModel은 화면 상태의 공개 권위 원본이다. 선택형 Reducer는 순수 함수로 다음 상태 계산만 돕는다.
- Activity와 Fragment에 Repository·Manager 직접 호출이 없다.
- 필수 안내와 업무 결과가 손실 가능한 Effect에만 존재하지 않는다.
- ViewModel과 Domain은 Binder 객체와 Android Service 구현을 알지 않는다.
- 상태 전이, 오류, 취소, 프로세스 복원과 재연결을 테스트한다.