본문 바로가기

Android Architecture

Android Repository 패턴

👩‍💻 오늘의 할 일

멋쟁이 사자처럼에서 진행한 팀 프로젝트를 진행하면서 처음으로 Repository 패턴을 사용해 봤습니다. 개인 프로젝트에서도 Repository를 사용하긴 했지만 제대로 공부하다 보니까 제가 잘못 사용 중인걸 알게 되었습니다. 앞으로 클린 아키텍처를 공부하는 데 있어서 큰 발판이 된 경험이 되었기 때문에 한번 정리해 보겠습니다.

👩‍🏫 Repository 패턴이란?

Repository는 ViewModel에서 DataSource에 접근할 때 직접 DataSource에 접근하는 것이 아니라  Repository를 통해 데이터를 관리하고 접근합니다. 이를 통해서  ViewModel은 단순히 UI 상태(State)를 관리하고 비즈니스 로직을 처리하는 데 집중할 수 있으며, 데이터 액세스와 관련된 부분은 Repository에 위임됩니다.

👩‍🏫 Repository 패턴의 장점

 

 

Repository Pattern은 데이터 소스와의 상호작용을 추상화하여 데이터 접근 로직을 캡슐화  합니다. Repository Pattern 을 사용하는 주된 이유는 데이터 소스와 비즈니스 로직 사이의 결합도를 낮추어, 애플리케이션의 유지보수성과 확장성을 높이기 위함입니다.

 

이로 인해 얻는 다양한 이점을 얻을 수 있습니다.

 

첫 번째는 데이터의 출처(local/remote)와 상관없이 동일한 인터페이스로 데이터에 접근할 수 있습니다.

 

두 번째로는 Presenter계층(View, ViewModel)에선 도메인과 연관된 모델을 가져오기 위해서 필요한 DataSource을 알 수 없고 반대로 Data Layer에선 내부 정보를 감출 수 있습니다.

 

예를 들어 진구가 도라에몽한태 Repository가 뭐야 ? 라고 물어본다면 도라에몽은 구글링을 하던 교수님 한태 물어보던 어디선가 Repository에 대한 정보를 알아와 진구에게 알려줄 겁니다. 그렇다면 진구는 도라에몽이 어디서 이 정보를 알아왔는지 알 필요가 없겠죠!

interface FreeBoardRepository {
  suspend fun fetchAllBoardData(): List<BoardModel>
  suspend fun fetchAllBoardImage(boardIdx: String, imgName: String): URI?
}

 

또한 제가 사용하면서 가장 편리한 점은 코드 추적과 관리가 무척이나 편리했단 점입니다. 아래 사진처럼 기능별로 구조를 잡고 진행하니까 필요한 기능별로 관리하기가 정말 쉬웠습니다.

 

다음으로는 ViewModel 테스트 코드를 만들기 쉬워진다는 장점이 있습니다. 

interface FreeBoardRepository {
  suspend fun fetchAllBoardData(): List<BoardModel>
}

