[5주차 챌린지반 강의] 복습 TIL
1. [06:30] Data Layer : Repository, RemoteDatasource로 구성된다 - 앱의 데이터를 읽고 쓰는 레이어
DataSource = Model(Room) - SQLite
UI Layer -> ViewModel -> Repository ->
DataSource = RemoteDataSource(Retrofit) - Api Server
2. [08:50] ViewModel Factory 정의 방법만, 사용법은 이후에 다룸
3. [13:40] REST API : 자원을 이름으로 구분하여 해당 자원의 상태를 주고 받는 모든 것을 의미함
4. [14:28] Retrofit : REST API 기반의 웹 서비스를 통해 JSON 구조의 데이터를 쉽게 가져오고 업로드함
5. [15:00] Retrofit 사용을 위한 data class(Response), interface, object 정의
// 1. 데이터를 전달 받기위한 Response 객체 정의
// 배운점 : response의 키값과 data class의 변수명이 다를 때만 SerializedName을 쓰는 것이 아니라
// 전체적으로 SerializedName을 매칭시킨다
data class SearchImageResponse(
@SerializedName("meta") val meta: MetaResponse?,
@SerializedName("documents") val documents: List<ImageDocumentResponse>?,
)
// or
// video와 image api의 response의 구조가 동일하기 때문에 제네릭으로 처리할 때 아래와 같이 사용함
data class SearchResponse<T>(
@SerializedName("meta") val meta: MetaResponse?,
@SerializedName("documents") val documents: List<ImageDocumentResponse>?,
)
data class MetaResponse(
@SerializedName("total_count") val totalCount: Int?,
@SerializedName("pageable_count") val pageableCount: Int?,
@SerializedName("is_end") val isEnd: Boolean?,
)
data class ImageDocumentResponse(
@SerializedName("collection") val collection: String?,
@SerializedName("thumbnail_url") val thumbnailUrl: String?,
@SerializedName("image_url") val imageUrl: String?,
@SerializedName("width") val width: Int?,
@SerializedName("height") val height: Int?,
@SerializedName("display_sitename") val displaySitename: String?,
@SerializedName("doc_url") val docUrl: String?,
@SerializedName("datetime") val datetime: Date?,
)
// 2. HTTP 통신에 필요한 인터페이스 정의
// 배운점 : 일단 안 쓰는 쿼리값도 호출 시 디폴트 값으로 다 전달하기에 함수의 파라미터에 포함시킨다.
interface SearchRemoteDatasource {
@GET("/v2/search/image")
suspend fun getSearchImage(
@Query("query") query: Stinrg, // [필수] 쿼리
@Query("sort") sort: Stinrg, // [옵션] 필수가 아니여도 일단 넣기?
@Query("page") page: Int, // [옵션] 필수가 아니여도 일단 넣기?
@Query("size") size: Int, // [옵션] 필수가 아니여도 일단 넣기?
): SearchImageResponse
}
// suspend 사용 이유 : Coroutine 사용을 위함
// 3. Retrofit Builder 정의
object RetrofitClient {
private const val BASE_URL = "https://dapi.kakao.com" // 제일 뒤에 '/' 안 붙는 거 체크!!!
private val okHttpClient by lazy {
OkHttpClient.Builder()
.addInterceptor(AuthorizationInterceptor())
.build()
}
private val retrofit by lazy {
Retrofit.builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create()) // 받아온 데이터를 Json으로 변환하기 위함
.build()
}
val search: SearchRemoteDatasource by lazy {
retrofit.create(searchRemoteDatasource::class.java)
}
6. [46:00] ViewModel Factory
class SearchViewModelFactory : ViewModelProvider.Factory {
override fun <T : ViewModel> create(
modelClass: Class<T>,
extras: CreationExtras
): T {
// data - Remote(Datasource)
val searchRemoteDatasource = RetrofitClient.search
// data - Repository
val searchRepository = SearchRepositoryImpl(searchRemoteDatasource)
// ViewModel의 파라미터 searchRepository의 타입 : UI - Repository
// data의 RepositoryImpl class에서 UI의 Repository 구현 하므로 타입 문제 없음
return SearchViewModel(searchRepository) as T
// 기존 코드
return SearchViewModel(
SearchRepositoryImpl(RetrofitClient.search)
) as T
}
}
디렉토리 구조 및 역할 정리 - 이해한 대로 작성하여 의역의 여지가 있음
- network(data에서 분리함)
- RetrofitClient : RetrofitBuilder object 정의
- AuthInterceptor : 네트워크 통신 시, 가로채서 API 호출에 필요한 공통 Header(인증 Key값 등)를 심어준다
- data
- model(SearchResponse - data class) : Response의 Json 타입에 맞게 data calss 셋업
- remote(SearchRemoteDatasource - interface) : API 또는 내부 DB로 부터 데이터를 가져오기 위한 명세
- repository(SearchRepositoryImpl - class) : UI Layer와 Data Layer의 연결을 위한 구현체
(1) 생성자로 (Api 호출 기능이 구현된) network/remote의 interface 객체를 전달 받는다.
(2) ui/repository의 interface를 상속받아 구현한다.
(3) 전달 받은 interface 객체에 접근해 Api Call Method를 호출한다.
- ui (presentation)
- model : SearchEntity - data class : network의 model과 구성은 동일함 = 중간 백업 역할
SearchMapper - function : Response를 SearchEntity 타입으로 매핑하는 역할
-> "규모가 클 때, 효과적인 로직이다" ?!
- repository(SearchRepository - interface) : UI Layer에서 Data Layer에 접근하기 위한 interface 구현
Data Layer에서 이 interface를 오버라이드 하여 함수 재구현함 -> Layer 간의 연결
api 쿼리에서 optinal한 속성을 default value 설정 -> viewModel 에서는 query 값만 파라미터로 주고 함수 호출
- viewmodel(SearchViewModel - class) : 생성자로 ui/repository의 객체를 가지고 있음
-> ViewModel Factory를 활용하여 외부에서 ui/repository 객체를 주입 받는다.
- domain(optinal) - 선택적 이지만, 다양한 usecase가 존재할 경우에는 유지보수를 위해 필수 구현
- model:
- repository :
- usecase :
- 데이터를 가져오는 로직의 흐름 정리 (퍼사드 패턴*)
- UI Layer(ViewModel -> ui/repository) -> Data Layer(data/repository -> network/remote => API Call)
이후 return 값(Type: Response)을 통해 UI Layer로 데이터가 전달된다.
* 퍼사드(Facade) 패턴 : (라이브러리 혹은 프레임워크에) 간소화된 인터페이스를 제공하는 패턴
Q. [GPT] network의 model과 ui의 model을 분리해서 따로 사용했을 때의 장점과 단점은 뭐야?
A. 네트워크 계층의 모델(`model`)과 UI 계층의 모델(`ui/model`)을 분리해서 사용하는 방식은 여러 이점과 단점을 가지고 있습니다. 이런 설계 패턴은 일반적으로 Clean Architecture나 Layered Architecture에서 자주 보이는데, 각 계층의 책임을 명확하게 분리하는 것을 목표로 합니다.
### 장점
1. **계층 간의 독립성**: 네트워크 계층의 모델과 UI 계층의 모델을 분리함으로써, 한 쪽의 변경이 다른 쪽에 미치는 영향을 최소화합니다. 예를 들어, 서버에서 오는 데이터 포맷이 변경되어도, UI 모델에는 영향을 미치지 않을 수 있습니다. -> 튜터님이 언급하신 내용임 + "관심사 분리의 느낌이다"
2. **재사용성과 유지보수성 향상**: 각 계층이 잘 분리되어 있으면, 특정 계층만을 수정하거나 업그레이드하는 것이 더 쉬워집니다. 예를 들어, 데이터 소스가 변경되었을 때 네트워크 계층만 수정하면 되고, UI 계층은 그대로 유지할 수 있습니다. -> "유지보수 때문에 썼다고 하기엔 애매하다, 각각의 Layer에 맞게 모듈 분리를 한 것"
3. **테스트 용이성**: 계층별로 분리되어 있으면, 각 계층을 독립적으로 테스트할 수 있습니다. 네트워크 모델이 API 응답을 올바르게 파싱하는지, UI 모델이 사용자 인터페이스에 적합한 데이터를 제공하는지 각각 검증할 수 있습니다.
4. **보안성 증가**: 민감한 데이터를 네트워크 계층에서 처리하고, UI 계층에서는 접근하지 못하게 하는 등 보안 정책을 효과적으로 적용할 수 있습니다.
### 단점
1. **복잡성 증가**: 각 계층마다 별도의 모델을 관리해야 하므로 설계가 복잡해지고, 개발 초기 단계에서 더 많은 노력이 필요할 수 있습니다.
2. **코드 중복**: 네트워크 모델과 UI 모델이 유사한 경우, 비슷한 코드를 여러 번 작성해야 할 수 있어 중복이 발생할 가능성이 있습니다.
3. **개발 시간 증가**: 각 계층을 독립적으로 개발해야 하기 때문에, 전체 시스템을 완성하기까지의 개발 시간이 늘어날 수 있습니다.
4. **성능 저하**: 모델 간의 데이터 변환 과정에서 추가적인 처리 시간이 필요할 수 있으며, 이는 전체 시스템의 응답 시간을 저하시킬 수 있습니다. -> 대용량 데이터라면 이건 신경써야 할 듯??
이처럼 계층을 분리하는 설계 방식은 프로젝트의 요구 사항과 팀의 작업 방식에 따라 장단점이 명확하게 나뉩니다. 이를 고려하여 프로젝트의 목적과 환경에 가장 적합한 아키텍처를 선택하는 것이 중요합니다.
확실히 위와 같이 분리할 경우, UI Layer에서 Data Layer에 직접 참조를 하지 않는 구조가 된다?
-> ViewModel 에서는 Data Layer를 참조함 (애초에 ViewModel의 생성자로
주요 로직인 4개의 파일을 지운 뒤, 재구현함
기존 코드와 비교해서 누락된 부분 체크
1. ViewModel Class의 생성자에 접근 제한자(private) 누락
2. ui/repository : interface 내부의 메서드에 suspend 키워드 누락
3. data/repository interface 구현체 : 생성자에 접근 제한자(private) 누락
4. RemoteDatasource : @GET 이후 호출 경로 누락
이외 로직은 기존과 같으며, 바로바로 코딩했던 것 같다.
다만, 아래 로직만 제외하고 search의 타입이 뭐가 되야할지 2분 정도 고민한 것 같다.
val search: SearchRemoteDatasource by lazy {
// retrofit.create(<T>) T 인터페이스의 구현체 search를 반환한다.
retrofit.create(SearchRemoteDatasource::class.java)
}
'Android' 카테고리의 다른 글
SharedPreferences, Room 사용법 (0) | 2024.05.08 |
---|---|
ch 6주차 정리 (1) | 2024.05.01 |
앱 개발 숙련 과제 후기 (1) | 2024.04.18 |
Retrofit2 (2) | 2024.04.15 |
Fragment의 정의, 사용법 및 데이터 전달 방식 (1) | 2024.04.12 |