Clean Architecture 실습 및 정리 [2]

ee·2024년 11월 14일

Clean Architecture

목록 보기
5/6

이번에 다룰 layer는 domain과 presentation이다.

domain

domain layer는 model, repository 그리고 usecase로 이루어져있다.
domain layer를 작성할 때 주의할 점은 외부에서 domain layer에 접근 가능하게 해야하고 내부에서는 외부를 알 수 없다는 것이다.

데이터 흐름을 보면 HTTP 요청을 하면 WordInfoDto에 받아온다. 다시 이것을 WordInfoEntity로 매핑하여 database에 저장한다. 그리고 이 것을 database에서 가져와서 WordInfo로 맵핑해 emit()하여 data를 presentation layer에서 접근할 수 있다.

model

이 모델은 presentation layer에서 접근할 때 쓰는 data class이다.

repository

WordInfo를 가져오는 WordInfoRepository를 작성한다.

usecase

word가 blank일 경우 flow{}를 리턴하고 아니면 repository에 접근하여 getWordInfo를 호출한다.
이 때 repository는 생성자로 작성하여 외부에서 의존성을 주입할 수 있게 한다.

presentation

presentation layer는 사용자와 상호작용하는 state, events 등 유저에게 보여지는 UI등을 담고 있고 디자인 패턴인 MVVM 혹은 MVI도 포함된다.

state와 event

state와 event는 viewmodel안에 작성해도 되지만 그 수가 많다면 따로 파일을 만들어서 작성하는 것이 좋다.

viewModel

@HiltViewModel
class WordInfoViewModel @Inject constructor(
    private val getWordInfo: GetWordInfo
): ViewModel() {

    private val _searchQuery = mutableStateOf("")
    val searchQuery: State<String> = _searchQuery

    private val _state = mutableStateOf(WordInfoState())
    val state: State<WordInfoState> = _state

    private val _eventFlow = MutableSharedFlow<UIEvent>()
    val eventFlow = _eventFlow.asSharedFlow()

    private var searchJob: Job? = null

    fun onSearch(query: String){
        _searchQuery.value = query
        searchJob?.cancel()
        searchJob = viewModelScope.launch {
            delay(500L)
            getWordInfo(query)
                .onEach { result ->
                    when(result){
                        is Resource.Success -> {
                            _state.value = state.value.copy(
                                wordInfoItems = result.data ?: emptyList(),
                                isLoading = false
                            )
                        }
                        is Resource.Error -> {
                            _state.value = state.value.copy(
                                wordInfoItems = result.data ?: emptyList(),
                                isLoading = false
                            )
                            _eventFlow.emit(UIEvent.ShowSnackbar(
                                result.message ?: "Unknown error"
                            ))
                        }
                        is Resource.Loading -> {
                            _state.value = state.value.copy(
                                wordInfoItems = result.data ?: emptyList(),
                                isLoading = true
                            )
                        }
                    }
                }.launchIn(this)
        }
    }

    sealed class UIEvent{
        data class ShowSnackbar(val message: String): UIEvent()
    }
}

viewModel 선언시 hiltViewModel 어노테이션을 달아주고 usecase를 생성자로 주입시킨다.
한 글자를 입력할 때마다 api를 호출하는 것을 방지하기 위해 코루틴을 컨트롤하는 Job 변수를 사용한다. query가 변경될때 마다 searchQuery.value에 넣어 ui를 업데이트하고 먼저 코루틴을 중단시킨다. 그 후에 delay를 주어 단어를 다 입력했을 때만 usecase인 getWordInfo를 호출하고 그 것에 결과를 반영한다.

di

@Module
@InstallIn(SingletonComponent::class)
object WordInfoModule {

    @Provides
    @Singleton
    fun provideGetWordInfoUseCase(repository: WordInfoRepository): GetWordInfo{
        return GetWordInfo(repository)
    }

    @Provides
    @Singleton
    fun provideWordInfoRepository(db: WordInfoDatabase, api: DictionaryApi): WordInfoRepository{
        return WordInfoRepositoryImpl(api, db.dao)
    }

    @Provides
    @Singleton
    fun provideWordInfoDatabase(@ApplicationContext context: Context): WordInfoDatabase{
        return Room.databaseBuilder(
            context, WordInfoDatabase::class.java, "word_db"
        ).addTypeConverter(Converters(GsonParser(Gson())))
            .build()
    }

    @Provides
    @Singleton
    fun provideDictionaryApi(): DictionaryApi {
        return Retrofit.Builder()
            .baseUrl(DictionaryApi.BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(DictionaryApi::class.java)
    }
}

hilt가 각각 어떤 유형의 인스턴스를 주입해야하는지 알려주기 위해 usecase, repositoryImpl, database 그리고 api에 관한 함수를 작성하고 외부 라이브러리 (roomdb, retrofit)를 생성자 주입하기 위해 Provides 어노테이션을 지정하고 동일한 인스턴스를 주입하기 위해 Singleton 어노테이션을 지정해주었다.

총정리

데이터의 흐름, Flow, koroutine의 사용법, di를 하는 방법등을 실습하고 이해하는데 도움이 많이 되었다. 다음번엔 조금 더 복잡하고 규모가 큰 프로젝트를 정리해보겠다.

profile
정진이

0개의 댓글