[Android] Repository 패턴

yeonjeen·2024년 5월 30일

Android

목록 보기
5/10
post-thumbnail

SOPT 34기 안드로이드 파트 7주차 과제이자 개인적으로 공부해보고 싶었던 주제를 가져와봤다!


1️⃣ repository 패턴이란?

repository는 말 그대로 '저장소'이다. 그럼 저장소 패턴은 무엇일까?

Data의 출처 상관없이 동일한 인터페이스로 데이터에 접근 할 수 있게 해주는 패턴

즉 repository 패턴은 저장소에서 Local이나 Remote에 있는 필요한 데이터를 가져와 ViewModel에게 전달하는 방식이다.

그렇게 되면 ViewModel은 데이터에 직접 접근하지 않아도 되며, Data Layer를 캡슐화 할 수 있게 된다.

🌟 캡슐화란? - 클래스 안에 서로 연관있는 속성과 기능들을 하나의 캡슐(capsule)로 만들어 데이터를 외부로부터 보호하는 것

2️⃣ repository pattern의 장점

1. 코드의 가독성과 유지보수성 향상

  • 데이터 접근 로직의 분리: 데이터베이스나 네트워크 호출 등의 데이터 접근 로직이 비즈니스 로직과 분리되므로 코드가 명확해짐.
  • 단일 책임 원칙(Single Responsibility Principle) 준수: 각 클래스가 자신의 역할에만 집중. 데이터 접근 로직은 레포지터리나 데이터 소스 클래스에 집중되고, 비즈니스 로직은 서비스나 ViewModel에 집중.

2. 데이터 소스 교체 용이

  • 유연한 데이터 소스 교체: 로컬 데이터베이스, 원격 서버, 캐시 등 다양한 데이터 소스를 쉽게 교체하거나 추가👌🏻. 예를 들어, 로컬 데이터베이스를 Room에서 SQLite로 바꾸거나 원격 API를 REST에서 GraphQL로 바꾸는 작업이 상대적으로 쉬워짐.
  • 의존성 주입 활용: 의존성 주입(DI) 프레임워크를 사용하면 데이터 소스를 교체할 때 레포지터리의 구현만 변경하면 되므로 애플리케이션의 다른 부분에는 영향을 미치지❌

3. 테스트 용이성

  • 독립적인 테스트: 레포지터리 패턴을 사용하면 비즈니스 로직과 데이터 접근 로직이 분리되므로 각각을 독립적으로 테스트할 수 있습니다.

4. 재사용성

  • 코드 재사용: 동일한 데이터 접근 로직을 여러 곳에서 사용할 수 있음. 예를 들어, 여러 ViewModel이나 서비스에서 동일한 레포지터리를 사용할 수 있음.
  • 중앙 집중 관리: 데이터 접근 로직을 중앙에서 관리하기 때문에, 수정이 필요할 때 여러 곳을 수정할 필요 없이 레포지터리 클래스만 수정하면 됌.

3️⃣ repository pattern 적용하기

① 데이터 모델 정의

data class User(
    val id: Int,
    val name: String,
    val email: String
)

② 데이터 소스 인터페이스 정의 : 데이터를 가져오는 방법을 정의하는 인터페이스를 만든다.

interface UserDataSource {
    fun getUser(userId: Int): User?
    fun getAllUsers(): List<User>
    fun saveUser(user: User)
    fun deleteUser(userId: Int)
}

③-𝟙 로컬 데이터 소스 구현 : 데이터를 로컬 데이터베이스에서 가져오는 로직 구현

class LocalUserDataSource(private val userDao: UserDao) : UserDataSource {
    override fun getUser(userId: Int): User? {
        return userDao.getUserById(userId)
    }

    override fun getAllUsers(): List<User> {
        return userDao.getAllUsers()
    }

    override fun saveUser(user: User) {
        userDao.insertUser(user)
    }

    override fun deleteUser(userId: Int) {
        userDao.deleteUserById(userId)
    }
}

③-𝟚 원격 데이터 소스 구현 : 데이터를 원격 서버(API)에서 가져오는 로직 구현

class RemoteUserDataSource(private val apiService: ApiService) : UserDataSource {
    override fun getUser(userId: Int): User? {
        val response = apiService.getUser(userId).execute()
        return if (response.isSuccessful) response.body() else null
    }

    override fun getAllUsers(): List<User> {
        val response = apiService.getAllUsers().execute()
        return if (response.isSuccessful) response.body() ?: emptyList() else emptyList()
    }

    override fun saveUser(user: User) {
        apiService.saveUser(user).execute()
    }

    override fun deleteUser(userId: Int) {
        apiService.deleteUser(userId).execute()
    }
}

④ 레포지터리 클래스 구현 : 레포지터리 클래스는 여러 데이터 소스를 관리하고 필요한 데이터를 제공하는 역할

class UserRepository(
    private val localDataSource: UserDataSource,
    private val remoteDataSource: UserDataSource
) {
    fun getUser(userId: Int): User? {
        // 우선 로컬 데이터 소스에서 데이터를 가져오고 없으면 원격 데이터 소스에서 가져옴
        var user = localDataSource.getUser(userId)
        if (user == null) {
            user = remoteDataSource.getUser(userId)
            if (user != null) {
                localDataSource.saveUser(user)  // 원격 데이터 소스에서 가져온 데이터를 로컬에 저장
            }
        }
        return user
    }

    fun getAllUsers(): List<User> {
        // 동일한 로직으로 모든 사용자 데이터를 가져옴
        val users = localDataSource.getAllUsers()
        return if (users.isEmpty()) {
            val remoteUsers = remoteDataSource.getAllUsers()
            remoteUsers.forEach { localDataSource.saveUser(it) }
            remoteUsers
        } else {
            users
        }
    }

    fun saveUser(user: User) {
        localDataSource.saveUser(user)
        remoteDataSource.saveUser(user)  // 원격에도 저장
    }

    fun deleteUser(userId: Int) {
        localDataSource.deleteUser(userId)
        remoteDataSource.deleteUser(userId)  // 원격에서도 삭제
    }
}

⑤ ViewModel에서 레포지터리 사용 : ViewModel에서 레포지터리를 사용하여 데이터를 관리

class UserViewModel(private val userRepository: UserRepository) : ViewModel() {
    private val _user = MutableLiveData<User>()
    val user: LiveData<User> get() = _user

    fun loadUser(userId: Int) {
        viewModelScope.launch {
            val user = userRepository.getUser(userId)
            _user.postValue(user)
        }
    }

    fun saveUser(user: User) {
        viewModelScope.launch {
            userRepository.saveUser(user)
        }
    }

    fun deleteUser(userId: Int) {
        viewModelScope.launch {
            userRepository.deleteUser(userId)
        }
    }
}

⑥ 의존성 주입 : 의존성 주입을 통해 ViewModel과 Repository를 연결 / Hilt 또는 Dagger와 같은 라이브러리를 사용

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    fun provideLocalUserDataSource(userDao: UserDao): UserDataSource = LocalUserDataSource(userDao)

    @Provides
    fun provideRemoteUserDataSource(apiService: ApiService): UserDataSource = RemoteUserDataSource(apiService)

    @Provides
    fun provideUserRepository(
        localDataSource: UserDataSource,
        remoteDataSource: UserDataSource
    ): UserRepository = UserRepository(localDataSource, remoteDataSource)
}

@HiltViewModel
class UserViewModel @Inject constructor(private val userRepository: UserRepository) : ViewModel() {
    // ViewModel 코드
}

0개의 댓글