Android

(Android) Repository Pattern 의 이해

돗개진 2024. 10. 1. 16:05

 

Repository Pattern

: Data Layer 를 앱의 나머지 부분에서 분리하는 디자인 패턴, Data Layer 는 UI 와 별도로 앱의 데이터와 비지니스 로직을 처리하는 레이어이다. 다른 레이어는 Data Layer 에서 제공하는 API 를 통해서 데이터에 엑세스가 가능하다.

 

Data Layer 가 다른 Layer 와 분리된 구조도

 

 

아래 그림은 안드로이드에서 권장하는 아키텍처이다. 아래 다이어그램에선 아키텍처의 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/

 

안드로이드 Repository 패턴은 무엇인가

 

ppeper.github.io

https://velog.io/@ilil1/Repository-Pattern-%EC%9D%B4%EB%9E%80