Android 공부

감자튀김의 다시 쓰는 Android MVVM(3) - Repository

-1. 이전 글


2019/03/17 - [Android 공부] - 감자튀김의 다시 쓰는 Android MVVM(1) - DI

 

감자튀김의 다시 쓰는 Android MVVM(1) - DI

-1. MVVM 메인 가이드: https://developer.android.com/jetpack/docs/guide 구글의 앱 아키텍처 가이드 개념을 기본으로 해서 프로젝트를 관리해야 한다고 생각합니다. MVVM을 사용하기 이전에 해당 자료를 통해..

gamjatwigim.tistory.com

2019/03/27 - [Android 공부] - 감자튀김의 다시 쓰는 Android MVVM(2) - DI

 

감자튀김의 다시 쓰는 Android MVVM(2) - DI

-1. 이전 글 2019/03/17 - [Android 공부] - 감자튀김의 다시 쓰는 Android MVVM(1) - DI 0. 서론 해당 프로젝트는 Upbit거래 시세를 알아낼 수 있는 앱을 만드는 것이 목표입니다. 이전에 DI를 사용한 구조에서..

gamjatwigim.tistory.com

0. 서론

- Repository 모듈은 데이터 작업을 처리합니다. 이를 통해 지속 모델, 웹 서비스, 캐시 등 다양한 데이터 중재소 역할을 합니다.

- Repository 패턴이란, 앞서 잠깐 얘기했지만, ViewModel을 통해서 LiveData를 관리하게 되면 앱이 커지면서 유지보수가 어려워질 수 있고, 해당 ViewModel에 많은 책임을 부여하면 관심사 분리의 원칙에 위배됩니다.

- 위와 같은 그림으로 ViewModel에 각종 Repository를 부여해 Room이나 Retrofit등을 붙여서 사용합니다. DI를 사용할 경우에는 각 객체를 외부에서 의존성을 주입해줘서 코드를 절약할 수 있습니다.

 

1. Retrofit

Retofit의 홈페이지에 들어가보면 A type-safe HTTP client for Android and Java.

이라고 레트로핏을 소개합니다. type-safe는 뭐고, HTTP client는 무엇일까요?

레트로핏 홈페이지의 배너

type-safe는 타입을 판별해서 컴파일시에 문제를 해결할 수 있다는 것을 의미합니다.

HTTP Client는 URI로 식별되는 리소스에 HTTP 요청을 보내고, 응답을 받기 위한 클래스라고 합니다.

내가 생각하는 Repository와 DI

위의 MVVM 구조에서 Repository를 확대시켜보면 위와 같은 그림이 아닐까 싶다. DI로 관리되어 NetModule을 만들고, 해당 NetModule에서 OkHttpClient, Retrofit객체를 생성, ApiService등의 역할을 하고, 이것을 Repository 객체에서 사용하는 구조라고 생각한다.

@Module
class NetModule {
    @Provides
    @Singleton
    fun provideHttpLogging(): OkHttpClient {
        val logging = HttpLoggingInterceptor()
        logging.level = HttpLoggingInterceptor.Level.BODY
        return OkHttpClient.Builder()
            .addInterceptor(logging)
            .build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit
            .Builder()
            .baseUrl(BaseUrl.UPBIT.url)
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .addConverterFactory(GsonConverterFactory.create())
            .client(okHttpClient)
            .build()
    }

    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): UpbitAPI = retrofit.create(UpbitAPI::class.java)

    @Provides
    @Singleton
    fun provideJJwtHelper() : JJwtHelper = JJwtHelper()
}

해당 프로젝트에서는 NetModule이라는 모듈을 만들어서 다른 모듈에 전달해주는 구조로 되어있습니다. 특히, Singleton으로 하여, 객체들을 관리하고 있습니다. 우리가 일반적으로 Retrofit을 사용하는 것과 같이 HttpLogging과 같은 OkHttp의 클라이언트를 생성하고, 해당 클라이언트를 Retrofit 객체로 만들고, 사용할 인터페이스 내용을 provide하는 구조로 되어있습니다. 또한 JJwtHelper라는 JWT를 보내는 객체 클래스를 NetModule에서 함께 관리해주고 있습니다.

