2026/05/08

오늘의 이야기


🔍 프로젝트 진단 | Health501 아키텍처 & 코드 품질 개선 로드맵


샘플이미지


 


📊 개요 (Executive Summary)



  • 작업 일자: 2025-12-15

  • 작업 유형: 프로젝트 전반 분석 및 개선 방안 도출

  • 목적: Health501 프로젝트의 현재 상태를 진단하고, 유지보수성·확장성·안정성 향상을 위한 구체적 개선 로드맵 수립

  • 핵심 발견: 아키텍처 문서(ARCHITECTURE.md)와 실제 코드 구조 간 불일치, 테스트 커버리지 부족, 보안 취약점 존재



🎯 핵심 목표: 문서화된 원칙을 실제 코드에 반영하여 장기적으로 확장 가능한 프로젝트 기반 마련




🏗️ 1. 아키텍처 레이어 분리 (최우선 과제)



현재 상태 분석


문제점:



  • ARCHITECTURE.md에는 명확한 3계층 구조(UI → Domain → Data)가 정의되어 있으나, 실제 코드에는 domain 레이어가 존재하지 않음

  • ViewModel이 직접 Manager 클래스를 호출하여 비즈니스 로직이 UI 레이어에 혼재

  • 데이터 레이어와 UI 레이어가 강결합되어 테스트 및 변경이 어려움


현재 구조:


app/src/main/java/com/billcoreatech/health501/
├── viewmodels/
│ ├── HealthConnectViewModel.kt (← HealthConnectManager 직접 호출)
│ └── CoupangViewModel.kt
├── data/
│ ├── HealthConnectManager.kt
│ └── HealthConnectUtil.kt
└── (domain 레이어 부재)

개선 방안


목표 구조:


app/src/main/java/com/billcoreatech/health501/
├── presentation/ (기존 presentaion 오타도 수정)
│ ├── viewmodels/
│ └── screens/
├── domain/ ← 새로 생성
│ ├── model/
│ │ ├── Result.kt (sealed interface)
│ │ ├── StepData.kt
│ │ └── ExerciseSessionData.kt
│ └── usecase/
│ ├── GetTodayStepsUseCase.kt
│ ├── GetExerciseSessionsUseCase.kt
│ ├── SyncWearDataUseCase.kt
│ └── GetWeeklyStatsUseCase.kt
└── data/
├── repository/ ← 새로 생성
│ ├── HealthDataRepository.kt
│ ├── HealthDataRepositoryImpl.kt
│ └── WearSyncRepository.kt
└── datasource/
├── HealthConnectDataSource.kt (기존 Manager 래핑)
└── WearDataSource.kt

구현 예시 (UseCase 패턴)


// domain/usecase/GetTodayStepsUseCase.kt
class GetTodayStepsUseCase @Inject constructor(
private val healthRepository: HealthDataRepository
) {
suspend operator fun invoke(): Result<Long> = withContext(Dispatchers.IO) {
try {
val steps = healthRepository.getTodaySteps()
Result.Success(steps)
} catch (e: Exception) {
Result.Error(e.message ?: "Unknown error")
}
}
}

// domain/model/Result.kt
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Error(val message: String) : Result<Nothing>
object Loading : Result<Nothing>
}

// data/repository/HealthDataRepositoryImpl.kt
class HealthDataRepositoryImpl @Inject constructor(
private val healthConnectDataSource: HealthConnectDataSource
) : HealthDataRepository {
override suspend fun getTodaySteps(): Long {
return healthConnectDataSource.readTodaySteps()
}
}

// ViewModel에서 사용
@HiltViewModel
class HealthConnectViewModel @Inject constructor(
private val getTodayStepsUseCase: GetTodayStepsUseCase
) : ViewModel() {

val stepsState: StateFlow<Result<Long>> = flow {
emit(Result.Loading)
emit(getTodayStepsUseCase())
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Result.Loading)
}

우선순위: 긴급 (Phase 1)


예상 소요 시간: 1-2주


기대 효과:



  • 비즈니스 로직과 UI 로직의 명확한 분리

  • 테스트 용이성 대폭 향상 (UseCase 단위 테스트 가능)

  • 코드 재사용성 증가 (Wear 앱과 공유 가능)



🧪 2. 테스트 커버리지 강화



현재 상태


