-
Notifications
You must be signed in to change notification settings - Fork 0
2. Data
data layer는 데이터를 가져오는 역할을 합니다.
안드로이드에선 보통 Room이나 Retrofit 라이브러리를 사용하여 데이터를 로컬, 서버에서 가져옵니다.
저는 이 프로젝트에서 domain layer에 의존성을 가진 후 domain에 정의된 데이터 타입을 참조하여 override합니다.
접미어로 어떤 의미의 클래스인지를 구분하였습니다.
이 모듈의 Convention은 아래와 같습니다.
- Item
공공Api에서 주는 데이터의 단위입니다.
보통 list로 구성되어있으며 domain의 Info를 상속 받은 dataclass입니다.
- ResponseData
공공 Api의 응답 데이터입니다.
위의 Item 뿐 아니라 메시지, 페이지 번호, 총 페이지 수 등 여러 필요한 데이터도 포함되어 있습니다.
domain layer의 ApiBody를 상속받는 클래스입니다.
- Service
Retrofit의 메소드를 정의하는 Interface입니다.
추후에 이야기 할 CallAdapter를 등록해 놓아서 domain layer에 직접 정의해 놓은 ApiResponse를 바로 반환합니다.
- Source
데이터 제공을 담당합니다. Interface로 작성하였으며, 이를 상속 받는 Impl클래스로 이루어져 있습니다.
- RepositoryImpl
domain layer의 Repository 클래스를 상속 받는 클래스 입니다.
설계만 들어가있는 Interface인 Repository를 위의 Source를 사용하여 구현하는 클래스 입니다.
Hilt의 @Inject로 Source의 인스턴스를 주입받습니다.
이와 같이 구성한 패키지는 아래와 같습니다.

api에서 제공하는 데이터와 실제 프로젝트에서 사용하는 데이터는 다를 수 있습니다.
그래서 보통 이 중간 단계에 데이터 변환기를 놓는 프로젝트도 존재하는데요,
저는 크게 데이터가 변경 되는게 없었습니다.
다만 추가로 어떤 데이터가 사용될 지 모르기에 api에서 제공하는 모든 데이터를 담아 놓는 Item클래스에
domain layer에 있는 실제 사용할 데이터 타입 Info를 상속시키고,
getter를 정의하여 조금씩 가공하였습니다.
아래는 이의 예시입니다.

data class BusInfoItem(
@SerializedName("nodeid")
val _stationId: String = "",
@SerializedName("nodenm")
val _stationName: String = "",
@SerializedName("routeid")
val _id: String = "",
@SerializedName("routeno")
val _number: String = "",
@SerializedName("routetp")
val _lineType: String = "",
@SerializedName("arrprevstationcnt")
val _remainStation: Int = -1,
@SerializedName("arrtime")
val _remainTimeSec: Int = -1,
@SerializedName("vehicletp")
val _busType: String = "",
@SerializedName("endnodenm")
val _endStation: String = "",
@SerializedName("startnodenm")
val _startStation: String = "",
) : BusInfo {
override val id
get() = _id
override val number
get() = _number
override val lineType
get() = _lineType
override val endStation
get() = _endStation
override val startStation
get() = _startStation
override val remainStation
get() = _remainStation
override val remainTimeSec
get() = _remainTimeSec
override val busType
get() = _busType
}이렇게 데이터를 구성하고 Item이라는 데이터 타입을 사용하지 않습니다.
앱 내부에서는 상속 받은 Info 타입으로만 사용한다면 api 데이터 타입의 변경에 대한 직접적인 의존을 줄일 수 있습니다.
그 다음은 ResponseData 예시입니다.
공공 api에서는 데이터를 구분하지 않고 한데 모아서 주게 됩니다.
{
"response":{
"header":{
"resultCode":"00",
"resultMsg":"NORMAL SERVICE."
},
"body":{
"items":{
"item":[
{
"arrprevstationcnt":17,
"arrtime":1196,
"nodeid":"GGB213000227",
"nodenm":"소하2동행정복지센터.광명동굴길입구",
"routeid":"GGB213000017",
"routeno":2,
"routetp":"일반버스",
"vehicletp":"저상버스"
},
{
"arrprevstationcnt":8,
"arrtime":938,
"nodeid":"GGB213000227",
"nodenm":"소하2동행정복지센터.광명동굴길입구",
"routeid":"GGB208000008",
"routeno":3,
"routetp":"일반버스",
"vehicletp":"저상버스"
},
{
"arrprevstationcnt":4,
"arrtime":348,
"nodeid":"GGB213000227",
"nodenm":"소하2동행정복지센터.광명동굴길입구",
"routeid":"GGB213000005",
"routeno":17,
"routetp":"일반버스",
"vehicletp":"일반차량"
},
{
"arrprevstationcnt":10,
"arrtime":940,
"nodeid":"GGB213000227",
"nodenm":"소하2동행정복지센터.광명동굴길입구",
"routeid":"GGB213000005",
"routeno":17,
"routetp":"일반버스",
"vehicletp":"일반차량"
}
]
},
"numOfRows":100,
"pageNo":1,
"totalCount":12
}
}
}
Body에 Header라는 데이터까지 가지고 있는 아이러니한 상황 입니다.
저는 이러한 데이터 타입을 고려하지 않고 domain에서는 아래와 같이 정의하였습니다.

