공부/Android

[아키텍쳐] 클린아키텍쳐 적용하면서 했던 고민 메모, 좋았던점

데자와 맛있다 2023. 9. 26. 04:44

수면 데이터를 가져오기 위해 엔티티와 모델을 아래처럼 만들었다

보시는바와 같이 아무생각없이 그냥 똑~같이 생겼다

매퍼도 이렇게 했다.. 이럴거면 매퍼를 왜만들지? 하는 생각이 들었다

 

암튼 이렇게  하고있는데

화면단에서 총 수면시간, 깬 시간, 렘, 가벼운 수면, 깊은수면 이렇게만 데이터를 가져도 된다는것을 알게되었다

이렇게 생긴 화면이었기 때문!

그런데 이렇게만 알면 되는데 헬스커넥트에서 주는 데이터 그대로~~~ 받아서 쓰니 화면단에서 한번 더 처리를 하게되어 지저분한 코드가 되어있었다!

@Composable
fun SleepBottomSheet(
    sleepSessionData: MutableList<SleepSessionDto>
) {
    var awake: Duration = Duration.ZERO
    var rem: Duration = Duration.ZERO
    var light: Duration = Duration.ZERO
    var deep: Duration = Duration.ZERO
    var totalSleep: Duration = Duration.ZERO

	//받은 수면 데이터를 원하는 모양으로 가공하기 위해 거치는 작업
    if (sleepSessionData.size > 0) {
        sleepSessionData.forEach {
            it.stages.forEach {
                val start = it.startTime.atZone(ZoneId.systemDefault()).toLocalDateTime()
                val end = it.endTime.atZone(ZoneId.systemDefault()).toLocalDateTime()
                val durationValue = Duration.between(start, end)
                totalSleep+=durationValue
                if (it.stage == STAGE_TYPE_AWAKE) {
                    awake += durationValue
                } else if (it.stage == STAGE_TYPE_REM) {
                    rem += durationValue
                } else if (it.stage == STAGE_TYPE_LIGHT) {
                    light += durationValue
                } else if (it.stage == STAGE_TYPE_DEEP) {
                    deep += durationValue
                }
            }
        }
        Log.d(TAG, "SleepBottomSheet: awake = ${awake} ${awake.toHours()} ${awake.toMinutes()%60}")
        Log.d(TAG, "SleepBottomSheet: rem = ${rem} ${rem.toHours()} ${rem.toMinutes()%60}")
        Log.d(TAG, "SleepBottomSheet: light = ${light} ${light.toHours()} ${light.toMinutes()%60}")
        Log.d(TAG, "SleepBottomSheet: deep = ${deep} ${deep.toHours()} ${deep.toMinutes()%60}")
    }

    Column(modifier = Modifier.padding(20.dp)) {
        BottomSheetHeader(
            missionType = MissionType.SLEEP,
            sleepHour = totalSleep.toHours(),
            sleepMinute = totalSleep.toMinutes()%60
        )
        Spacer(modifier = Modifier.size(20.dp))
        SleepBottomSheetBody(
            awake.toHours(),
            awake.toMinutes()%60,
            rem.toHours(),
            rem.toMinutes()%60,
            light.toHours(),
            light.toMinutes()%60,
            deep.toHours(),
            deep.toMinutes()%60
        )
        Spacer(modifier = Modifier.size(50.dp))
    }
}

 

if문과 for문을 돌면서 화면단에 이렇게 지저분한 로직을 넣고 말았다

