[Dagger/DependencyCycle] Found a dependency cycle

leeeha·2024년 11월 20일

Trouble Shooting

목록 보기
3/5
post-thumbnail

LoveMarker 프로젝트를 개발하며 발생했던 이슈들을 기록합니다!

💥 어떤 문제가 발생했나요?

Retrofit -> ReissueTokenService -> Authenticator -> OkHttpClient -> Retrofit (순환 의존성 발생)

[Dagger/DependencyCycle] Found a dependency cycle:
retrofit2.Retrofit is injected at
com.capstone.lovemarker.core.network.di.ServiceModule.provideReissueTokenService(retrofit) // 시작점 

com.capstone.lovemarker.core.network.service.ReissueTokenService is injected at
com.capstone.lovemarker.core.network.authenticator.LoveMarkerAuthenticator(…, reissueTokenService, …)

com.capstone.lovemarker.core.network.authenticator.LoveMarkerAuthenticator is injected at
com.capstone.lovemarker.core.network.di.NetworkModule.AuthenticatorBinder.provideAuthenticator(authenticator)

okhttp3.Authenticator is injected at
com.capstone.lovemarker.core.network.di.NetworkModule.provideOkHttpClient(…, authenticator)

okhttp3.OkHttpClient is injected at
com.capstone.lovemarker.core.network.di.NetworkModule.provideRetrofit(client, …)

retrofit2.Retrofit is injected at
com.capstone.lovemarker.core.network.di.ServiceModule.provideReissueTokenService(retrofit) // 도착점 

...
The cycle is requested via:
retrofit2.Retrofit is injected at
com.capstone.lovemarker.auth.di.ServiceModule.provideAuthService(retrofit)
com.capstone.lovemarker.auth.service.AuthService is injected at
com.capstone.lovemarker.auth.source.AuthDataSourceImpl(authService)
com.capstone.lovemarker.auth.source.AuthDataSourceImpl is injected at
com.capstone.lovemarker.auth.di.DataSourceModule.bindAuthDataSource(authDataSourceImpl)
com.capstone.lovemarker.auth.source.AuthDataSource is injected at
com.capstone.lovemarker.auth.repository.AuthRepositoryImpl(authDataSource)
com.capstone.lovemarker.auth.repository.AuthRepositoryImpl is injected at
com.capstone.lovemarker.auth.di.RepositoryModule.bindAuthRepository(authRepositoryImpl)
com.capstone.lovemarker.auth.repository.AuthRepository is injected at
com.capstone.lovemarker.feature.login.LoginViewModel(authRepository)
com.capstone.lovemarker.feature.login.LoginViewModel is injected at
com.capstone.lovemarker.feature.login.LoginViewModel_HiltModules.BindsModule.binds(arg0)
@dagger.hilt.android.internal.lifecycle.HiltViewModelMap java.util.Map,javax.inject.Provider> is requested at
dagger.hilt.android.internal.lifecycle.HiltViewModelFactory.ViewModelFactoriesEntryPoint.getHiltViewModelMap() [com.capstone.lovemarker.LoveMarkerApplication_HiltComponents.SingletonC → com.capstone.lovemarker.LoveMarkerApplication_HiltComponents.ActivityRetainedC → com.capstone.lovemarker.LoveMarkerApplication_HiltComponents.ViewModelC]

🤷‍♀️ 왜 발생했나요?

다음과 같은 의존성 주입 흐름에 따라 순환 사이클이 생겨버렸다.. 😱

Retrofit -> ReissueTokenService

@Module
@InstallIn(SingletonComponent::class)
object ServiceModule {
    @Provides
    @Singleton
    fun provideReissueTokenService(retrofit: Retrofit): ReissueTokenService = retrofit.create()
}

ReissueTokenService -> Authenticator

class LoveMarkerAuthenticator @Inject constructor(
    private val userPreferencesDataSource: UserPreferencesDataSource,
    private val reissueTokenService: ReissueTokenService,
    @ApplicationContext private val context: Context,
) : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        if (response.code == CODE_TOKEN_EXPIRED) {
            // ... 
        }
        return null
    }

    companion object {
        const val CODE_TOKEN_EXPIRED = 401
    }
}

Authenticator -> OkHttpClient

@Singleton
@Provides
fun provideOkHttpClient(
    @Logging loggingInterceptor: Interceptor,
    @Auth authInterceptor: Interceptor,
    authenticator: Authenticator
): OkHttpClient = OkHttpClient.Builder()
    .addInterceptor(loggingInterceptor)
    .addInterceptor(authInterceptor)
    .authenticator(authenticator)
    .build()

OkHttpClient -> Retrofit

@Singleton
@Provides
fun provideRetrofit(
    client: OkHttpClient,
    converterFactory: Converter.Factory
): Retrofit = Retrofit.Builder()
    .baseUrl(BuildConfig.LOVE_MARKER_BASE_URL)
    .client(client)
    .addConverterFactory(converterFactory)
  .build()