//아무 것도 없이 경로에 접근할 때
@GET("v1/market/all/")
fun getMarketInfo(): Single<List<MarketCode>> 

//Header를 입력해야할 때
@GET("v1/accounts/")
fun getMyAccount(@Header("Authorization") jwtToken: String): Single<List<AccountCoin>>

//Query를 입력해야할 때
@GET("v1/orderbook/")
fun getOrderBook(@Query("markets") markets: String): Single<List<OrderBook>>

우연찮게 앱을 만들면서 사용한 Retrofit들이 단순 경로만 입력할 때, Header와 함께 입력할 때, Query를 입력할 때와 같이 다양한 예제가 만들어졌다.

@GET(url)이라는 문장을 시작으로하는 Retrofit은 GET방식으로 url을 BaseUrl이후로 접근하겠다는 것을 의미하고, 그것을 레트로핏을 통해 편하게 wrapping해서 사용하는데, getMarketInfo()를 호출하게 되면 RxJava의 Single로 하여금, List단위의 MarketCode를 얻어온다는 내용이다.

2. Upbit API & JWT 

 

https://docs.upbit.com/v1.0.2/reference

 

Upbit API입니다. 특히, 우리가 여기서 다룰 것은 

 

Upbit 전체 계좌 조회

전체 계좌를 조회하는 방법입니다. 여기에선 jwt라는 개념을 통해서 payload를 전달해주고, token을 API에 실어서 headers에 올려줘야 합니다.

JWT 구조

이와 같은 구조가 필요하며, Retrofit의 Header에 데이터를 암호화해서 올리기 위해서 JWT를 통해서 암호화를 하는 로직이 필요합니다.

JWT의 소개

JWT에 대한 소개는 RFC 7519를 기반으로한 JSON 웹 토큰이라고 합니다. 저희는 업비트의 API에서 payload를 jwt 라이브러리를 통해서 만들고, 해당 토큰을 레트로핏의 헤더에 담아 주셔야 합니다.

api 'io.jsonwebtoken:jjwt-api:0.10.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.10.5'
    runtimeOnly('io.jsonwebtoken:jjwt-orgjson:0.10.5') {
        exclude group: 'org.json', module: 'json' //provided by Android natively
    }

저는 jwt 라이브러리를 사용하기 위해서 해당 라이브러리를 사용하고 있으며,

https://github.com/jwtk/jjwt

 

jwtk/jjwt

Java JWT: JSON Web Token for Java and Android. Contribute to jwtk/jjwt development by creating an account on GitHub.

github.com

에서 구체적인 내용을 확인해보세요.

fun convertJWS(accessKey: String, secretKey: String): String =
        Jwts.builder()
            .claim("access_key",accessKey) //UPBIT API의 access key
            .claim("nonce", Date(System.currentTimeMillis())) //nonce를 만들기 위해서 현재시간을 뽑음.
            .signWith(SignatureAlgorithm.HS256, secretKey.toByteArray(Charsets.UTF_8))//HS256알고리즘으로 UTF8형태로 뽑아냅니다.
            .compact()//토큰을 만듦

convertJWS는 JavaWebToken으로 만드는 것을 의미하고, accessKey를 API로부터 얻어오고,

현재시간을 출력해서 각종 변수값으로 저장해서 HS256알고리즘으로 해당 Key를 변환해줍니다.

val jwtToken = jjwtHelper.convertJWS(BuildConfig.PasswordKey, BuildConfig.SecretKey)
        val jwtParam = "Bearer $jwtToken"
        val getMyAccountDisposable = upbitRepositoryImpl.getMyAccount(jwtParam)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .filter { accountCoinList -> !accountCoinList.isNullOrEmpty() }
            .subscribe({ accountCoinList: List<AccountCoin> ->
                updateMyWallet(accountCoinList)
            }, {
                Log.d("error", it.message)
            })

