3 분 소요

Repository

이전의 포스팅에서 안드로이드 MVVM 디자인 패턴에 대해서 알아보면서 아주 잠깐 Repository의 개념 대해서 알아보았다. 이번에 새로운 프로젝트에 Repository를 적용하면서 공부하였던 내용들을 정리해 보려고 한다.

단어의 의미보면 파악할 수 있듯이 Repository는 필요한 데이터들을 저장하고 있는 저장소라고 생각하면 된다. 그러면 안드로이드에서는 Repository가 어떻게 사용되는가??

안드로이드 아키첵처 컴포넌트

위의 그림이 안드로이드에서 권장하는 아키텍처이다. 위 다이어그램을 보면 아키텍처의 Activity / Fragment 즉 UI 레이어에서 직접 Data에 접근하지 않고 Repository 를 통하여 데이터를 가지고 오는것을 볼 수 있다.

Repository는 UI에서 사용할 데이터들을 가져올 수 있도록 접근하는 LocalDataSource(앱 내부 데이터 ex)Room), RemoteDataSource(서버데이터 ex)Retrofit) 을 캡슐화하여 사용하는 것이다.

Repository는 데이터의 출처(local/remote)와 상관없이 동일한 인터페이스로 데이터에 접근할 수 있도록 만든것이다.

Repository 패턴을 사용하는 이유?

이전에 안드로이드에서 MVC패턴으로 개발을 하게되면 View(Activity/Fragment)에 모든 코드를 작성을 하여 많은 단점들이 존재하였었다.

View와 Model사이에 의존성이 발생하여 View의 UI 갱신을 위해 Model을 직/간접적으로 참조하여 Activity/Frament의 크기가 커지고 로직들이 복잡해 질 수록 유지보수가 힘들어진다.(처음 안드로이드 프로젝트를 진행하였을때 이러한 문제에 직면하였었다..😅)

그렇다면 Repository 패턴을 사용하게 되면 얻는 이점은 무었일까?

  • 도메인과 연관된 모델을 가져오기 위해서 필요한 DataSource가 Presenter 계층에서는 알 필요가 없다(필요한 DataSource가 몇개든 사용될 data만 가져오면 된다)
    • 따라서 DataSource를 새롭게 추가하는 것도 부담이 없다.
  • DataSource 의 변경이 되더라도 다른 계층에는 영향이 없다.
  • Client는 Repository 인터페이스에 의존하기 때문에 테스트 하기 용이하다.

🔔 Repository는 결국 Presenter 계층과 Data 계층간의 Coupling을 느슨하게 만들어 주는것이다.

Repository 패턴을 사용한다는 것은 DataSource 즉, Data Layer를 캡슐화한다는 의미이다.

Repository를 추가하여 View에서 데이터를 참조하는 흐름을 보면 다음과 같다.

  1. View -> ViewModel로만 데이터를 가져온다.

  2. ViewModel -> Repository로 데이터 접근한다.

  3. Repository -> DataSource(local/remote)로 부터 데이터를 요청한다.


앱에서 회원가입을 구현 해야한다고 했을때 Repository 패턴을 적용하여 요청을 보내는 예시를 보면 다음과 같다.(Hilt 의존성 주입 사용)

먼저 UserRemoteDataSource로 부터 서버로 회원가입 요청을 보내는 함수를 만들어준다.

// 서버로 부터 데이터 요청하는 class 인터페이스화
interface UserRemoteDataSource {
    suspend fun signInUser(user: User): Response<ResponseUser>
    .
    .
}

// 인터페이스 UserRemoteDataSource를 구체화할 Impl 클래스
class UserRemoteDataSourceImpl(
    private val userService: UserService
): UserRemoteDataSource {
    override suspend fun signInUser(user: User): Response<ResponseUser> {
        return userService.signInUser(user)
    }
    .
    .
}

UserRepository는 UserRemoteDataSource를 참조하여 서버로 회원가입 요청을 보낸다.

interface UserRepository {
    suspend fun signInUser(user: User): APIResponse<ResponseUser>
    .
    .
}

class UserRepositoryImpl(
    private val userRemoteDataSource: UserRemoteDataSource
): UserRepository {
    // 회원가입 요청이 성공하면 Success에 데이터를 실어서 ,실패하면 Error에 message 리턴
    override suspend fun signInUser(user: User): APIResponse<ResponseUser> {
        val response = userRemoteDataSource.signInUser(user)
        if (response.isSuccessful) {
            response.body()?.let { result ->
                return APIResponse.Success(result)
            }
        }
        return APIResponse.Error(response.message())
    }
    .
    .
}

Retrofit으로 서버의 요청에 대한 응답을 받으면 return값을 APIResponse 를 통하여 State와 data를 매핑해 주었다.

sealed class APIResponse<T>(
    val data: T? = null,
    val message: String? = null
) {
    class Success<T>(data: T? = null): APIResponse<T>(data)
    class Loading<T>(data: T? = null): APIResponse<T>(data)
    class Error<T>(message: String, data: T? = null): APIResponse<T>(data, message)
}

UserViewModel은 UserRepository만을 이용해서 데이터에 접근하고 LiveData를 관찰하는 observer에게 값을 넘겨준다.

@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository
): ViewModel() {

    // request state
    val state: MutableLiveData<APIResponse<ResponseUser>> = MutableLiveData()

   fun signInUser(user: User) {
       // 서버의 요청에대한 response가 오기전에는 Loading 상태
       state.value = APIResponse.Loading()
       viewModelScope.launch(Dispatchers.IO)  {
           val response = repository.signInUser(user)
           try {
               if (response.data != null) {
                   state.postValue(response)
               } else {
                   state.postValue(APIResponse.Error(response.message.toString()))
               }
           } catch (e: Exception) {
               state.postValue(APIResponse.Error(e.message.toString()))
           }
       }
   }
   .
   .
}

사용할 Activity에서 직접 서버로 요청을 하지 않고 UserViewModel을 사용하여 회원가입을 요청(requestSignIn)하고 LiveData를 observe한다.

@AndroidEntryPoint
class LogInActivity : AppCompatActivity() {
    private val userViewModel: UserViewModel by viewModels()
    private lateinit var binding: ActivityLogInBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        binding = ActivityLogInBinding.inflate(layoutInflater)
        super.onCreate(savedInstanceState)
        setContentView(binding.root)


        with(binding) {
            // 로그인 요청
            buttonLogin.setOnClickListener {
                // user의 input 값이 들어갔다고 가정
                    val userLogin =
                        UserLogin(editTextLoginId.text.toString(), editTextLoginPwd.text.toString())
                    requestLogin(userLogin)
                }
            }
        }
    }

    private fun requestLogin(userLogin: UserLogin) {
        userViewModel.getTokenRequest(userLogin)
        userViewModel.isLogin.observe(this@LogInActivity, Observer { response ->
            when (response) {
                is APIResponse.Success -> {
                    // success code
                }
                is APIResponse.Error -> {
                    // error code
                }
                is APIResponse.Loading -> {
                    // loading code
                }
            }
        })
    }
}

References

맨 위로 이동 ↑

댓글남기기