문제점:



  • 전체 테스트 파일: 3개 (AiAutoSelectorTest.kt, AiWeightsLoadTest.kt, ExampleUnitTest.kt)

  • 핵심 비즈니스 로직(ViewModel, Manager, Sync)에 대한 테스트 전무

  • TESTING.md에 domain ≥80%, data ≥70% 목표가 명시되어 있으나 실제 커버리지는 추정 10% 미만


개선 계획





































레이어 테스트 대상 테스트 유형 목표 커버리지
Domain UseCase 클래스들 Unit Test (MockK) 80%+
Data Repository, DataSource Unit Test + Integration Test 70%+
ViewModel 상태 관리, 비즈니스 흐름 Unit Test (Turbine for Flow) 60%+
UI 핵심 화면 플로우 Compose UI Test 50%+

필수 테스트 파일 목록


app/src/test/java/com/billcoreatech/health501/
├── domain/
│ └── usecase/
│ ├── GetTodayStepsUseCaseTest.kt
│ ├── GetExerciseSessionsUseCaseTest.kt
│ └── SyncWearDataUseCaseTest.kt
├── data/
│ ├── repository/
│ │ ├── HealthDataRepositoryTest.kt
│ │ └── WearSyncRepositoryTest.kt
│ └── datasource/
│ └── HealthConnectDataSourceTest.kt
├── viewmodels/
│ ├── HealthConnectViewModelTest.kt
│ └── CoupangViewModelTest.kt
└── testutil/
├── FakeHealthConnectClient.kt
├── FakeWearDataSyncManager.kt
└── TestDispatchers.kt

테스트 도구 추가 (build.gradle.kts)


dependencies {
// 기존 의존성...

// 테스트 라이브러리 추가
testImplementation("io.mockk:mockk:1.13.9")
testImplementation("app.cash.turbine:turbine:1.0.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
testImplementation("androidx.arch.core:core-testing:2.2.0")

// Compose UI 테스트
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}

우선순위: 긴급 (Phase 2)


예상 소요 시간: 2-3주



🔒 3. 보안 강화 (API 키 관리)



취약점 분석


현재 코드 (app/build.gradle.kts):


// 48-49행: 심각한 보안 문제
resValue("string", "cupang_access_key", cupangAccessKey)
resValue("string", "cupang_secret_key", cupangSecretKey)

// 53행: 디버그 로그로 키 길이 노출
println("[CoupangKeys@Gradle] access.len=${cupangAccessKey.length}, secret.len=${cupangSecretKey.length}")

문제점:



  • API 키가 res/values/strings.xml에 평문으로 포함되어 APK 디컴파일 시 즉시 노출

  • ProGuard/R8 난독화로도 리소스 파일은 보호 불가

  • SECURITY.md에는 Keystore 기반 암호화 권장하나 미구현


개선 방안 (3단계)


Step 1: BuildConfig로 이동 (즉시 적용 가능)


// app/build.gradle.kts
android {
defaultConfig {
// resValue 삭제하고 BuildConfig로 변경
buildConfigField("String", "CUPANG_ACCESS_KEY", "\"${cupangAccessKey}\"")
buildConfigField("String", "CUPANG_SECRET_KEY", "\"${cupangSecretKey}\"")
}

buildFeatures {
buildConfig = true // BuildConfig 활성화
}
}

// 사용 방법
// Before: context.getString(R.string.cupang_access_key)
// After: BuildConfig.CUPANG_ACCESS_KEY

Step 2: ProGuard 규칙 강화


# proguard-rules.pro
-keepclassmembers class com.billcoreatech.health501.BuildConfig {
!public <fields>;
}

# 난독화 강화
-repackageclasses 'o'
-allowaccessmodification

Step 3: NDK 또는 서버 프록시 (장기 과제)



  • NDK 방식: C++ 네이티브 레이어에서 키 관리 (역공학 난이도 ↑)

  • 서버 프록시 방식 (권장): 앱은 자체 서버를 호출하고, 서버가 Coupang API를 호출하여 키 노출 완전 차단


즉시 적용 사항


// 디버그 로그 제거
// println("[CoupangKeys@Gradle] access.len=${cupangAccessKey.length}...") ← 삭제

// 또는 릴리스 빌드에서만 제거
if (gradle.startParameter.taskNames.any { it.contains("Debug", ignoreCase = true) }) {
println("[Dev] Coupang keys loaded (debug only)")
}

우선순위: 긴급 (Phase 1)


예상 소요 시간: 1-2일



🚀 4. 상태 관리 개선



현재 문제점


HealthConnectViewModel.kt 분석:


// 일관성 없는 상태 관리 패턴 혼용
var uiState: UiState by mutableStateOf(UiState.Uninitialized) // Compose State
val _stepsTotal = MutableStateFlow(0L) // StateFlow
val stepsTotal: StateFlow<Long> = _stepsTotal
var hasPermission = mutableStateOf(false) // 또 다른 mutableStateOf

// 총 20개 이상의 개별 상태 프로퍼티가 산재

개선 방안: 단일 UiState 패턴


// 통합된 상태 클래스
data class HealthUiState(
val isLoading: Boolean = false,
val stepsToday: Long = 0L,
val sessions: List<ExerciseSession> = emptyList(),
val sessionMetrics: ExerciseSessionData = ExerciseSessionData(""),
val bucketData: List<BucketData> = emptyList(),
val permissionsGranted: Boolean = false,
val backgroundReadAvailable: Boolean = false,
val error: String? = null,
val currentDateTime: ZonedDateTime = ZonedDateTime.now()
)

// ViewModel
@HiltViewModel
class HealthConnectViewModel @Inject constructor(
private val getTodayStepsUseCase: GetTodayStepsUseCase,
private val getExerciseSessionsUseCase: GetExerciseSessionsUseCase
) : ViewModel() {

private val _uiState = MutableStateFlow(HealthUiState())
val uiState: StateFlow<HealthUiState> = _uiState.asStateFlow()

fun loadTodayData() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }

when (val result = getTodayStepsUseCase()) {
is Result.Success -> {
_uiState.update {
it.copy(
stepsToday = result.data,
isLoading = false
)
}
}
is Result.Error -> {
_uiState.update {
it.copy(
error = result.message,
isLoading = false
)
}
}
}
}
}
}