🤔 어떻게 해결했나요?

Retrofit -> TokenService -> Authenticator -> OkHttpClient -> Retrofit

위와 같은 순환 의존성을 끊어내려면, Retrofit 객체를 생성할 때 필요한 OkHttpClientAuthenticator에 대해 모르게 만들어야 한다.

즉, 아래 코드처럼 OkHttpClient 객체를 생성할 때 Authenticator를 주입하면 안 된다는 뜻이다.

@Singleton
@Provides
fun provideOkHttpClient(
    @Logging loggingInterceptor: Interceptor,
    @Auth authInterceptor: Interceptor,
    //authenticator: Authenticator
): OkHttpClient = OkHttpClient.Builder()
    .addInterceptor(loggingInterceptor)
    .addInterceptor(authInterceptor)
    //.authenticator(authenticator)
    .build()

그리고 액세스 토큰 재발급할 때는 리프레시 토큰만 필요하기 때문에, 헤더에 액세스 토큰을 추가하는 AuthInterceptor 역시 불필요하다.

@Singleton
@Provides
fun provideOkHttpClient(
    @Logging loggingInterceptor: Interceptor,
    //@Auth authInterceptor: Interceptor,
    //authenticator: Authenticator
): OkHttpClient = OkHttpClient.Builder()
    .addInterceptor(loggingInterceptor)
    //.addInterceptor(authInterceptor)
    //.authenticator(authenticator)
    .build()

정리하면, 액세스 토큰을 재발급 하는 API를 호출할 때는 아래와 같이 OkHttpClient 객체를 생성하면 순환 의존성을 해결할 수 있다.

@Singleton
@Provides
fun provideOkHttpClient(
    @Logging loggingInterceptor: Interceptor,
): OkHttpClient = OkHttpClient.Builder()
    .addInterceptor(loggingInterceptor)
    .build()

이를 구현하기 위해 @AuthRequired, @AuthNotRequired 한정자로 타입을 구분하여 OkHttpClient, Retrofit 객체를 생성하도록 변경했다.

import javax.inject.Qualifier

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

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthNotRequired
@AuthRequired
@Singleton
@Provides
fun provideOkHttpClient(
    @Logging loggingInterceptor: Interceptor,
    @Auth authInterceptor: Interceptor,
    authenticator: Authenticator
): OkHttpClient = OkHttpClient.Builder()
    .addInterceptor(loggingInterceptor)
    .addInterceptor(authInterceptor)
    .authenticator(authenticator)
    .build()

@AuthNotRequired
@Singleton
@Provides
fun provideOkHttpClientAuthNotRequired(
    @Logging logInterceptor: Interceptor,
): OkHttpClient = OkHttpClient.Builder()
    .addInterceptor(logInterceptor)
    .build()

@AuthRequired
@Singleton
@Provides
fun provideRetrofit(
    @AuthRequired client: OkHttpClient,
    converterFactory: Converter.Factory
): Retrofit = Retrofit.Builder()
    .baseUrl(BuildConfig.AUTH_BASE_URL)
    .client(client)
    .addConverterFactory(converterFactory)
    .build()

@AuthNotRequired
@Singleton
@Provides
fun provideRetrofitAuthNotRequired(
    @AuthNotRequired client: OkHttpClient,
    converterFactory: Converter.Factory,
): Retrofit = Retrofit.Builder()
    .baseUrl(BuildConfig.AUTH_BASE_URL)
    .client(client)
    .addConverterFactory(converterFactory)
    .build()
@Module
@InstallIn(SingletonComponent::class)
object ServiceModule {
    @Provides
    @Singleton
    fun provideAuthService(@AuthRequired retrofit: Retrofit): AuthService = retrofit.create()
}
@Module
@InstallIn(SingletonComponent::class)
object ServiceModule {
    @Provides
    @Singleton
    fun provideReissueTokenService(@AuthNotRequired retrofit: Retrofit): ReissueTokenService = retrofit.create()
}

🙏 오늘의 교훈

서버로부터 액세스 토큰을 재발급 받을 때는, 추가 인증 없이 (유효 기간이 지나지 않은) 리프레시 토큰만 갖고 재발급 받게 된다. 리프레시 토큰 자체가 자격 증명 역할을 하는 것이다.

따라서, 액세스 토큰 재발급 API는 인증이 필요한 다른 API와는 구분지어서 구현해야 함을 알 수 있었다.

이를 위해 @AuthRequired, @AuthNotRequired 한정자로 구분지어서 별도로 OkHttpClient, Retrofit 객체를 생성한 것이다. (힐트의 도움을 받아서)

참고: JWT 인증 방식이란?
https://github.com/leeeha/Android-TIL/blob/main/CS/Network/cookie-session-token-jwt.md#jwt%EC%9D%B4%EB%9E%80

profile
습관이 될 때까지 📝

0개의 댓글