Repository Pattern
: Data Layer 를 앱의 나머지 부분에서 분리하는 디자인 패턴, Data Layer 는 UI 와 별도로 앱의 데이터와 비지니스 로직을 처리하는 레이어이다. 다른 레이어는 Data Layer 에서 제공하는 API 를 통해서 데이터에 엑세스가 가능하다.
아래 그림은 안드로이드에서 권장하는 아키텍처이다. 아래 다이어그램에선 아키텍처의 Activity / Fragment (= UI Layer) 가 직접 Data 에 접근하지 않고 Repsitory 를 통하여 데이터를 가지고 오는 것을 볼 수 있다.
해당 그림은 안드로이드에서 권장하는 아키텍처이다. 다이어그램의 순서를 살펴보면 아키텍처의 Activity / Fragment 와 같은 UI 레이어에서 직접 Data 에 접근하지 않고 Repository 를 통하여 데이터를 가져오는 것을 확인할 수 있다.
이 부분이 Repository Pattern 의 핵심이며 Repository는 UI에서 사용할 데이터들을 가져올 수 있도록 접근하는 LocalDataSource (앱 내부 데이터 = Room), RemoteDataSource (서버 데이터 = Retrofit) 를 캡슐화하여 사용하는 것이다.
Repository 는 데이터의 출처 (Local / Remote) 와 상관없이 동일한 인터페이스로 데이터에 접근할 수 있도록 만든 것이다.
Repository Pattern 을 사용하는 이유
기존의 안드로이드에서 MVC 패턴으로 개발을 하게 되면 View (Activity / Fragment) 에 모~든 코드를 작성해야 했다. 하지만 이렇게 하게 되면 많은 단점들이 존재한다.
해당 단점은 View <-> Model 사이에 의존성이 발생하여 View 의 UI 갱신을 위해 Model 을 직/간접적으로 참조하여 Activity / Fragment 의 크기가 커지고 로직들이 복잡해질수록 유지보수가 힘들어진다는 큰 문제점이 발생한다.
이러한 단점을 해결하기 위해 사용되는 것이 바로 "Repository Pattern 이며 해당 패턴이 가지고 있는 장점"들에 대해서 알아보자
1. 도메인과 연관된 모델을 가져오기 위해서 필요한 DataSource 가 Presenter 계층에서는 알 필요가 없다 (필요한 DataSource가 몇 개든 사용될 Data 만 가져오면 됨) => DataSource 를 새롭게 추가하는 것에 대한 부담 X
2. DataSource 의 변경이 되더라도 다른 계층에는 영향이 없다
3. 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
}
}
})
}
}
- 참고
https://ppeper.github.io/android/repository-pattern/
https://velog.io/@ilil1/Repository-Pattern-%EC%9D%B4%EB%9E%80
'Android' 카테고리의 다른 글
(Android) 의존성 주입(Dependency Injection) 정의 및 예시 + Dagger-Hilt 활용 (1) | 2024.09.02 |
---|---|
(Android) - MVC, MVP, MVI 패턴의 이해 및 장단점 (0) | 2024.08.24 |
(Android / Kotlin) - Room 을 사용한 데이터 유지 (0) | 2024.08.16 |
(Android) - MVVM 패턴 (ViewModel, LiveData, Observer) (0) | 2024.06.26 |
(Android / Kotlin) - SharedPreferences (2) | 2024.04.30 |