// Compose UI에서 사용
@Composable
fun HealthScreen(viewModel: HealthConnectViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

when {
uiState.isLoading -> LoadingIndicator()
uiState.error != null -> ErrorMessage(uiState.error!!)
else -> StepsDisplay(uiState.stepsToday)
}
}

장점:



  • 단일 진실 공급원(Single Source of Truth)

  • Compose recomposition 최적화 (불필요한 재구성 최소화)

  • 상태 변경 추적 용이 (디버깅 개선)

  • 테스트 단순화


우선순위: 중요 (Phase 2)


예상 소요 시간: 3-5일



📦 5. 빌드 & 의존성 최적화



개선 사항


5.1 gradle.properties 최적화


# 빌드 속도 개선 설정 추가
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError
kotlin.incremental=true
kotlin.incremental.usePreciseJavaTracking=true

# Configuration cache (Gradle 8.x)
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn

5.2 libs.versions.toml 번들링


[bundles]
lifecycle = [
"androidx-lifecycle-runtime",
"androidx-lifecycle-viewmodel-compose",
"androidx-lifecycle-runtime-compose"
]

compose = [
"androidx-compose-ui",
"androidx-compose-ui-graphics",
"androidx-compose-ui-tooling-preview",
"androidx-compose-material3"
]

ktor = [
"ktor-client-core",
"ktor-client-okhttp",
"ktor-client-logging",
"ktor-client-content-negotiation",
"ktor-serialization-kotlinx-json"
]

# 사용
dependencies {
implementation(libs.bundles.lifecycle)
implementation(libs.bundles.compose)
implementation(libs.bundles.ktor)
}

5.3 불필요한 의존성 검토


// app/build.gradle.kts
// ❓ 검토 필요
implementation(libs.services.fitness) // Health Connect 사용 시 필요성 재평가
implementation(libs.dialog.core) // Material3 Dialog로 대체 가능
implementation(libs.dialog.lifecycle) // 상동

우선순위: 일반 (Phase 3)



🌐 6. 네트워크 레이어 개선



현재 상태


Ktor Client가 NetworkModule.kt에만 정의되어 있으며, 에러 핸들링·재시도·타임아웃 정책이 미흡합니다.


개선 구조


