개발 일지/Nutri Capture

Nutri Capture 백엔드 - NutritionInfo 리팩토링

interfacer_han 2025. 3. 26. 21:43

#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