Repository 패턴은 앱에서 데이터를 관리하기 위해 사용되는 패턴 중 하나 입니다. Repository 패턴은 데이터를 처리하는 로직을 추상화하여, 데이터를 가져오는 소스를 분리하고 앱에서 데이터에 접근할 때 일관성을 유지시켜줍니다.
Repository 패턴을 적용하면, UI레이어와 데이터 소스 사이에 인터페이스를 둘 수 있습니다. 이 인터페이스를 통해 UI레이어에서는 데이터 소스의 구현에 대해 알 필요가 없으며, 데이터 소스에서는 UI레이어의 구현에 대해 알 필요가 없습니다. 이렇게 함으로써 UI 레이어와 데이터 소스 사이의 결합도를 낮출 수 있으며, 유지보수성과 테스트 용이성을 향상 시킬 수 있습니다.
Interface: 데이터에 접근할 때 사용되는 메서드가 선언된 Interface 입니다.
Repository: Interface를 구현하는 클래스입니다. 해당 클래스에서는 데이터 소스에서 데이터를 가져오는 방법이 구현되어 있습니다.
데이터 소스: 테이터를 저장하고 가져오는 방법을 구현하는 클래스입니다.
위의 구성요소만을 가지고는 이해하기 어려울 수 있으니 간단한 코드를 통해서 설명하겠습니다.
UserRepository를 통해서 데이터를 가져오는 로직을 구현해보겠습니다.
제일 먼저 데이터 소스 부분을 작성해보겠습니다.
로컬 데이터베이스에서 User 정보를 가져오는 코드를 작성해보겠습니다.
@Dao
interface UserDao {
@Query("SELECT * FROM users")
suspend fun getUsers(): List<UserEntity>
}
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: Int,
val name: String,
val email: String,
val phone: String
)
@Singleton
class LocalUserDataSource @Inject constructor(
private val userDao: UserDao
) {
suspend fun getUsers(): List<User> {
return userDao.getUsers().map { it.toUser() }
}
private fun UserEntity.toUser(): User {
return User(
id = this.id,
name = this.name,
email = this.email,
phone = this.phone
)
}
}
interface UserRepository {
suspend fun getUsers(): List<User>
}
다음으로 Interface를 선언해줍니다.
class UserRepositoryImpl @Inject constructor(
private val localUserDataSource: LocalUserDataSource
) : UserRepository {
override suspend fun getUsers(): List<User> {
return localUserDataSource.getUsers()
}
}
UserRepository Interface를 상속받은 UserRepositoryImpl를 만들어 줍니다. UserRepositoryImpl 클래스에서는 LocalUserDataSource를 이용해 데이터를 가져오는 로직을 구현해줍니다.
마지막으로 viewModel에서 UserRepositoryImpl을 주입받아 사용하는 것 입니다.
class UserViewModel @ViewModelInject constructor(
private val userRepository: UserRepository
) : ViewModel() {
private val _users = MutableLiveData<List<User>>()
val users: LiveData<List<User>> = _users
fun getUsers() {
viewModelScope.launch {
_users.value = userRepository.getUsers()
}
}
}
userRepository의 getUsers()을 통해 데이터를 가져오고 그 데이터를 LiveData에 담아줍니다. 해당 함수의 경우 viewModelScope에서 실행되므로 Activity/Fragment가 종료되면 자동으로 취소됩니다.
@AndroidEntryPoint
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
viewModel.users.observe(this, { users ->
// binding 해주는 작업
})
viewModel.getUsers()
}
}
예시코드에서 hilt를 사용하고 있기에 안드로이드 라이브러리에서 제공하는 kotlin 확장 함수인 viewModels()를 사용해줍니다. 그렇기에 위의 코드처럼 by viewModels()를 통해 viewModel을 주입해줍니다.
by?? = 를 통해서 주입해주면 안되나요? 라고 할 수 있습니다. 저도 처음에 왜 위임만을 해야할까? 고민을 했기 때문입니다. 그 이유는 다음과 같습니다.
위임 프로퍼티인 by를 사용하면 액티비티나 프래그먼트의 생명주기와 관련된 viewModel의 인스턴스 생성과 소멸을 안전하게 처리할 수 있습니다. 이 방식은 Kotlin에서 자주 사용되는 방식입니다. 하지만 = viewModels()는 일반적인 프로퍼티 초기화 구문을 사용하여 인스턴스를 생성해주는 방식으로 인스턴스 생성 및 생명주기 관리를 개발자가 수동으로 처리해야 한다라는 단점이 발생합니다. 그만큼 오류가 발생할 가능성이 높아진다는 뜻입니다. 그렇기에 by viewModels()를 사용하여 안전하게 처리하는 것이 좋습니다.
이번 글은 여기까지 작성하도록 하며 더욱더 많은 글을 작성할 수 있도록 노력하겠습니다. 읽어주셔서 감사합니다.