class FakeFreeBoardRepository: FreeBoardRepository {
  override suspend fun fetchAllBoardData(): List<BoardModel> {
    return listOf(
     	BoardModel(
        	boardIdx = 1,
        	boardTitle = "테스트용~가리",
      )
  }  
}

 

다음은 실제 사용했던 코드를 통해 살펴보겠습니다.

 

먼저 interface를 선언해 주었습니다. interface를 사용하는 이유는 이 interface의 구현체가 이런 함수를 가지고 있고 이런 타입의 데이터를 반환한다고 약속만 하는 인터페이스의 근본적인 역할을 수행합니다.

interface PetRepository {
    suspend fun fetchMyPetData(ownerIdx: String): List<PetModel>
    suspend fun fetchMyPetImage(ownerIdx: String, imgName: String): URI
    suspend fun readMyPetData(): Flow<List<MyPetEntity>>
}

 

Repositoy Pattern의 장점을 활용해 다양한 방식으로 약속을 지킬 수 있습니다. 이 코드를 만들면서 가장 고민이 많이 되었던 부분은 DataSource에 대한 의존성을 어디서 어떻게 주입해 주느냐였습니다.

 

그러던 중 객체지향의 5대 원칙 중 하나인 단일 책임 원칙(SRP, Single-responsibility principle) 에 대해 생각하게 되었습니다. 만약 이 Class 내부에서 직접 프로퍼티로 DataSource를 생성할 경우 클래스간의 결합도가 강해지고 무엇보다 다른 클래스에 대한 책임이 생깁니다. 

 

그 외에도 매번 새로운 인스턴스가 생성되 비효율적이고 유연성이 부족해 2안을 채택했습니다.

 

Room Database를 주입할 때 도 Database 자체를 주입할지 하나의 Dao만 주입할지에 대해 많은 고민을 했는데요, 결과적으론 하나의 Dao만 주입을 해주었습니다.

 

그 이유는 Room Database 특성상 여러 개의 테이블 즉, 다수의 Dao를 가질 수 있는데 만약 Database 자체를 주입하게 될 경우 다른 Table들에 대한 의존성이 생겨 불필요한 의존 관계가 생긴다고 생각했기 때문에 하나의 Dao만 주입을 해주었습니다.

// 1안
class PetRepositoryImpl(
): PetRepository {
    private val reference = CollectionReference(),
    private val storage  = StorageReference(),
    private val myPetDao = MyPetDao()

    override suspend fun readMyPetData(): Flow<List<MyPetEntity>> {}
    override suspend fun fetchMyPetData(ownerIdx: String): List<PetModel> {}
    override suspend fun fetchMyPetImage(ownerIdx: String, imgName: String): URI {}
}

// 2안
class PetRepositoryImpl(
    private val reference: CollectionReference,
    private val storage: StorageReference,
    private val myPetDao: MyPetDao
): PetRepository {

    override suspend fun readMyPetData(): Flow<List<MyPetEntity>> {}
    override suspend fun fetchMyPetData(ownerIdx: String): List<PetModel> {}
    override suspend fun fetchMyPetImage(ownerIdx: String, imgName: String): URI {}
}

 

위에서 만든 Repository는 ViewModel Factory에서 주입해 주었습니다. 아래 코드에 대해 간단히 설명드리면 onLoginSuccess 함수는 로그인을 성공할 경우 Firebase에 저장돼있던 반려동물 정보를 가져와서 Room에 저장이 완료되면 결과를 Callback으로 돌려받는 함수입니다. 

 

class StartViewModel(
    private val petRepositoryImpl: PetRepositoryImpl
): ViewModel() {

    fun onLoginSuccess(callback: (Boolean) -> Unit) = viewModelScope.launch {
        val success = try{
            val myUserNumber = MainDataStore.getUserNumber().stateIn(this).value
            val result = petRepositoryImpl.fetchMyPetData(myUserNumber)

            result.map{
                petRepositoryImpl.insertMyPetData(MyPetEntity(
                    petName = it.petName,
                    ownerIdx = it.ownerIdx,
                    petBreed = it.petBreed,
                    petGender = it.petGender,
                    petWeight = it.petWeight,
                    petAge = it.petAge,
                    isNeutering = it.isNeutering,
                    petSignificant = it.petSignificant,
                    imgPath = it.imgName
                ))
            }

            true
        }catch (e: Exception){
            false
        }
        callback(success)
    }
}

class StartViewModelFactory(context: Context): ViewModelProvider.Factory {
    private val petRepositoryImpl = PetRepositoryImpl(
        Firebase.firestore.collection("Pet"),
        Firebase.storage.reference,
        MainDataBase.getDatabase(context).myPetDao()
    )
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(StartViewModel::class.java)){
            return StartViewModel(petRepositoryImpl) as T
        }
        return super.create(modelClass)
    }
}

 

View에선 ViewModel에서 전달받은 Callback이 true가 될 경우 화면을 이동시킵니다.

class StartActivity : AppCompatActivity() {
    private lateinit var binding: ActivityStartBinding
    private val viewModel: StartViewModel by viewModels { StartViewModelFactory(this) }
    private val dialog = RequestPermissionDialog(
        buttonClick = {
            startActivity(Intent(this, LoginActivity::class.java))
            finish()
        }
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_start)
        binding = ActivityStartBinding.inflate(layoutInflater)

        repeatOnStarted {
            delay(450)
            if (viewModel.checkFistFlag()) {
                dialog.show(supportFragmentManager, "RequestPermissionDialog")
            } else {
                viewModel.onLoginSuccess {
                    if (it) {
                        startActivity(Intent(this@StartActivity, MainActivity::class.java))
                        finish()
                    }
                }
            }
        }
    }
}

🤔 개선점

이렇게 사용하고 나니 한 가지 고민되는 점이 있었는데요, 만약 제 코드처럼 PetRepository를 호출할 때 생성자로 주입해준 DataSource 중에 사용하지 않는 DataSource가 있다면 이걸 또 어떻게 분리를 해줄까에 대한 문제입니다. 이 부분에 대해서 앞으로 더 고민해 보고 해결 방안을 찾아봐야겠습니다. 

 

또한 사용하다 보니까 UseCase에 대해 알게 되었는데요! UseCase는 Repostory를 캡슐화하고 그 안에서 필요한 로직만 쏙쏙 뽑아 쓰면 되는 것 같슴니다... 이 부분에 대해선 아직 많이 부족하네요. 그래도 앱 스쿨 처음 시작할 때 클린 아키텍처가 레이어가 어쩌고... 의존성이 어쩌고 ... 정말 하나도 몰랐는데 이젠 점점 이해가 될 만큼 많이 성장한 것 같아서 뿌듯합니다😃