
사용자 정보, 설정 정보와 같은 간단한 정보들은 앱 내부 저장소에 저장을 하곤 합니다. 대부분 한번쯤은 저장을 해봤을텐데 저는 이전까지는 SharedPreferences 를 이용했었습니다. 그 외에도 암호화 적용된 Encryptedsharedpreferences 채택할 수도 있지만 BuddyCon에서는 DataStore를 이용하였고 DataStore에 대해 빠르게 소개해볼까 합니다.
앱에서 데이터를 저장하고 관리하기 위한 솔루션으로 SharedPreferences 대안으로 설계되었습니다. DataStore는 Kotlin 코루틴 및 Flow 를 사용하여 비동기적으로 일관된 트랜잭션 방식으로 데이터를 저장합니다.
DataStore에서는 2가지 형태로 Preferences DataStore와 Proto DataStore가 있습니다.
BuddyCon에서는 많은 데이터를 저장하지도 않고 클래스를 별도로 정의할 필요도 없었기에 Proto DataStore 보다는 쉽게 구현이 가능한 Preferences DataStore를 사용하였습니다. 하지만, 요즘 프로토콜 버퍼를 활용하여 데이터를 저장하고 서버와 데이터를 주고 받는다고 알고 있어서 나중에 한번쯤 사용해보면 좋을 것 같습니다. 추후에 프로토콜 버퍼를 사용한 후에 Proto DataStore 사용가이드까지 업데이트 해보겠습니다!
간단하게 프로토콜 버퍼에 대해서도 알아가면 Google 에서 개발한 언어 중립적, 플랫폼 중립적인 직렬화 포맷입니다. 데이터를 구조화하여 효율적으로 저장하고 네트워크를 통해 전송할 수 있는 방식입니다. 만일, 프로토콜 버퍼를 통해서 서버와 데이터를 주고 받으려면 클라이언트 - 서버 모두 프로토콜 버퍼를 통해 파싱을 하는 작업이 필요하니 협업을 하실 때 미리 정하면 좋을 것 같습니다.
@InstallIn(SingletonComponent::class)
object LocalDataModule {
const val BUDDYCON_DATASTORE = "BUDDYCON_DATASTORE"
@Provides
@Singleton
fun provideDataStore(
@ApplicationContext context: Context
): DataStore<Preferences> =
PreferenceDataStoreFactory.create {
context.preferencesDataStoreFile(BUDDYCON_DATASTORE)
}
}
class TokenRepositoryImpl @Inject constructor(
private val dataStore: DataStore<Preferences>
) : TokenRepository {
override fun getToken(): Flow<String> =
dataStore.data.map { preference ->
preference[BUDDYCON_TOKEN] ?: ""
}
override fun getTokenExpiration(): Flow<Long> =
dataStore.data.map { preference ->
preference[BUDDYCON_TOKEN_EXPIRATION] ?: 0L
}
override fun getRefreshToken(): Flow<String> =
dataStore.data.map { preference ->
preference[BUDDYCON_REFRESH_TOKEN] ?: ""
}
companion object {
val BUDDYCON_TOKEN = stringPreferencesKey("BUDDYCON_TOKEN")
val BUDDYCON_TOKEN_EXPIRATION = longPreferencesKey("BUDDYCON_TOKEN_EXPIRATION")
val BUDDYCON_REFRESH_TOKEN = stringPreferencesKey("BUDDYCON_REFRESH_TOKEN")
}
}
class TokenRepositoryImpl @Inject constructor(
private val dataStore: DataStore<Preferences>
) : TokenRepository {
override suspend fun saveToken(
accessToken: String,
accessTokenExpiresIn: Long,
refreshToken: String
) {
dataStore.edit { preference ->
Timber.d("saveToken accessToken: $accessToken accessTokenExpiresIn: $accessTokenExpiresIn refreshToken: $refreshToken")
preference[BUDDYCON_TOKEN] = accessToken
preference[BUDDYCON_TOKEN_EXPIRATION] = accessTokenExpiresIn
preference[BUDDYCON_REFRESH_TOKEN] = refreshToken
}
}
companion object {
val BUDDYCON_TOKEN = stringPreferencesKey("BUDDYCON_TOKEN")
val BUDDYCON_TOKEN_EXPIRATION = longPreferencesKey("BUDDYCON_TOKEN_EXPIRATION")
val BUDDYCON_REFRESH_TOKEN = stringPreferencesKey("BUDDYCON_REFRESH_TOKEN")
}
}
class BuddyConInterceptor @Inject constructor(
private val tokenRepository: TokenRepository,
private val userRepository: UserRepository
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val tokenInfo = runBlocking {
combine(
tokenRepository.getToken(),
tokenRepository.getRefreshToken(),
tokenRepository.getTokenExpiration()
) { accessToken, refreshToken, accessTokenExpiresIn ->
UserInfo(accessToken, refreshToken, accessTokenExpiresIn)
}.first()
}
var (accessToken, refreshToken, accessTokenExpiresIn) = tokenInfo
val currentTime = System.currentTimeMillis()
if (accessTokenExpiresIn < currentTime) {
try {
val refreshTokenInfo = runBlocking {
userRepository.requestRefreshToken(accessToken, refreshToken).first()
}
accessToken = refreshTokenInfo.accessToken
refreshToken = refreshTokenInfo.refreshToken
accessTokenExpiresIn = refreshTokenInfo.accessTokenExpiresIn
runBlocking { tokenRepository.saveToken(accessToken, accessTokenExpiresIn, refreshToken) }
} catch (_: Exception) {
}
}
val newRequest = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $accessToken")
.build()
return chain.proceed(newRequest)
}
}