Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[REFACTOR] 데이터 레이어 다른 계층 참조하지 않도록 코드 분리 #340

Merged
merged 18 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a2d3c2b
[FEAT] #339 Interceptor에서 액세스, 리프레시 토큰 관리 작업 구현
sxunea Mar 14, 2024
b609e6c
[FEAT] #339 액세스 토큰 재발급 성공시, preference에 해당 값 저장
sxunea Mar 14, 2024
8b882b7
[FEAT] #339 리프레시 토큰 또한 만료 / 통신 실패 시 빈 값 preference에 저장
sxunea Mar 14, 2024
123e7e7
[CHORE] #339 주석 추가 및 코드 정리
sxunea Mar 14, 2024
60adff3
[DEL] #339 Authenticator 제거
sxunea Mar 14, 2024
3e8d971
[MOD] #339 레트로핏 모듈에 Interceptor 반영
sxunea Mar 14, 2024
1aebc18
[FEAT] #339 accessToken 빈 문자열이면 토스트메시지와 함께 재로그인 요청
sxunea Mar 14, 2024
2ac7be5
[REFACTOR] #339 non null 단언 연산자 제거
sxunea Mar 14, 2024
0c210d0
[REFACTOR] #339 named argument 적용
sxunea Mar 24, 2024
ead0369
[REFACTOR] #339 interceptor return if로 response처리
sxunea Mar 24, 2024
60bbd81
[REFACTOR] #339 리뷰 간단사항 반영
sxunea Mar 24, 2024
a5eab3d
[ADD] #339 LoginStatus Enum Class 만들어 적용해 토큰 값 따른 로그인 상태 관리
sxunea Mar 24, 2024
b3267c8
[REFACTOR] #339 토큰preferenceManager AuthUtil로 빼서 관리
sxunea Mar 24, 2024
625d913
[MOD] #339 applicationContext 주입
sxunea Mar 24, 2024
e9c245b
[MOD] #339 runBlocking제거
sxunea Mar 27, 2024
0af3516
[REFACTOR] #339 applicationContext -> context로 변경하고, null 안전하게 체크
sxunea Mar 27, 2024
a390166
Merge remote-tracking branch 'origin/develop' into feature/refactor-data
sxunea Apr 24, 2024
7315ecc
[MERGE] #339 충돌 해결
sxunea Apr 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import com.runnect.runnect.R
import com.runnect.runnect.application.ApiMode
import com.runnect.runnect.application.ApplicationClass
import com.runnect.runnect.application.PreferenceManager
import com.runnect.runnect.data.service.TokenAuthenticator
import com.runnect.runnect.presentation.mypage.setting.accountinfo.MySettingAccountInfoFragment
import com.runnect.runnect.util.custom.toast.RunnectToast
import kotlinx.coroutines.Dispatchers
Expand All @@ -44,8 +43,8 @@ class RunnectDeveloperActivity : AppCompatActivity(R.layout.activity_runnect_dev

private fun initUserInfo() {
val ctx: Context = context ?: return
val accessToken = PreferenceManager.getString(ctx, TokenAuthenticator.TOKEN_KEY_ACCESS) ?: ""
val refreshToken = PreferenceManager.getString(ctx, TokenAuthenticator.TOKEN_KEY_REFRESH) ?: ""
val accessToken = PreferenceManager.getString(ctx, TOKEN_KEY_ACCESS) ?: ""
val refreshToken = PreferenceManager.getString(ctx, TOKEN_KEY_REFRESH) ?: ""

setPreferenceSummary("dev_pref_key_access_token", accessToken)
setPreferenceSummary("dev_pref_key_refresh_token", refreshToken)
Expand Down Expand Up @@ -167,6 +166,8 @@ class RunnectDeveloperActivity : AppCompatActivity(R.layout.activity_runnect_dev

companion object {
private const val CLIPBOARD_LABEL = "keyword"
const val TOKEN_KEY_ACCESS = "access"
const val TOKEN_KEY_REFRESH = "refresh"
}
}
}

This file was deleted.

141 changes: 141 additions & 0 deletions app/src/main/java/com/runnect/runnect/data/service/AuthInterceptor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.runnect.runnect.data.service

import com.runnect.runnect.application.ApplicationClass
import com.runnect.runnect.application.PreferenceManager
import com.runnect.runnect.data.dto.response.ResponseGetRefreshToken
import com.runnect.runnect.data.dto.response.base.BaseResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import timber.log.Timber
import javax.inject.Inject

class AuthInterceptor @Inject constructor(
private val json: Json
) : Interceptor {
// access Header 에 보내고 이때 401(토큰 만료) 뜨면 액세스 재발급 요청
// 재발급 성공 : 저장
// 재발급 실패 : 재 로그인 토스트 메시지 띄우고 preference 빈 값 넣고 로그인 화면 이동
override fun intercept(chain: Interceptor.Chain): Response {
runBlocking { Timber.e("AccessToken : ${getAccessToken()}, RefreshToken : ${getRefreshToken()}") }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그 찍는 이 부분을 runBlocking으로 감싸신 이유가 있나요?

val originalRequest = chain.request()

val headerRequest = originalRequest.newAuthTokenBuilder()
.build()

val response = headerRequest.let { chain.proceed(it) }

when (response.code) {
CODE_TOKEN_EXPIRED -> {
try {
Timber.e("Access Token Expired: getNewAccessToken")
response.close()
return handleTokenExpired(chain, originalRequest, headerRequest)
} catch (t: Throwable) {
Timber.e("Exception: ${t.message}")
saveToken("", "")
}
}
}
return response
}

private fun Request.newAuthTokenBuilder() =
runBlocking(Dispatchers.IO) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 runBlocking 쓰신 이유가 있나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저 때는 해당 작업이 완료될때까지 스레드를 막으려고 runBlocking을 사용하긴 했습니다 ! 다만 runBlocking이 아예 차단이라는 점에서 스레드를 점유하니까 좋진 않을거라고 작성할때도 생각하긴했습니다 .. .ㅎ ㅎ 처음에는 suspend로 작성했었는데 그러면 newAuthTokenBuilder를 사용하는 okhttp3 intercept까지 suspend func이 되어야 하는데, 이를 지원하지 않더라구요 😭 그래서 차선책으로 사용했습니다!!

지금 생각해보니 저 동작이 꼭 비동기여야할 필요는 없다고 생각이 드는데 어떻게 생각하시나요 ? 아니면 혹시 runBlocking을 쓰지 않고 다르게 처리할 방법이 있는지 궁금합니다

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 runBlocking을 작성하지 않았을 때 제대로 동작하지 않았나요??
interceptor가 비동기적으로 동작하더라도 같은 메소드 내에서는 실행 순서가 보장될테니 별도 처리가 필요 없을 것 같다는 생각이 들었습니다~!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음에 구현할땐 저도 필요없다고 생각했어서 작성하지 않았는데 동작이 안됐어서 runBlocking을 사용했는데, 지금 제거하고 보니 또 잘 가져오네요 🥹 다른 부분이 잘못됐었는데 불필요하게 같이 고쳐준 것 같아요 저도 runBlocking이 여전히 불필요하다고 생각합니다 ㅎㅎㅎ 없애서 수정할게요 !

val accessToken = getAccessToken()
val refreshToken = getRefreshToken()
newBuilder().apply {
addHeader(ACCESS_TOKEN, accessToken)
addHeader(REFRESH_TOKEN, refreshToken)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 더 쪼개서 구조를 아래처럼 바꿔보는 것은 어떨까요? 개인적으로 가독성을 더 높일 수 있다고 생각합니다.
*참고용으로 구조만 러프하게 잡은 거라 누락된 코드가 있습니다.(=기존의 logic이 그대로 유지되지 않았습니다.) 보충이 필요합니다.
ex) response.close, saveToken, exception catch 등

    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val modifiedRequest = originalRequest.newAuthTokenBuilder().build()

        val response = chain.proceed(modifiedRequest)

        return if (response.code == CODE_TOKEN_EXPIRED) {
            handleTokenExpired(chain, originalRequest, modifiedRequest)
        } else {
            response
        }
    }

    private fun Request.newAuthTokenBuilder(): Request.Builder {
        return this.newBuilder().apply {
            val accessToken = getAccessToken() 
            val refreshToken = getRefreshToken()
            addHeader(ACCESS_TOKEN, accessToken)
            addHeader(REFRESH_TOKEN, refreshToken)
        }
    }

Copy link
Collaborator Author

@sxunea sxunea Mar 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 처음에 구현할 때 응답코드에 따른 다른 처리가 있을까봐 미리 when문으로 짜둔 건데 일단 지금은 불필요한것 같네요 !! 감사합니당 다만 말씀하신 의도가 return if로 response를 직접 리턴해주자는 게 맞나요 ? 쪼갠다는 맥락에 해당하는지는 잘모르겠어서 (ead0369) 에 수정해 푸쉬했으니 한번 확인부탁드려요 ~!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return if를 써보자는 것보다는 if문 안이 true일 때랑 false일 때 어떤 일이 일어나는지 간략하게 보여주고 구체적인 logic은 호출하는 함수 내부로 숨겨보자는 의미였습니다! (캡슐화)



private fun getAccessToken(): String {
return PreferenceManager.getString(
ApplicationClass.appContext,
TOKEN_KEY_ACCESS
) ?: ""
}

private fun getRefreshToken(): String {
return PreferenceManager.getString(
ApplicationClass.appContext,
TOKEN_KEY_REFRESH
) ?: ""
}

private fun saveToken(accessToken: String, refreshToken: String) {
PreferenceManager.setString(ApplicationClass.appContext, TOKEN_KEY_ACCESS, accessToken)
PreferenceManager.setString(ApplicationClass.appContext, TOKEN_KEY_REFRESH, refreshToken)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 AuthUtils라는 SharedPreference 값 set/get 하는 목적의 유틸 클래스 하나 만들어서 거기로 옮기는 건 어떨까요? Login 쪽에서 getAccessToken() 함수 호출하는 부분이 있던데 유틸로 만들면 각각이 쓰여지는 부분을 좀 더 간결하게 만들 수 있을 것 같아서요


private fun handleTokenExpired(
chain: Interceptor.Chain,
originalRequest: Request,
headerRequest: Request
): Response {
val refreshTokenResponse = getRefreshToken(originalRequest, chain)
return if (refreshTokenResponse.isSuccessful) {
handleGetRefreshTokenSuccess(refreshTokenResponse, originalRequest, chain)
} else {
handleGetRefreshTokenFailure(refreshTokenResponse, headerRequest, chain)
}
}

private fun getRefreshToken(originalRequest: Request, chain: Interceptor.Chain): Response {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accessToken을 재발급 받는 api인데 개인적으로는 이름이 getRefreshToken으로 돼있어서 조금 헷갈리는 것 같아 혹시 이름을 api endpoint와 통일해서 getNewToken으로 해보는 건 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅎㅎㅎ 공감합니다 전 코드가 refresh길래 일단 남겨둔거였어요 : ) 수정했습니다 !

val baseUrl = ApplicationClass.getBaseUrl()
val refreshToken = getRefreshToken()
val refreshTokenRequest = originalRequest.newBuilder().post("".toRequestBody())
.url("$baseUrl/api/auth/getNewToken")
.addHeader(REFRESH_TOKEN, refreshToken)
.build()

return chain.proceed(refreshTokenRequest)
}

private fun handleGetRefreshTokenSuccess(
refreshTokenResponse: Response,
originalRequest: Request,
chain: Interceptor.Chain
): Response {
refreshTokenResponse.use { response ->
val responseToken = json.decodeFromString<BaseResponse<ResponseGetRefreshToken>>(
response.body?.string().orEmpty()
)
responseToken.data?.data?.let {
Timber.e("New Refresh Token Success: ${it.refreshToken}")
saveToken(it.accessToken, it.refreshToken)
}
}

val newRequest = originalRequest.newAuthTokenBuilder().build()
return chain.proceed(newRequest)
}

private fun handleGetRefreshTokenFailure(
refreshTokenResponse: Response,
headerRequest: Request,
chain: Interceptor.Chain
): Response {
Timber.e("New Refresh Token Failure: ${refreshTokenResponse.code}")
saveToken("", "")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상황이 조금 더 직관적으로 파악될 수 있게 named argument 써보시는 건 어떨까요?

return chain.proceed(headerRequest)
}


companion object {
private const val ACCESS_TOKEN = "accessToken"
private const val CODE_TOKEN_EXPIRED = 401
private const val REFRESH_TOKEN = "refreshToken"

const val TOKEN_KEY_ACCESS = "access"
const val TOKEN_KEY_REFRESH = "refresh"
}

}

