#1 문제점
#1-1 리팩토링할 기존 코드
package com.example.nutri_capture_new.db
import androidx.room.ColumnInfo
data class NutritionInfo(
// 과식 정도값
@ColumnInfo(name = "overeating_excess")
var overeatingExcess: Int = 0,
// 정제당 섭취 정도값
@ColumnInfo(name = "refined_sugar_excess")
var refinedSugarExcess: Int = 0,
// 정제 곡물 섭취 정도값
@ColumnInfo(name = "refined_grain_excess")
var refinedGrainExcess: Int = 0,
// 밀가루 섭취 정도값
@ColumnInfo(name = "flour_excess")
var flourExcess: Int = 0,
// 섬유질 섭취 정도값
@ColumnInfo(name = "fiber_quality")
var fiberQuality: Int = 0,
// 단백질 섭취 정도값
@ColumnInfo(name = "protein_quality")
var proteinQuality: Int = 0,
// 나트륨 섭취 정도값
@ColumnInfo(name = "sodium_excess")
var sodiumExcess: Int = 0
)
#1-2 Column의 이름에 프로그래밍 로직이 들어감
기존 코드에는 컬럼 이름에 excess가 붙으면, '일반적으로 많이 먹을수록 나쁜 것', 컬럼 이름에 quality가 붙으면, '일반적으로 많으 먹을수록 좋은 것'이라는 규칙이 있었다. 이 코드는 분명 개인 프로젝트의 코드다. 따라서 나만 알아보면 되긴 한다. 그러나, 항상 '남들이 봤을 때 직관적인' 코드를 지향해야 한다. 분명 언젠간 회사에 들어가 팀 프로젝트를 하게 될 테니까. 또, 사정이 생겨 개발에 오랜 기간 손을 떼고 다시 개발하려는 경우 곤란할 수도 있다. 어쨌거나, 이 규칙을 폐기한다.
#1-3 영양소를 순회하기 불편함
명색이 데이터 클래스인데, getter나 setter가 없다. NutritionInfo 클래스는 오로지 Room에 테이블 스키마를 전달하는 용도로만 쓰이고 있다. 즉, 너무나 정직하고 순박(?)하다. 다른 코드에서 NutritionInfo를 참조하여 각 영양소를 순회하는 경우는 반드시 발생할 수 밖에 없다. 그러나 기존 코드에는 관련된 코드가 없다.
#2 코드 스니펫
#2-1 NutritionDetail 클래스 추가
package com.example.nutri_capture_new.db
import androidx.room.ColumnInfo
import androidx.room.Ignore
import com.example.nutri_capture_new.R
data class NutritionDetail(
@Ignore
val name: String = "기본 이름",
@ColumnInfo(name = "value")
var value: Int = 0,
@Ignore
val iconId: Int = R.drawable.default_nutrition,
@Ignore
val nutritionCategory: NutritionCategory = NutritionCategory.MODERATE
)
#1-1의 각 프로퍼티를 NutritionDetail이라는 이름의 클래스로 감쌀 것이다. name은 말 그대로 영양소의 이름, value는 섭취 정도값, iconId는 영양소를 대표하는 아이콘 벡터 이미지의 리소스 ID, nutritionCategory는 영양소의 범주다. NutritionCategory는 enum class로 아래와 같다.
package com.example.nutri_capture_new.db
enum class NutritionCategory {
GOOD, // 많이 먹을수록 좋은 영양소
BAD, // 많이 먹을수록 나쁜 영양소
MODERATE // 적당히 먹어야 좋은 영양소
}
NutritionCategory는 #1-2에서 말한 규칙을 대신한다.
#2-2 리팩토링한 코드
...
data class NutritionInfo(
// 과식 정도값
@Embedded(prefix = "overeating_")
val overeating: NutritionDetail = NutritionDetail(
name = "과식한 정도",
value = 0,
iconId = R.drawable.overeating,
nutritionCategory = NutritionCategory.BAD
),
// 정제당 섭취 정도값
@Embedded(prefix = "refined_sugar_")
val refinedSugar: NutritionDetail = NutritionDetail(
name = "정제당",
value = 0,
iconId = R.drawable.refined_sugar,
nutritionCategory = NutritionCategory.BAD
),
// 정제 곡물 섭취 정도값
@Embedded(prefix = "refined_grain_")
val refinedGrain: NutritionDetail = NutritionDetail(
name = "정제 곡물",
value = 0,
iconId = R.drawable.refined_grain,
nutritionCategory = NutritionCategory.BAD
),
// 밀가루 섭취 정도값
@Embedded(prefix = "flour_")
val flour: NutritionDetail = NutritionDetail(
name = "밀가루",
value = 0,
iconId = R.drawable.flour,
nutritionCategory = NutritionCategory.BAD
),
// 섬유질 섭취 정도값
@Embedded(prefix = "fiber_")
val fiber: NutritionDetail = NutritionDetail(
name = "식이섬유",
value = 0,
iconId = R.drawable.fiber,
nutritionCategory = NutritionCategory.GOOD
),
// 단백질 섭취 정도값
@Embedded(prefix = "protein_")
val protein: NutritionDetail = NutritionDetail(
name = "단백질",
value = 0,
iconId = R.drawable.protein,
nutritionCategory = NutritionCategory.GOOD
),
// 나트륨 섭취 정도값
@Embedded(prefix = "sodium_")
val sodium: NutritionDetail = NutritionDetail(
name = "나트륨",
value = 0,
iconId = R.drawable.sodium,
nutritionCategory = NutritionCategory.BAD
),
)
NutritionCategory가 있으므로, 컬럼 이름에서 "excess" 또는 "quality"를 제거한다. @Embedded 어노테이션의 prefix 프로퍼티는 컬럼 이름이 아니다. 어노테이션 구조 상의 자식 중 @ColumnInfo 어노테이션이 존재하면, 그 @ColumnInfo 어노테이션의 name 프로퍼티 앞에 붙는 접두사가 바로 prefix다.
가령, NutritionInfo.sodium.value는 "sodium_value"라는 컬럼에 저장된다. NutritionInfo.sodium.value에 붙은 어노테이션이 @ColumnInfo(name = "value")가 아니라 @ColumnInfo(name = "sample")이었다면 "sodium_sample"이라는 이름의 컬럼에 저장되었을 것이다.
#2-3 getter 및 setter
...
data class NutritionInfo(
...
) {
fun toMutableMap(): MutableMap<String, NutritionDetail> {
return mutableMapOf(
"overeating" to overeating,
"refinedSugar" to refinedSugar,
"refinedGrain" to refinedGrain,
"flour" to flour,
"fiber" to fiber,
"protein" to protein,
"sodium" to sodium
)
}
fun updateNutritionDetail(nutritionKey: String, update: (Int) -> Int): NutritionInfo {
val updatedMap = this.toMutableMap().apply {
get(nutritionKey)?.let { detail ->
this[nutritionKey] = detail.copy(value = update(detail.value))
}
}
return NutritionInfo(
overeating = updatedMap["overeating"] ?: this.overeating,
refinedSugar = updatedMap["refinedSugar"] ?: this.refinedSugar,
refinedGrain = updatedMap["refinedGrain"] ?: this.refinedGrain,
flour = updatedMap["flour"] ?: this.flour,
fiber = updatedMap["fiber"] ?: this.fiber,
protein = updatedMap["protein"] ?: this.protein,
sodium = updatedMap["sodium"] ?: this.sodium
)
}
}
getter는 모든 영양소의 순회를 위함이다. setter는 보기에 약간 지저분하지만, 여기에서 사용하기 위해서 위와 같이 짰다. 내가 봐도 깔끔한 코드는 아니기에, 언젠간 다시 리팩토링하게 될 것으로 보인다. 지금은 그저 떠오르는 최선의 방식에 충실했다.
#4 완성된 앱
#4-1 이 게시글 시점의 Commit
GitHub - Kanmanemone/nutri-capture-new
Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.
github.com
#4-2 본 프로젝트의 가장 최신 Commit
GitHub - Kanmanemone/nutri-capture-new
Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.
github.com
'개발 일지 > Nutri Capture' 카테고리의 다른 글
Nutri Capture 프론트엔드 - NutrientBottomSheet 부분 구현 (0) | 2025.03.26 |
---|---|
Nutri Capture 프론트엔드 - '피자' 아이콘 임시 적용 (0) | 2025.03.25 |
Nutri Capture 프론트엔드 - '피자' 아이콘 구현 (0) | 2025.03.20 |
Nutri Capture 프론트엔드 - 커스텀 BottomSheetScaffold 개발 유예 (0) | 2025.03.19 |
Nutri Capture 백엔드 - Hilt 도입 (0) | 2025.02.01 |