반성좀 하고 난 다음에 그다음에는 viewModel에서 해당 로직을 처리하도록 만들었다

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val healthConnectClient: HealthConnectClient,
    private val homeUseCases: HomeUseCases
) : ViewModel() {

    val permissions = setOf(
        HealthPermission.getReadPermission(SleepSessionRecord::class)
    )

    var permissionsGranted = mutableStateOf(false)
        private set

    private val _sleepPermissionState = MutableStateFlow<SleepPermissionState>(
        SleepPermissionState.PermissionStateLoading
    )
    val sleepPermissionState: StateFlow<SleepPermissionState> = _sleepPermissionState.asStateFlow()

    private val _sleepSessionData = MutableStateFlow<MutableList<SleepSessionDto>>(mutableListOf())
    val sleepSessionData: StateFlow<MutableList<SleepSessionDto>> = _sleepSessionData.asStateFlow()

	//총 수면 데이터 저장하는 flow
    //awake 데이터 저장하는 flow
    //rem 데이터 저장하는 flow
    //light 데이터 저장하는 flow
    //deep 데이터 저장하는 flow

    fun checkPermission() {
        viewModelScope.launch {
            val permissionState = healthConnectClient.permissionController.getGrantedPermissions()
                .containsAll(permissions)
            if (permissionState == true) {
                _sleepPermissionState.value = SleepPermissionState.PermissionAccepted
            } else {
                _sleepPermissionState.value = SleepPermissionState.PermissionNotAccepted
            }
        }
    }

    private fun tryWithPermissionsCheck(block: suspend () -> Unit) {
        viewModelScope.launch {
            Log.d(TAG, "tryWithPermissionsCheck: 블록실행시작")
            //퍼미션이 이미 허용되었는지 확인합니다
            val permissionState = healthConnectClient.permissionController.getGrantedPermissions()
                .containsAll(permissions)
            //퍼미션 허용 되었으면 매개변수로 받은 block을 실행합니다
            _sleepPermissionState.value = try {
                if (permissionState) {
                    block()
                }
                SleepPermissionState.PermissionAccepted
            } catch (e: Exception) {
                SleepPermissionState.PermissionErr
            }
            Log.d(TAG, "tryWithPermissionsCheck:블록끝 ${permissionState}")
        }

    }

    fun getSleepData() {
        tryWithPermissionsCheck {
            val today = ZonedDateTime.now()
            //_sleepSessionData.value = homeUseCases.getSleepDataUseCase.invoke(today)
            //대강여기에서 다섯가지 수면 타입 총 시간을 계산
        }
    }

}

지금은 그 뷰모델에서 처리하는 코드가 없어서 위에 대강 주석으로 설명했다

암튼 이렇게 useCase를 통해서 수면 데이터 받고 거기서 if for문으로 수면 값들 합쳐주니

total, awake, rem, light, deep이렇게 5개의 state flow가 생겼다

이렇게 하면 되겠지? 하고 생각했는데 또 생각해보니 이게 5개로 나눠져있으면 컴포저블에서 얘가 상태 변하면 리컴포지션을 할건데 그러면 5번이나 쓸데없이 리컴포지션하게 되는것..

그래서 또 생각한거는 얘들을 배열로 만들어서 [total, awake, rem, light, deep] 이렇게 만들어서 이 배열을 state flow로 관리할까? 이생각을함..

근데 그럴바에는 걍 domain에 있는 model에 총수면 시간, 깬시간, 렘시간, 가벼운 수면, 깊은수면시간 이렇게 저장하는게 낫지 않나? 생각을 했다

 

그래서 도메인에 있는 모델을 이렇게 바꿨다

매퍼는 이렇게 바꿨다

수면 데이터들을 리스트로 받으면 매퍼에서 가공해서 종류별 총 수면시간을 계산하고 SleepSessionDto로 반환한다

앞에서 본 SleepCottomSheet는 드러운 코드가 없어져 더 간략해졌다. 화면단에서 했던 일을 빼서 관심사 분리가 더 잘 되었다

 

그리고 또 다른 이점을 생각해보자면 똑같은 수면 데이터 소스이지만 화면이 새로 추가되어서 각 종류별 총합이 아니라 각각 또 따로 보여줘야 되는 화면이 생긴다면 도메인단에 새로 모델을 하나 만들어주고 매퍼도 적절히 만들어주면 데이터 소스쪽은 바꿔야될게 없어진다 

 

그나저나 다른 블로그들 보면서 든 생각인데 도메인쪽 모델이름뒤에 dto를 붙이는게 맞나 싶은생각이 든다

그리고 %60 하는것도 매퍼에서 해도 될듯

