Skip to content

2. Data

Moony-H edited this page Mar 27, 2023 · 9 revisions

data layer는 데이터를 가져오는 역할을 합니다.

안드로이드에선 보통 Room이나 Retrofit 라이브러리를 사용하여 데이터를 로컬, 서버에서 가져옵니다.

저는 이 프로젝트에서 domain layer에 의존성을 가진 후 domain에 정의된 데이터 타입을 참조하여 override합니다.



1. Convention

접미어로 어떤 의미의 클래스인지를 구분하였습니다.

이 모듈의 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의 인스턴스를 주입받습니다.



이와 같이 구성한 패키지는 아래와 같습니다.



스크린샷 2023-03-02 시간: 12 03 00



2. 사용할 데이터와 실제 데이터 구분

api에서 제공하는 데이터와 실제 프로젝트에서 사용하는 데이터는 다를 수 있습니다.

그래서 보통 이 중간 단계에 데이터 변환기를 놓는 프로젝트도 존재하는데요,

저는 크게 데이터가 변경 되는게 없었습니다.

다만 추가로 어떤 데이터가 사용될 지 모르기에 api에서 제공하는 모든 데이터를 담아 놓는 Item클래스에

domain layer에 있는 실제 사용할 데이터 타입 Info를 상속시키고,

getter를 정의하여 조금씩 가공하였습니다.

아래는 이의 예시입니다.



스크린샷 2023-03-02 오전 11 27 20

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
        }
    }
}

스크린샷 2023-03-02 시간: 11 39 37



Body에 Header라는 데이터까지 가지고 있는 아이러니한 상황 입니다.

저는 이러한 데이터 타입을 고려하지 않고 domain에서는 아래와 같이 정의하였습니다.



스크린샷 2023-03-02 시간: 12 07 08

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로 구분된 데이터 타입을 사용할 수 있게 되었습니다.



3. Retrofit 반환 타입 변경을 위한 CallAdapter

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로 변형할 것인지에 대한 함수를 아래와 같이 작성합니다.



스크린샷 2023-03-02 시간: 16 35 14

    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 합니다.



스크린샷 2023-03-02 시간: 16 28 06

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을 반환해 줍니다.



스크린샷 2023-03-02 시간: 16 38 52

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를 반환해 줍니다.



스크린샷 2023-03-02 시간: 16 42 08

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()
    }
}


저는 DI 툴로 Hilt를 사용하였습니다.

data layer에서는 Provider를 사용하지 않고 App Module에서 사용합니다.

즉 아래와 같이 App Module에서 Retrofit에 커스텀으로 정의한 ApiResponseCallAdapterFactory를 등록합니다.



스크린샷 2023-03-02 시간: 18 25 48

package com.moonyh.metabus.di


@Qualifier
annotation class ArrivalInfoRetrofit

@Qualifier
annotation class CityInfoRetrofit

@Qualifier
annotation class StationInfoRetrofit

@InstallIn(SingletonComponent::class)
@Module
class RetrofitModule {

    @ArrivalInfoRetrofit
    @Singleton
    @Provides
    fun providesArrivalInfoRetrofit(): Retrofit {
        val okHttpClient = OkHttpClient().newBuilder()
            .connectTimeout(TimeOut.TIMEOUT_CONNECT, TimeUnit.SECONDS)
            .readTimeout(TimeOut.TIMEOUT_READ, TimeUnit.SECONDS)
            .writeTimeout(TimeOut.TIMEOUT_WRITE, TimeUnit.SECONDS)
            .build()

        return Retrofit.Builder()
            .baseUrl(Url.ARRIVAL_INFO_SERVICE_URL)
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
            .build()
    }

    @CityInfoRetrofit
    @Singleton
    @Provides
    fun providesCityInfoRetrofit(): Retrofit {
        val okHttpClient = OkHttpClient().newBuilder()
            .connectTimeout(TimeOut.TIMEOUT_CONNECT, TimeUnit.SECONDS)
            .readTimeout(TimeOut.TIMEOUT_READ, TimeUnit.SECONDS)
            .writeTimeout(TimeOut.TIMEOUT_WRITE, TimeUnit.SECONDS)
            .build()

        return Retrofit.Builder()
            .baseUrl(Url.CITY_INFO_SERVICE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(okHttpClient)
            .addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
            .build()
    }

    @StationInfoRetrofit
    @Singleton
    @Provides
    fun providesStationInfoRetrofit(): Retrofit {
        val okHttpClient = OkHttpClient().newBuilder()
            .connectTimeout(TimeOut.TIMEOUT_CONNECT, TimeUnit.SECONDS)
            .readTimeout(TimeOut.TIMEOUT_READ, TimeUnit.SECONDS)
            .writeTimeout(TimeOut.TIMEOUT_WRITE, TimeUnit.SECONDS)
            .build()

        return Retrofit.Builder()
            .baseUrl(Url.STATION_INFO_SERVICE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(okHttpClient)
            .addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
            .build()
    }
}


이렇게 Data Layer의 구성을 살펴 보았습니다.

출처: https://proandroiddev.com/modeling-retrofit-responses-with-sealed-classes-and-coroutines-9d6302077dfe

Clone this wiki locally