convertJWS매소드를 통해서 PaswordKey와 SecretKey를 넣어주고, jwtParam으로 Bearer $jwtToken으로 token을 삽입해줍니다.

@GET("v1/accounts/")
fun getMyAccount(@Header("Authorization") jwtToken: String): Single<List<AccountCoin>>

그러면 해당 코드와 같이 jwtToken을 Header에 넣어줄 수 있습니다.

이를 통해서 계좌에 대한 정보를 UpbitAPI나 혹은 그 외의 API로 받아올 수 있습니다.

 

3. 레파지토리 패턴 사용

interface UpbitRepository {
    fun getMarketCodeAll(): Single<List<MarketCode>>
    fun getOrderBook(market: String): Single<List<OrderBook>>
    fun getMyAccount(jwtToken: String): Single<List<AccountCoin>>
}

class UpbitRepositoryImpl @Inject constructor(val upbitAPI: UpbitAPI) : UpbitRepository {
    override fun getMyAccount(jwtToken: String): Single<List<AccountCoin>> {
        return upbitAPI.getMyAccount(jwtToken)
    }

    override fun getMarketCodeAll(): Single<List<MarketCode>> {
        return upbitAPI.getMarketInfo()
    }

    override fun getOrderBook(market: String): Single<List<OrderBook>> {
        return upbitAPI.getOrderBook(market)
    }
}

UpbitRepository를 만들었다면, 이를 통해 얻는 이점은 무엇일까?

- 만약 ViewModel에 해당 로직이 있다면, ViewModel은 너무 많은 역할을 하게 됩니다.

- 만약 해당 로직이 다른 ViewModel에도 사용된다면, 해당 로직을 또 추가해줘야 합니다.

 위와 같은 이유로 Repository 패턴을 사용합니다. Android Developer에서는 '관심사 분리'라는 단어를 사용하는데, 이를 통해서 ViewModel의 부담을 줄여주며, Repository 패턴을 재활용해준다고 합니다.

특히, MVVM 패턴, 뿐만 아니라 개발에서는 '관심사 분리'를 통해서 의존성을 낮춰서 재활용 해야한다고 합니다.

@Module
class RepositoryModule{
    @Provides
    @Singleton
    fun provideUpbitRepository(upbitAPI: UpbitAPI) : UpbitRepositoryImpl= UpbitRepositoryImpl(upbitAPI)
}
@Module
abstract class MainViewModelModule {
    @Module
    companion object {
        @JvmStatic
        @Provides
        @ViewModelScope
        fun provideMainViewModel(upbitRepositoryImpl:UpbitRepositoryImpl, jjwtHelper:JJwtHelper): MainViewModel {
            return MainViewModel(upbitRepositoryImpl, jjwtHelper)
        }
    }
}

UpbitRepository패턴을 극대화 시키려면, DI와 함께 사용해야 합니다.

- 만약 DI가 없다면, Repository 패턴을 사용하는 것에 있어서 객체를 계속 생성하는 문제

- 만약 DI가 없다면, 라이프사이클에 따른 객체를 관리하는 문제

 위와 같은 이유로 Repository는 DI와 함께 사용해야 한다고 합니다. 객체를 생성하는 문제와 라이프사이클에 의한 문제를 해결할 수 있는 이점이 있다고 합니다.

 하지만, 최근에 면접을 보면서 만약에 DI를 사용안하고, 단순

"싱글톤으로 해당 객체를 관리한다면 문제가 없지 않은가?"라고 했는데, 해당 질문을 통해서 아직도 DI를 사용할 때 사용하지 않을 때와 비교할 때의 이점을 설명하지 못 할 정도로 부족한 점이 많다고 생각한다.

 

4. 우오옹?

 초보 개발자의 입장에서 MVVM을 조금이나마 이런 방식으로 사용한다고 작성하는 글임을 명시하면서, 해당 내용들을 다양한 레퍼런스와 함께 읽어서 옳바른 MVVM을 만들면 좋겠다.