그리고 도메인쪽 디펜던시에 안드로이드 관련된거 없어야된다던데 나는 수면 데이터 가져오는 부분 때문에 health connect를 디펜던시에 넣음 근데 다시 살펴보니 없어도 될것 같기도하다 물론 그 부분을 위한 데이터 클래스를 새로 하나 더 만들어야된다...

 

 

-참고자료들

https://vagabond95.me/posts/clean-architecture-1/

 

[Android] Clean Architecture 를 도입하며 - 기록은 기억을 지배한다

들어가며 최근 안드로이드 진영에서 클린 아키텍처를 채택하는 흐름으로 굳혀진 것 같다. 현재 진행하고 있는 프로젝트에서도 신규 코드에 대해서는 클린 아키텍처를 도입해보자는 목표를 세

vagabond95.me

https://ddangeun.tistory.com/138

 

안드로이드 Clean Architecture 구현하기

Clean Architecture 위의 Uncle Bob Clean Architecture(CA)는 오늘날 많은 어플리케이션의 핵심 Architecture가 되었습니다. Dependecy Rule 하위 계층으로 갈수록 상위 계층을 몰라야 합니다. 내부 원의 어떤것도 외

ddangeun.tistory.com

https://bj-turtle.tistory.com/109

 

프로젝트 구조.. 왜 그렇게 희한하게 생겼는가.. Clean Architecture

▩ 목 차 ▩ 1. Clean Architecture 란 ? 1-1. Clean Architecture의 필요성 1-2. 안드로이드 Clean Architecture에 대해 1-3. 안드로이드 클린 아키텍처에서 사용되는 계층 1-4. 안드로이드 클린 아키텍처의 계층에서

bj-turtle.tistory.com


+

추가로 발견한 좋았던점!

드디어 "관심사 분리" 가 뭔 소리인지 실감이 났다!

머냐면 이거 만들다가.. 이게 게임이랑 관련된거다 보니 게임쪽에서 캐릭터 스탯을 새벽 3시에 새로고침 해야됐음

그니깐 모든것이 새벽 3시를 기점으로 리셋이 되어야했음

근데 그거를 내가 간과하고 수면 데이터 받는것을 만들어가지고

홈화면에 나와야되는 수면 양을 오늘 0시~12시 사이에 기상한 수면양만 가져온거임

그래서 이게 미션 달성 여부도 서버에서 받아오는건데 이것도 새벽 3시에 리셋된단말임?

그래서 어떤 문제가 생기냐?

만약에 오늘이 26일이라고 쳐 그러면 26일 밤 12시 까지는 올바르게 나옴 모든것이

근데 26일에서 27로 바뀌면 내 수면양은 홈에서 0시간으로 나오는데 미션 달성 여부는 어제것이 나오는것임

그래서 그거때문에 홈에서 보여줘야되는 수면 데이터를 바꿔야했음

그래서 나는 그냥 HealthDataSource 데이터 소스만 바꾸면 됐음, 걍 데이터 가져오는 부분만 바꾸면 됐고 화면을 표시하는 presentation은 그대로 놔둬도 됐었다

화면단은 오로지 관심사가 유저와 상호작용, 유저한테 값보여주기 이게 끝이기때문에 데이터 소스가 변경되는것은 presentation의 관심영역이 아니고 그저 주는 데이터를 늘 하던대로 보여주면 되기때문

그래서 아 이게 관심사 분리구나! 했다

 

 

그리고 약간의 고민

지금 시간이 없기도 하고 클린아키텍쳐를 처음 해보는거기도 하고그래서

지금 뷰모델에 엄청난.. if문과 로직들이 들어가있는데 얘들을 presentation이 아니라 다른 레이어로 보내야될것같은 그런느낌스.. 리팩토링이 필요하다


+ 걷기 데이터 가져오는 로직 추가

이때 바보이슈가 발생하였음

알람 매니저를 사용해서 백그라운드에서 걸음수 데이터를 전송했어야 했는데

헬스커넥트는 백그라운드에서 데이터를 가져가는것을 원천 차단함

그래서 포그라운드에서 걷기 데이터를 보내도록 해야 햇다

이때 useCase 사용하는 곳만 바꿔주면 되었기때문에 빠른 수정이 가능했다