This file was deleted.

25 changes: 15 additions & 10 deletions app/src/main/java/com/runnect/runnect/di/RetrofitModule.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.runnect.runnect.di

import com.google.android.gms.auth.api.Auth
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.runnect.runnect.BuildConfig
import com.runnect.runnect.application.ApplicationClass
Expand All @@ -16,6 +17,7 @@ import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
Expand All @@ -34,14 +36,20 @@ object RetrofitModule {
@Retention(AnnotationRetention.BINARY)
annotation class Tmap

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Auth

@Provides
@Singleton
fun provideOkHttpClient(
logger: HttpLoggingInterceptor,
appInterceptor: AppInterceptor,
tokenAuthenticator: TokenAuthenticator
): OkHttpClient = OkHttpClient.Builder().addInterceptor(logger).addInterceptor(appInterceptor)
.authenticator(tokenAuthenticator).build()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인터셉터 하나로 합치신 거 좋네요 굿

logger : HttpLoggingInterceptor,
@Auth authInterceptor: Interceptor
): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(logger)
.addInterceptor(authInterceptor)
.build()


@Provides
@Singleton
Expand All @@ -51,12 +59,9 @@ object RetrofitModule {

@Provides
@Singleton
fun provideAppInterceptor(): AppInterceptor = AppInterceptor()
@Auth
fun provideAuthInterceptor(interceptor: AuthInterceptor): Interceptor = interceptor

@Provides
@Singleton
fun provideTokenAuthenticator(): TokenAuthenticator =
TokenAuthenticator(ApplicationClass.appContext)

@OptIn(ExperimentalSerializationApi::class, InternalCoroutinesApi::class)
@Provides
Expand Down
Loading
Loading