umbum
57
2019-06-24 23:30:54 작성 2019-06-26 18:36:30 수정됨
4
289

안드로이드 MVVM 패턴 적용 시 callback 안에서 context가 필요한 경우


안녕하세요. MVVM 패턴을 적용하면서 리팩토링 하던 중 고민이 생겨 질문 드립니다.


AccessTokenRepository.kt

fun requestAccessToken(code: String) {

    val retrofit = Retrofit.Builder()
            .baseUrl("https://github.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()

    val ghService = retrofit.create(GithubService::class.java)
    val request = ghService.requestAccessToken(CLIENT_ID, CLIENT_SECRET, code)


    request.enqueue(object : Callback<AccessTokenResponse> {
        override fun onFailure(call: Call<AccessTokenResponse>, t: Throwable) {
            Log.d(DBG_TAG, "retrofit onFailure: " + t.toString())
        }

        override fun onResponse(call: Call<AccessTokenResponse>, response: Response<AccessTokenResponse>) {
            Log.d(DBG_TAG, response.body().toString())
            if (response.isSuccessful) {
                val access_token = response.body()?.access_token ?: return
                val sharedPreferences = getSharedPreferences("SETTINGS", Context.MODE_PRIVATE)
                val editor = sharedPreferences.edit()
                editor.putString("access_token", access_token)
                editor.apply()
            } else {
                Log.d(DBG_TAG, "|onResponse| isSuccessful is false")
            }
        }
    })
}


이런 식으로 retrofit을 사용해서 AccessToken을 받아오는 코드이고, 이는 비즈니스 로직이라고 생각되어 repository로 분리했습니다. 

그리고 SharedPreference에 값을 삽입하는 것은 안드로이드 API에 종속적인 작업이니 비즈니스 로직 단에 있을 것이 아니라 ViewModel이나 Activity 쪽으로 빼는게 맞지 않을까? 하는 생각이 드는데요. (게다가 getSharedPreferences를 쓰려면 Activity의 context를 여기까지 끌고 와야 한다는 점도 부자연스럽다고 느껴집니다.)

그래서 이 코드에서는 sharedPreferences를 호출하지 않고, ViewModel이나 Activity로 access_token을 전달하고 거기서 sharedPreferences를 사용하고 싶습니다. 


문제는 레트로핏을 사용하면 반드시 비동기로 요청해야 해서 응답이 돌아올 때 onResponse라는 콜백이 실행된다는 점입니다. 그래서 위 코드에서 return을 해봐야 제대로 access_token이 리턴되지 않습니다.

원래 MVVM은 LiveData를 리턴하거나 data binding을 사용하여 이 문제를 해결하는 것으로 알고있습니다...(onResponse에서 뷰와 연결된 변수가 수정되면 뷰도 변경되는 식...)

그러나 access_token은 단순히 SharedPreference에 집어넣기만 할거라 따로 연결된 뷰가 없습니다. 


이 문제를 어떻게 깔끔하게 해결할 수 있을까요? 

고수 님들의 가르침 부탁드립니다!!$!!


0
0
  • 답변 4

  • devcrema
    212
    2019-06-25 09:37:10

    SharedPreference도 하나의 저장하는 수단이라고 보시면 repository에서 제어하는 것이 맞다고 생각합니다.

    안드로이드에서 repository역할은 data가 local인지 remote인지 어디에 붙어있는 지를 추상화하는 역할이니까요.

    즉 뷰모델단부터는 엑세스토큰을 서버통신으로 가져오는지, 저장된 프리퍼런스에서 가져오는지, 로컬디비에서 가져오는지 상관하지 않아도 되는 거죠.

    그리고 SharedPreference를 어플리케이션 컨텍스트로도 사용가능하니 굳이 액티비티의 context를 불러오지 않아도 됩니다.

    1
  • umbum
    57
    2019-06-26 18:48:20

    @devcrema


    안녕하세요. 댓글 감사합니다. 말씀하신 부분이 죄송하지만 정확히 이해가 안됩니다.

    SharedPreference를 repository에서 제어해야 한다는 의견에는 동의합니다. 근데 retrofit을 이용해서 AccessToken을 가져오는 로직이랑, SharedPreference를 이용해서 AccessToken을 저장하고 받아오는 로직은 분리해야 맞지 않을까? 라는 생각도 듭니다. 두 개를 함께 놓았을 때는 아마 코드가 이런 식으로 될 것 같은데요...

    fun getAccessToken() {
        val accessToken = sharedPreferences.getString("access_token", null)
        if (accessToken == null) {
            requestAccessToken(code)
        }
        return accessToken???????
    }

    이렇게 현재 AccessToken이 없는 상황이라면 request를 해서 AccessToken을 서버로부터 받아와서 SharedPreference에 저장하기 위해 requestAccessToken()을 호출해야 할겁니다.

    근데 문제는 requestAccessToken 안에서 retrofit을 이용해 AccessToken을 받아오는 로직은 비동기로 되어있어서, getAccessToken()함수를 호출한다고 항상 AccessToken을 반환하도록 할 수는 없을거같습니다.


    그래서 제 생각에는 

    fun viewModel() {
        val accessToken = retrofit을 사용해 AccessToken을 받아오는 코드
        repository.setAccessToken(accessToken)
    }

    이렇게 되는게 좋지 않을까 하는데요.


    제 생각이 틀렸을까요?


    0
  • devcrema
    212
    2019-06-27 01:35:02

    프리퍼런스같은 local데이터가 같은 데이터 영역에 속하는 remote데이터(레트로핏)을 호출하면 혼동이 있을 수도 있을 것 같네요.

    viewModel -> repository -> 프리퍼런스 체크 (토큰있는지) -> 있다면 반환하고 없다면 retrofit으로 요청 및 받아서 프리퍼런스 저장까지가 일반적인 구현이라고 생각됩니다.

    프리퍼런스를 관리해주는 객체가 토큰 저장, 프리퍼런스 토큰 저장여부 확인

    레트로핏은 remote 토큰 받아와주는 역할

    리포지터리는 로그인, 가입 등 추상화된 역할

    뷰모델은 이벤트를 받아 적절한 리포지터리 호출이 적당해 보입니다.


    정답은 없겠지만 저의 경우에는 구체적으로 정리하면

    위 뷰모델로직에서

    repository.isLogin() 같이 엑세스 토큰 체크해주고

    login이 안되있으면 (토큰이 없으면) repository.login(String username, String password, Callback callback) 같이 구현해서 호출하고 리포지터리 안에서 프리퍼런스에 저장하고 성공하면 callback.onSuccess(); 실패 시, callback.onFailed() 이런식으로 구현했어요.

    이렇게 했을 경우 장점은 만약 엑세스토큰 이외에 다른 로직으로 로그인을 구현할경우에도 뷰모델 수정없이 리포지터리만 수정하면 될 것 같네요.

    비동기의 경우에는 레트로핏이 콜백으로 돌려주듯이 리포지터리도 똑같이 callback으로 구현하면 됩니다.


    콜백으로 추상화하는 부분은 구글 샘플코드 참조하시면 좋을 것 같네요.

    https://github.com/googlesamples/android-architecture/blob/todo-mvvm-databinding/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/TasksRepository.java

    1
  • umbum
    57
    2019-06-27 16:16:25

    @devcrema


    감사합니다 :) 도움 많이 되었습니다. 말씀하신 부분도 찾아보고 다른 것도 찾아보다 Rx를 쓰는게 제일 깔끔한 것 같아서 Rx를 써서 해결했습니다 ㅎㅎ

    0
  • 로그인을 하시면 답변을 등록할 수 있습니다.