data/network/
├── KtorClientFactory.kt
├── NetworkErrorHandler.kt
├── ApiResponse.kt (sealed class)
├── interceptor/
│ ├── AuthInterceptor.kt
│ └── LoggingInterceptor.kt
└── service/
└── CoupangApiService.kt

구현 예시


// data/network/ApiResponse.kt
sealed interface ApiResponse<out T> {
data class Success<T>(val data: T) : ApiResponse<T>
data class Error(val code: Int, val message: String) : ApiResponse<Nothing>
object NetworkError : ApiResponse<Nothing>
object Timeout : ApiResponse<Nothing>
}

// di/NetworkModule.kt (개선)
@Provides
@Singleton
fun provideHttpClient(): HttpClient = HttpClient(OkHttp) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
})
}

install(Logging) {
logger = Logger.ANDROID
level = if (BuildConfig.DEBUG) LogLevel.BODY else LogLevel.NONE
}

install(HttpTimeout) {
requestTimeoutMillis = 30_000
connectTimeoutMillis = 15_000
socketTimeoutMillis = 30_000
}

install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 3)
exponentialDelay()
}

defaultRequest {
header("User-Agent", "Health501/${BuildConfig.VERSION_NAME}")
header("Accept", "application/json")
}
}

우선순위: 중요 (Phase 2)



🔧 7. 코드 품질 개선



즉시 수정 사항


7.1 패키지 오타 수정


현재: app/src/main/java/com/billcoreatech/health501/presentaion/
수정: app/src/main/java/com/billcoreatech/health501/presentation/

7.2 TODO 해결


// StepsStateListenerService.kt:26
// TODO: forward to a repository / shared flow if needed.

// 개선 방안: Repository 패턴 도입 시 함께 해결
class StepsStateListenerService : Service() {
@Inject lateinit var stepsRepository: StepsRepository

override fun onStepsChanged(steps: Long) {
viewModelScope.launch {
stepsRepository.updateSteps(steps) // Repository로 전달
}
}
}

7.3 Extension Functions 정리


// util/Extensions.kt (새로 생성)
fun ZonedDateTime.toFormattedString(pattern: String = "yyyy-MM-dd HH:mm"): String =
this.format(DateTimeFormatter.ofPattern(pattern))

fun Long.toStepString(): String = DecimalFormat("#,###").format(this)

fun Context.hasPermission(permission: String): Boolean =
ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED

우선순위: 긴급 (Phase 1 - 오타 수정만)



🤖 8. CI/CD 파이프라인 구축



GitHub Actions 워크플로우


# .github/workflows/ci.yml
name: CI Pipeline

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Cache Gradle packages
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Run unit tests
run: ./gradlew testDebugUnitTest --stacktrace

- name: Run lint
run: ./gradlew lintDebug

- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: app/build/reports/tests/

- name: Upload lint results
uses: actions/upload-artifact@v3
if: always()
with:
name: lint-results
path: app/build/reports/lint-results-debug.html

build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Build debug APK
run: ./gradlew assembleDebug

- name: Upload APK
uses: actions/upload-artifact@v3
with:
name: app-debug
path: app/build/outputs/apk/debug/app-debug.apk

우선순위: 중요 (Phase 3)



📊 9. 우선순위별 로드맵











































Phase 기간 작업 항목 우선순위 기대 효과
Phase 1
(즉시)
1주 ✅ presentaion → presentation 수정
✅ API 키 BuildConfig로 이동
✅ 디버그 로그 민감정보 제거
✅ gradle.properties 최적화
긴급 보안 취약점 해결
빌드 속도 향상
Phase 2
(단기)
2-3주 🏗️ Domain 레이어 구축
🏗️ Repository 패턴 도입
🏗️ UseCase 클래스 작성
🏗️ 상태 관리 통합 (단일 UiState)
긴급 아키텍처 정립
테스트 가능성 향상
Phase 3
(중기)
1개월 🧪 테스트 커버리지 70%+ 달성
🧪 Mock/Fake 구현
🌐 네트워크 에러 핸들링 강화
🤖 CI/CD 파이프라인 구축
중요 안정성 향상
자동화 구축
Phase 4
(장기)
2-3개월 📊 Firebase Analytics 통합
📊 Crashlytics 추가
🚀 성능 프로파일링
🚀 멀티모듈화 검토
일반 모니터링
확장성 확보


📈 10. 성과 측정 지표 (KPI)