interface ApiBody<out T : Any, out A : Any> {
val metaData: T
val items: A
}그래서 위의 타입을 상속한 후 getter를 설정하여 아래와 같이 원하는 형태로 변경하였습니다.
data class BusInStationResponseData(
@SerializedName("response")
val response:Response
) : BusInStationInfoBody {
override val metaData: MetaData
get() = object : MetaData {
override val resultCode: String
get() = response.header.resultCode
override val resultMessage: String
get() = response.header.resultMessage
override val nowPageCount: Int
get() = response.body.nowPageCount
override val totalItemCount: Int
get() = response.body.totalPageCount
}
override val items: ArrayList<BusInfoItem>
get() = response.body.items.item
data class Response(
@SerializedName("header")
val header: Header,
@SerializedName("body")
val body: Body
){
data class Body(
@SerializedName("items")
val items: Item,
@SerializedName("numOfRows")
val itemCount: Int,
@SerializedName("pageNo")
val nowPageCount: Int,
@SerializedName("totalCount")
val totalPageCount: Int
) {
data class Item(
@SerializedName("item")
val item: ArrayList<BusInfoItem>
)
}
}
}이로써 제가 원하는 metaData, items로 구분된 데이터 타입을 사용할 수 있게 되었습니다.
Retrofit는 Call과 Response라는 반환 타입을 사용할 수 있습니다.
하지만 저는 직접 domain layer에서 정의한 ApiResponse타입을 앱 내에서 사용해야 합니다.
그리므로 타입 변환이 필요한데, Call을 상속 받고 여러 상황에서 callback되는 함수들을 override 한다면
직접 작성한 ApiResponse를 반환 타입으로 사용할 수 있습니다.
제가 참고한 블로그는 이곳이며 이를 참고하여 만들었습니다.
아래가 그 예시 입니다.
class ApiResponseCall<T : Any>(
private val proxy: Call<T>
) : Call<ApiResponse<T>> {
override fun enqueue(callback: Callback<ApiResponse<T>>) {
proxy.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val networkResult = handleApi { response }
callback.onResponse(this@ApiResponseCall, Response.success(networkResult))
}
override fun onFailure(call: Call<T>, t: Throwable) {
val networkResult = ApiResponse.Exception<T>(t)
callback.onResponse(this@ApiResponseCall, Response.success(networkResult))
}
})
}
override fun execute(): Response<ApiResponse<T>> = throw NotImplementedError()
override fun clone(): Call<ApiResponse<T>> = ApiResponseCall(proxy.clone())
override fun request(): Request = proxy.request()
override fun timeout(): Timeout = proxy.timeout()
override fun isExecuted(): Boolean = proxy.isExecuted
override fun isCanceled(): Boolean = proxy.isCanceled
override fun cancel() { proxy.cancel() }
private fun <T : Any> handleApi(
execute: () -> Response<T>
): ApiResponse<T> {
return try {
val response = execute()
val body = response.body()
if (response.isSuccessful && body != null) {
ApiResponse.Success(body)
} else {
ApiResponse.Error(code = response.code(), message = response.message())
}
} catch (e: HttpException) {
ApiResponse.Error(code = e.code(), message = e.message())
} catch (e: Throwable) {
ApiResponse.Exception(e)
}
}
}먼저 Retrofit의 기본 반환형인 Response를 어떻게 자신이 정의한 ApiResponse로 변형할 것인지에 대한 함수를 아래와 같이 작성합니다.

private fun <T : Any> handleApi(
execute: () -> Response<T>
): ApiResponse<T> {
return try {
val response = execute()
val body = response.body()
if (response.isSuccessful && body != null) {
ApiResponse.Success(body)
} else {
ApiResponse.Error(code = response.code(), message = response.message())
}
} catch (e: HttpException) {
ApiResponse.Error(code = e.code(), message = e.message())
} catch (e: Throwable) {
ApiResponse.Exception(e)
}
}위와 같이 작성한다면 http exception으로 인한 앱이 죽는 현상은 없어질 것입니다.
그 다음 평소에 사용하는 Call 클래스를 상속받고 enqueue를 override 합니다.
그리고 enqueue, execute, clone, timeout, isCanceled 등등 여러 상황에 따른 Callback를 override 합니다.

class ApiResponseCall<T : Any>(
private val proxy: Call<T>
) : Call<ApiResponse<T>> {
//중략...
override fun execute(): Response<ApiResponse<T>> = throw NotImplementedError()
override fun clone(): Call<ApiResponse<T>> = ApiResponseCall(proxy.clone())
override fun request(): Request = proxy.request()
override fun timeout(): Timeout = proxy.timeout()
override fun isExecuted(): Boolean = proxy.isExecuted
override fun isCanceled(): Boolean = proxy.isCanceled
override fun cancel() { proxy.cancel() }
}Response를 커스텀 타입인 ApiResponse로 변환하는 클래스를 작성하였습니다.
이제 이 클래스를 Retrofit에게 알려야 합니다.
아래와 같이 CallAdapter를 상속받은 클래스를 작성하여 adapt 메서드의 반환형으로 ApiResponseCall을 반환해 줍니다.

class ApiResponseCallAdapter (
private val resultType: Type
) : CallAdapter<Type, Call<ApiResponse<Type>>> {
override fun responseType(): Type = resultType
override fun adapt(call: Call<Type>): Call<ApiResponse<Type>> {
return ApiResponseCall(call)
}
}그 다음 아래와 같이 CallAdapter.Factory를 상속 받아 get 메서드를 오버라이드하여 ApiResponseCallAdater를 반환해 줍니다.

class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
if (getRawType(returnType) != Call::class.java)
return null
val callType = getParameterUpperBound(0, returnType as ParameterizedType)
if (getRawType(callType) != ApiResponse::class.java)
return null
val resultType = getParameterUpperBound(0, callType as ParameterizedType)
return ApiResponseCallAdapter(resultType)
}
companion object {
fun create(): ApiResponseCallAdapterFactory = ApiResponseCallAdapterFactory()
}
}저는