Android 세부 아키텍처
작성일: 2025-08
문서 버전: v 1.0
시리즈: 2/4 — Android 멀티모듈 · MVI 아키텍처
1. 아키텍처 원칙
본 프로젝트는 다음 세 가지 원칙을 기반으로 Android 아키텍처를 설계한다.
Clean Architecture — Presentation / Domain / Data 계층을 명확히 분리하여 각 계층이 단방향으로만 의존하도록 한다. Domain 계층은 Android 프레임워크에 의존하지 않는 순수 Kotlin 모듈로 유지한다.
멀티모듈 — 기능 단위로 모듈을 분리하여 빌드 캐시 효율을 높이고, 모듈 간 의존성을 명시적으로 관리한다. 각 feature 모듈은 독립적으로 개발·테스트 가능하도록 설계한다.
MVI (Model-View-Intent) — 단방향 데이터 흐름으로 UI 상태를 예측 가능하게 관리한다. 모든 상태 변경은 Intent → ViewModel → State 경로로만 이루어진다.
2. 전체 모듈 구조
root/
├── app/ # 앱 진입점
│
├── feature/
│ ├── feature-home/ # 홈 · 카메라 프리뷰
│ ├── feature-analysis/ # 분석 결과 화면
│ ├── feature-history/ # 건강 기록 · 통계
│ └── feature-settings/ # 설정
│
├── domain/
│ └── domain/ # 비즈니스 로직 (순수 Kotlin)
│
├── data/
│ ├── data-local/ # Room DB · DataStore
│ ├── data-sensor/ # CameraX · IMU 센서
│ └── data-remote/ # Retrofit (선택적)
│
├── ml/
│ └── ml-core/ # 모든 AI/ML 추론 로직
│
└── core/
├── core-ui/ # 공통 Compose 컴포넌트
├── core-network/ # OkHttp · Retrofit 설정
├── core-di/ # Hilt 모듈 정의
├── core-testing/ # 테스트 유틸리티
└── core-utils/ # 확장 함수 · 유틸
3. 모듈 의존성 규칙
:feature-*
│ 의존 가능: :domain, :core-ui, :core-utils, :core-di
│ 의존 불가: 다른 :feature-*, :data-*, :ml-core 직접 접근
▼
:domain
│ 의존 가능: 없음 (순수 Kotlin)
│ 의존 불가: 모든 Android 프레임워크, :data-*, :ml-core
▼
:data-* / :ml-core
│ 의존 가능: :domain (인터페이스 구현), :core-*
│ 의존 불가: :feature-*, 서로 간 직접 의존
▼
:core-*
의존 가능: 없음 (최하단 공통 모듈)
규칙 요약: 의존성은 항상 위에서 아래 방향으로만 흐른다. feature 모듈끼리는 직접 참조하지 않고, Navigation 또는 공유 이벤트를 통해 통신한다.
4. 모듈별 상세 설명
4.1 :app
앱의 진입점. 최소한의 코드만 포함한다.
역할
Application클래스 및 Hilt 루트 설정- Navigation Graph 정의 (모든 feature 화면 연결)
- Splash Screen 처리
의존 모듈: 모든 :feature-*, :core-di
@HiltAndroidApp
class CatHealthApp : Application()
4.2 :feature-home
카메라 실시간 프리뷰 및 분석 트리거 화면.
역할
- CameraX 프리뷰 Composable 제공
- 고양이 감지 시 캡처 버튼 활성화 및 자동 캡처
- 실시간 오버레이 (관절 랜드마크 시각화)
- 분석 시작 Intent 발행 →
:feature-analysis로 네비게이션
주요 파일 구조
feature-home/
├── ui/
│ ├── HomeScreen.kt # Composable 루트 화면
│ ├── CameraPreviewComposable.kt
│ └── PoseOverlayCanvas.kt # 관절 오버레이 Canvas
├── HomeViewModel.kt # MVI ViewModel
└── HomeContract.kt # State · Intent · Effect 정의
MVI Contract
// HomeContract.kt
data class HomeState(
val isCameraReady: Boolean = false,
val isAnalyzing: Boolean = false,
val detectedLandmarks: List<PoseLandmark> = emptyList(),
val error: UiError? = null
)
sealed class HomeIntent {
data object InitCamera : HomeIntent()
data object CaptureAndAnalyze : HomeIntent()
data object ToggleAutoCapture : HomeIntent()
}
sealed class HomeEffect {
data class NavigateToAnalysis(val imageUri: Uri) : HomeEffect()
data class ShowPermissionDialog(val permission: String) : HomeEffect()
}
4.3 :feature-analysis
분석 결과를 표시하는 화면.
역할
- 종(Breed) 분류 결과 및 신뢰도 표시
- 건강 점수 (0~100) 게이지 시각화
- 감지된 행동 패턴 목록 및 설명
- 결과 저장 / 공유 액션 처리
주요 파일 구조
feature-analysis/
├── ui/
│ ├── AnalysisScreen.kt
│ ├── BreedResultCard.kt
│ ├── HealthScoreGauge.kt # 커스텀 Canvas 게이지
│ └── BehaviorInsightList.kt
├── AnalysisViewModel.kt
└── AnalysisContract.kt
MVI Contract
data class AnalysisState(
val isLoading: Boolean = false,
val breed: BreedResult? = null,
val healthScore: Float? = null,
val behaviorInsights: List<BehaviorInsight> = emptyList(),
val error: UiError? = null
)
sealed class AnalysisIntent {
data class StartAnalysis(val imageUri: Uri) : AnalysisIntent()
data object SaveResult : AnalysisIntent()
data object ShareResult : AnalysisIntent()
data object RetryAnalysis : AnalysisIntent()
}
sealed class AnalysisEffect {
data class ShowError(val message: String) : AnalysisEffect()
data object NavigateToHistory : AnalysisEffect()
data class ShowShareSheet(val uri: Uri) : AnalysisEffect()
}
4.4 :feature-history
시계열 건강 기록 및 통계 화면.
역할
- 날짜별 건강 점수 추세 그래프 (Compose Charts)
- 분석 이력 목록 (날짜, 썸네일, 점수)
- 특정 기록 상세 조회 및 삭제
- 다두 보호자를 위한 고양이 프로필 전환
주요 파일 구조
feature-history/
├── ui/
│ ├── HistoryScreen.kt
│ ├── HealthTrendChart.kt # 주간/월간 그래프
│ ├── AnalysisHistoryItem.kt
│ └── CatProfileSelector.kt # 다두 프로필 탭
├── HistoryViewModel.kt
└── HistoryContract.kt
4.5 :feature-settings
앱 설정 화면.
역할
- 알림 임계값 설정 (건강 점수 기준값)
- 분석 주기 설정 (자동 캡처 인터벌)
- 고양이 프로필 추가 / 편집 / 삭제
- 모델 업데이트 수동 트리거
- 데이터 초기화 / 내보내기
4.6 :domain
비즈니스 로직의 핵심. Android 프레임워크 의존성 없음.
역할
- UseCase 정의 (비즈니스 규칙 캡슐화)
- Repository 인터페이스 정의
- 도메인 모델 정의 (UI 모델, DB 엔티티와 분리)
패키지 구조
domain/
├── usecase/
│ ├── AnalyzeCatUseCase.kt # 종·건강 추론 오케스트레이션
│ ├── TrackMotionUseCase.kt # 움직임 패턴 추적
│ ├── HealthInsightUseCase.kt # 건강 지표 종합 분석
│ ├── SaveAnalysisUseCase.kt # 결과 저장
│ └── GetHealthHistoryUseCase.kt
├── repository/
│ ├── CatRepository.kt # interface
│ ├── HealthRepository.kt # interface
│ └── ModelRepository.kt # interface (모델 업데이트)
└── model/
├── BreedResult.kt
├── HealthScore.kt
├── BehaviorInsight.kt
├── PoseLandmark.kt
└── AnalysisRecord.kt
UseCase 예시
// AnalyzeCatUseCase.kt
class AnalyzeCatUseCase @Inject constructor(
private val breedClassifier: BreedClassifier,
private val poseEstimator: CatPoseEstimator,
private val motionAnalyzer: MotionAnalyzer,
private val healthScorer: HealthScorer
) {
suspend operator fun invoke(imageUri: Uri): Result<AnalysisResult> =
runCatching {
val bitmap = imageUri.toBitmap()
// 병렬 추론
val (breed, landmarks, motionVector) = coroutineScope {
val breedDeferred = async { breedClassifier.classify(bitmap) }
val poseDeferred = async { poseEstimator.estimate(bitmap) }
val motionDeferred = async { motionAnalyzer.analyze(bitmap) }
Triple(
breedDeferred.await(),
poseDeferred.await(),
motionDeferred.await()
)
}
val healthScore = healthScorer.score(
HealthFeatureVector(breed, landmarks, motionVector)
)
AnalysisResult(breed, landmarks, healthScore)
}
}
4.7 :data-local
로컬 영속 저장소 구현.
역할
- Room DB: 분석 이력, 고양이 프로필, 건강 점수 기록
- DataStore (Proto): 앱 설정, 알림 임계값, 선택된 프로필
Room 엔티티 구조
@Entity(tableName = "analysis_records")
data class AnalysisRecordEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val catProfileId: Long,
val timestamp: Long,
val breedName: String,
val breedConfidence: Float,
val healthScore: Float,
val behaviorTags: String, // JSON 직렬화
val imageUri: String?
)
@Entity(tableName = "cat_profiles")
data class CatProfileEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val name: String,
val breed: String?,
val birthDate: Long?,
val photoUri: String?
)
4.8 :data-sensor
카메라 및 센서 데이터 수집.
역할
- CameraX
ImageAnalysis파이프라인 설정 ImageProxy→Bitmap/Mat(OpenCV) 변환- IMU 센서 데이터 수집 (추후 활동량 보조 지표)
- 프레임 버퍼 관리 (최신 프레임 유지 전략)
CameraX 파이프라인 핵심 설정
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(1280, 720))
.setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST) // 최신 프레임만 유지
.setOutputImageFormat(OUTPUT_IMAGE_FORMAT_RGBA_8888)
.build()
.also { analysis ->
analysis.setAnalyzer(cameraExecutor) { imageProxy ->
val bitmap = imageProxy.toBitmap()
frameChannel.trySend(bitmap) // 코루틴 Channel로 ML 파이프라인 전달
imageProxy.close()
}
}
4.9 :data-remote
서버 통신 구현 (선택적 모듈).
역할
- Spring Boot API 통신 (모델 버전 확인 · 다운로드)
- 익명화된 통계 업로드
- Retrofit + OkHttp 구현체
활성화 조건: 백엔드 미사용 환경에서는 빌드에서 제외 가능하도록 설계. :domain 의 ModelRepository 인터페이스를 stub 구현체로 대체하여 오프라인 전용 빌드 지원.
4.10 :ml-core
모든 AI/ML 추론 로직을 단일 모듈에 집중.
세부 내용은
03_vision_ai_model.md참조.
역할
- TFLite 종 분류 모델 래퍼 (
BreedClassifier) - MediaPipe 자세 추정 래퍼 (
CatPoseEstimator) - OpenCV C++ JNI 브릿지 (
MotionAnalyzerJni) - ONNX Runtime 건강 점수 모델 (
HealthScorer) - 모델 파일 Asset 관리 및 버전 체크
4.11 :core-ui
공통 Compose UI 컴포넌트.
역할
- Design System 정의 (Color, Typography, Shape)
- 공통 컴포넌트:
LoadingOverlay,ErrorSnackbar,HealthScoreBadge - 커스텀 Canvas 컴포넌트:
PoseOverlay,TrendChart - Compose Preview 용 테마 설정
4.12 :core-di
Hilt 모듈 중앙 정의.
역할
@Module및@Provides정의 (Repository, UseCase 바인딩)- Coroutine Dispatcher 제공 (
@IoDispatcher,@DefaultDispatcher) - ML 모델 인스턴스 싱글턴 관리 (TFLite, ONNX 세션 재사용)
@Module
@InstallIn(SingletonComponent::class)
object MlModule {
@Provides
@Singleton
fun provideBreedClassifier(@ApplicationContext ctx: Context): BreedClassifier =
BreedClassifier(ctx)
@Provides
@Singleton
fun provideHealthScorer(@ApplicationContext ctx: Context): HealthScorer =
HealthScorer(ctx)
}
4.13 :core-testing
테스트 공통 유틸리티.
역할
FakeRepository구현체 제공 (단위 테스트용)- Coroutine 테스트 헬퍼 (
TestDispatcherRule) - 테스트용 더미 도메인 모델 팩토리
5. MVI 패턴 상세
5.1 구조
User Action
│
▼
[Intent] ← 사용자 의도 표현 (sealed class)
│
▼
[ViewModel] ← Intent 처리, UseCase 호출
│
├──► [State] ← UI 렌더링용 불변 상태 (data class)
│ │
│ ▼
│ [View] ← Composable이 State 소비
│
└──► [Effect] ← 1회성 이벤트 (네비게이션, 다이얼로그)
│
▼
[View] ← LaunchedEffect로 소비
5.2 기본 ViewModel 템플릿
모든 feature ViewModel이 상속하는 공통 베이스 클래스.
// :core-ui 또는 :domain에 위치
abstract class BaseViewModel<S : UiState, I : UiIntent, E : UiEffect> : ViewModel() {
private val _state: MutableStateFlow<S> by lazy { MutableStateFlow(initialState()) }
val state: StateFlow<S> = _state.asStateFlow()
private val _effect = Channel<E>(Channel.BUFFERED)
val effect: Flow<E> = _effect.receiveAsFlow()
abstract fun initialState(): S
abstract fun processIntent(intent: I)
protected fun updateState(reducer: S.() -> S) {
_state.update { it.reducer() }
}
protected suspend fun sendEffect(effect: E) {
_effect.send(effect)
}
}
5.3 Composable에서의 소비 패턴
@Composable
fun AnalysisScreen(
viewModel: AnalysisViewModel = hiltViewModel()
) {
val state by viewModel.state.collectAsStateWithLifecycle()
// 1회성 Effect 처리
val navController = LocalNavController.current
LaunchedEffect(Unit) {
viewModel.effect.collect { effect ->
when (effect) {
is AnalysisEffect.NavigateToHistory -> navController.navigate(Route.History)
is AnalysisEffect.ShowError -> { /* Snackbar 표시 */ }
}
}
}
// State 기반 UI 렌더링
AnalysisContent(
state = state,
onIntent = viewModel::processIntent
)
}
6. 의존성 관리 — Version Catalog
멀티모듈 환경에서 라이브러리 버전을 libs.versions.toml 한 곳에서 관리한다.
# gradle/libs.versions.toml
[versions]
kotlin = "2.0.0"
compose-bom = "2024.06.00"
hilt = "2.51"
room = "2.6.1"
camerax = "1.3.4"
mediapipe = "0.10.14"
tflite = "2.14.0"
onnxruntime = "1.18.0"
coroutines = "1.8.1"
navigation = "2.7.7"
[libraries]
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
camerax-core = { group = "androidx.camera", name = "camera-core", version.ref = "camerax" }
camerax-camera2 = { group = "androidx.camera", name = "camera-camera2",version.ref = "camerax" }
camerax-lifecycle = { group = "androidx.camera", name = "camera-lifecycle",version.ref = "camerax" }
mediapipe-tasks = { group = "com.google.mediapipe", name = "tasks-vision", version.ref = "mediapipe" }
tflite-support = { group = "org.tensorflow", name = "tensorflow-lite-support", version.ref = "tflite" }
tflite-gpu = { group = "org.tensorflow", name = "tensorflow-lite-gpu", version.ref = "tflite" }
onnxruntime = { group = "com.microsoft.onnxruntime", name = "onnxruntime-android", version.ref = "onnxruntime" }
[plugins]
android-application = { id = "com.android.application", version = "8.5.0" }
android-library = { id = "com.android.library", version = "8.5.0" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ksp = { id = "com.google.devtools.ksp", version = "2.0.0-1.0.21" }
7. 빌드 설정 공통화
각 모듈의 반복적인 build.gradle.kts 설정을 Convention Plugin으로 공통화한다.
build-logic/
└── convention/
├── AndroidLibraryConventionPlugin.kt # :data-*, :core-*용
├── AndroidFeatureConventionPlugin.kt # :feature-*용 (Compose, Hilt 포함)
├── AndroidHiltConventionPlugin.kt # Hilt 설정
└── MlCoreConventionPlugin.kt # :ml-core NDK/CMake 설정
feature 모듈 build. gradle. kts 예시 (플러그인 적용 후)
// feature-analysis/build.gradle.kts
plugins {
alias(libs.plugins.cathealth.android.feature) // Convention Plugin 적용
alias(libs.plugins.cathealth.android.hilt)
}
android {
namespace = "com.cathealth.feature.analysis"
}
dependencies {
implementation(project(":domain"))
implementation(project(":core-ui"))
}
8. 테스트 전략
| 계층 | 테스트 종류 | 도구 |
|---|---|---|
| Domain (UseCase) | 단위 테스트 | JUnit 5, MockK, Turbine |
| ViewModel | 단위 테스트 | JUnit 5, MockK, Turbine, TestDispatcher |
| Repository | 통합 테스트 | Robolectric, Room In-Memory |
| ML 모델 | 정확도 검증 | JVM 테스트, TFLite Test Util |
| UI (Composable) | 스크린샷 테스트 | Paparazzi |
| E 2 E | 시나리오 테스트 | Espresso + UIAutomator |
9. 관련 문서
| 문서 | 경로 |
|---|---|
| 전체 기획 개요 | 01_overview.md |
| 비전 및 AI 모델 | 03_vision_ai_model.md |
| 백엔드 세부 설명 | 04_backend.md |
본 문서는 개발 진행에 따라 지속적으로 업데이트됩니다.
C
Contents