측정 가능한 개선 목표











































지표 현재 목표 (3개월 후) 측정 방법
테스트 커버리지 ~10% 70%+ JaCoCo 리포트
빌드 시간 (Clean Build) 측정 필요 -30% 개선 Gradle Build Scan
Lint 경고 측정 필요 0건 (Critical) ./gradlew lint
코드 중복도 측정 필요 <5% Detekt 정적 분석
평균 메서드 길이 측정 필요 <30 LOC SonarQube


💭 회고 및 고찰



핵심 인사이트



  • 문서 vs 실제 코드의 괴리: ARCHITECTURE.md, TESTING.md에 명시된 원칙들이 실제 구현되지 않은 상태. 이는 프로젝트 초기에 이상적 구조를 설계했으나, 실제 개발 과정에서 우선순위나 시간 제약으로 인해 단계적 구현이 이뤄지지 않았음을 시사

  • 기술 부채의 누적: 초기에는 빠른 프로토타이핑을 위해 ViewModel에서 직접 Manager를 호출하는 방식을 택했으나, 이제 프로젝트가 성숙 단계에 접어들면서 리팩터링 필요성이 대두

  • 보안에 대한 인식 부족: API 키를 string resource로 노출하는 것은 초보적 실수. 민감정보 관리에 대한 체계적 접근 필요

  • 테스트 문화 부재: AI 관련 유틸만 테스트되고 핵심 비즈니스 로직은 테스트가 없다는 것은 개발 과정에서 테스트 우선 접근(TDD)이 적용되지 않았음을 의미


프로젝트의 강점



  • ✅ 현대적 기술 스택 채택 (Compose, Hilt, Kotlin Coroutines, Flow)

  • ✅ Version Catalog로 의존성 중앙 관리

  • ✅ 명확한 문서화 (ARCHITECTURE.md, TESTING.md, SECURITY.md)

  • ✅ AI 모델 자동 선택 유틸 등 독창적 기능 구현

  • ✅ Phone + Wear OS 통합 프로젝트로 복잡도 관리


장기적 비전


이번 개선 로드맵을 통해 Health501은 다음과 같은 장기적 이점을 얻을 수 있습니다:



  1. 확장성: 새로운 기능 추가 시 명확한 레이어 구조로 인해 변경 영향 범위를 최소화

  2. 협업 효율성: 팀원이 추가되어도 일관된 패턴으로 인해 온보딩 시간 단축

  3. 유지보수성: 높은 테스트 커버리지로 리그레션 방지 및 안전한 리팩터링 가능

  4. 품질 보증: CI/CD를 통한 자동화된 검증으로 버그 조기 발견



📋 다음 단계 (Next Actions)



즉시 착수 가능한 작업 (오늘~이번 주)



  1. presentaion → presentation 패키지 리네임
    git mv app/src/main/java/com/billcoreatech/health501/presentaion \
    app/src/main/java/com/billcoreatech/health501/presentation


  2. API 키 보안 강화

    • app/build.gradle.kts 수정 (resValue → buildConfigField)

    • CoupangViewModel.kt에서 사용 방식 변경

    • 디버그 로그 제거



  3. gradle.properties 최적화

    • 캐싱, 병렬 빌드 활성화

    • JVM 힙 메모리 증가



  4. domain 패키지 구조 생성
    mkdir -p app/src/main/java/com/billcoreatech/health501/domain/{model,usecase}
    mkdir -p app/src/main/java/com/billcoreatech/health501/data/repository


  5. Result.kt sealed interface 작성

    • Success, Error, Loading 상태 정의

    • 전체 프로젝트에서 일관되게 사용




주간 목표 설정 (Week 1-2)



  • [ ] Phase 1 모든 작업 완료

  • [ ] GetTodayStepsUseCase 구현 및 테스트 작성

  • [ ] HealthDataRepository 인터페이스 및 구현체 작성

  • [ ] HealthConnectViewModel 리팩터링 (UseCase 통합)

  • [ ] CI/CD 워크플로우 초안 작성



🎓 학습 자료 및 참고 문서






 



 





댓글 없음:

댓글 쓰기

오늘의 이야기

🔍 프로젝트 진단 | Health501 아키텍처 & 코드 품질 개선 로드맵 샘플이미지   📊 개요 (Executive Summary) 작업 일자: 2025-1...