-
Notifications
You must be signed in to change notification settings - Fork 3
[FIX] 토큰 재발급 및 실패시 로직 수정 #87
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
base: develop
Are you sure you want to change the base?
Conversation
Walkthrough토큰 재발급 흐름을 TokenService로 분리하고, TokenAuthenticator에서 재발급/실패 처리 및 재시도 제어를 구현. TokenManager에 logout 이벤트(SharedFlow)를 추가하고, MainActivity가 이를 구독해 앱을 재시작하는 로그아웃 플로우를 연결. AuthService/AuthRepository의 reissueToken 메서드는 제거. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant App as App (Call)
participant Auth as TokenAuthenticator
participant TM as TokenManager
participant TS as TokenService
participant UI as MainActivity
User->>App: API 요청 (Authorization: AccessToken)
App-->>Auth: 401 Unauthorized 발생
alt 처음 재시도
Auth->>TM: refreshToken 조회
alt refreshToken 존재
Auth->>TS: POST /api/users/reissue-token(refreshToken)
alt 재발급 성공
TS-->>Auth: new access/refresh tokens
Auth->>TM: 새 토큰 저장
Auth-->>App: 새 AccessToken으로 요청 재작성(+Retry-With-New-Token)
App->>Server: 재시도 요청
else 재발급 실패
TS--x Auth: 예외/실패
Auth->>TM: clearToken()
TM-->>UI: logoutEvent emit
Auth-->>App: null(재시도 중단)
end
else refreshToken 없음
Auth->>TM: clearToken()
TM-->>UI: logoutEvent emit
Auth-->>App: null
end
else 이미 재시도된 요청
Auth-->>App: null
end
UI->>UI: logoutEvent 수신 시 Activity 재시작(태스크 클리어)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes(없음) Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (3)
app/src/main/java/com/kuit/ourmenu/MainActivity.kt (1)
40-50: 로그아웃 이벤트 수집을 lifecycle-aware하게 + 중복 재시작 방지현재 collect는 액티비티 수명주기와 느슨하게 결합되어 있고, 연속 이벤트에 중복 재시작이 발생할 수 있습니다. STARTED 상태에서 1회만 처리하도록 조정하면 안전합니다.
적용 diff:
@@ -import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle @@ -import kotlinx.coroutines.launch +import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.take @@ - lifecycleScope.launch { - tokenManager.logoutEvent.collect { - // Activity 스택을 모두 클리어하고 MainActivity를 다시 시작 - val intent = Intent(this@MainActivity, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - startActivity(intent) - finish() - } - } + lifecycleScope.launch { + repeatOnLifecycle(androidx.lifecycle.Lifecycle.State.STARTED) { + tokenManager.logoutEvent + .take(1) // 한 번만 처리 + .collect { + val intent = Intent(this@MainActivity, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + startActivity(intent) + finish() + } + } + }참고: 이벤트 미수신을 더 확실히 막으려면 Application(ProcessLifecycleOwner) 레벨에서 수집하는 것도 고려해 보세요.
Also applies to: 16-17, 28-29
app/src/main/java/com/kuit/ourmenu/utils/auth/TokenAuthenticator.kt (2)
37-46: 매 401마다 Retrofit/OkHttp 인스턴스 생성 → 성능·구성 일관성 저하401 발생 때마다 클라이언트를 새로 만들면 오버헤드가 크고, 전역 타임아웃/인터셉터/핀닝 설정과도 어긋날 수 있습니다. DI로 분리된 “refresh 전용” TokenService(Authenticator 미적용, 공통 타임아웃/핀닝 적용)를 주입해 재사용하세요.
원한다면 Hilt 모듈 예시까지 포함해 리팩터링 패치를 드릴게요.
28-35: 동시성: 동시 401 폭주 시 단일-비행(single-flight) 보호 권장여러 요청이 동시에 401을 맞으면 refresh가 중복 호출됩니다. Mutex로 refresh 구간을 직렬화하면 불필요한 트래픽과 경합을 줄일 수 있습니다.
원하면 Mutex 적용 예시(토큰 캐시 재검증 포함) 패치 드리겠습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
app/src/main/java/com/kuit/ourmenu/MainActivity.kt(2 hunks)app/src/main/java/com/kuit/ourmenu/data/repository/AuthRepository.kt(0 hunks)app/src/main/java/com/kuit/ourmenu/data/service/AuthService.kt(0 hunks)app/src/main/java/com/kuit/ourmenu/data/service/TokenService.kt(1 hunks)app/src/main/java/com/kuit/ourmenu/utils/auth/TokenAuthenticator.kt(2 hunks)app/src/main/java/com/kuit/ourmenu/utils/auth/TokenManager.kt(3 hunks)
💤 Files with no reviewable changes (2)
- app/src/main/java/com/kuit/ourmenu/data/repository/AuthRepository.kt
- app/src/main/java/com/kuit/ourmenu/data/service/AuthService.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: ci-build
🔇 Additional comments (1)
app/src/main/java/com/kuit/ourmenu/utils/auth/TokenAuthenticator.kt (1)
23-27: 무한 재시도 방지 가드 좋습니다재시도 플래그 헤더로 루프 차단하는 접근 적절합니다. 👍
| suspend fun reissueToken( | ||
| @Body refreshToken: String | ||
| ): BaseResponse<ReissueTokenResponse> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Retrofit+kotlinx-serialization에서 @Body String은 변환 불가 → 요청 모델 필요
Json 컨버터만 등록된 상태에서 @Body String은 지원되지 않습니다. 런타임에 “no converter for String” 오류가 납니다. 요청 DTO를 정의해 주세요.
적용 diff:
@@
-import com.kuit.ourmenu.data.model.auth.response.ReissueTokenResponse
+import com.kuit.ourmenu.data.model.auth.response.ReissueTokenResponse
+import com.kuit.ourmenu.data.model.auth.request.ReissueTokenRequest
@@
- suspend fun reissueToken(
- @Body refreshToken: String
- ): BaseResponse<ReissueTokenResponse>
+ suspend fun reissueToken(
+ @Body body: ReissueTokenRequest
+ ): BaseResponse<ReissueTokenResponse>추가 파일(예시):
// app/src/main/java/com/kuit/ourmenu/data/model/auth/request/ReissueTokenRequest.kt
package com.kuit.ourmenu.data.model.auth.request
import kotlinx.serialization.Serializable
@Serializable
data class ReissueTokenRequest(val refreshToken: String)Also applies to: 3-5
🤖 Prompt for AI Agents
In app/src/main/java/com/kuit/ourmenu/data/service/TokenService.kt around lines
10-12, the Retrofit interface currently declares suspend fun reissueToken(@Body
refreshToken: String): BaseResponse<ReissueTokenResponse> which fails at runtime
because kotlinx-serialization's JSON converter does not support raw String
bodies; create a request DTO (e.g.
app/src/main/java/com/kuit/ourmenu/data/model/auth/request/ReissueTokenRequest.kt
annotated @Serializable with a single property refreshToken: String), update the
interface method to accept @Body ReissueTokenRequest, and update any call sites
to wrap the raw token string in the new DTO (apply the same change for the
similar methods referenced at lines 3-5).
| val tokenResponse = tokenService.reissueToken( | ||
| refreshToken = refreshToken | ||
| ) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reissueToken 호출 시 String 바디 사용 문제 (실패 원인)
TokenService가 요청 DTO를 받도록 바뀌면 여기 호출도 DTO로 교체해야 합니다.
적용 diff:
+import com.kuit.ourmenu.data.model.auth.request.ReissueTokenRequest
@@
- val tokenResponse = tokenService.reissueToken(
- refreshToken = refreshToken
- )
+ val tokenResponse = tokenService.reissueToken(
+ body = ReissueTokenRequest(refreshToken = refreshToken)
+ )Also applies to: 3-7
🤖 Prompt for AI Agents
In app/src/main/java/com/kuit/ourmenu/utils/auth/TokenAuthenticator.kt around
lines 43 to 46 (and also check occurrences at lines ~3-7), the call to
tokenService.reissueToken currently passes a raw String refreshToken body but
TokenService signature has changed to accept a request DTO; replace the String
argument with the appropriate ReissueTokenRequest DTO instance (e.g.,
ReissueTokenRequest(refreshToken)) and import/use that DTO, updating the call
site to match the new method signature and adjust any surrounding code/error
handling accordingly.
| tokenManager.saveAccessToken(tokenResponse.response?.accessToken ?: "") | ||
| tokenManager.saveRefreshToken(tokenResponse.response?.refreshToken ?: "") | ||
| Log.d("TokenAuthenticator", "토큰 재발급 성공: ${tokenResponse.response?.accessToken}") | ||
|
|
||
| newToken?.let { | ||
| tokenManager.saveAccessToken(it.accessToken) | ||
| // 기존 요청에 새로운 토큰으로 헤더를 추가하여 재시도 | ||
| response.request.newBuilder() | ||
| .header("Authorization", "Bearer ${it.accessToken}") | ||
| .header("Authorization", "Bearer ${tokenResponse.response?.accessToken}") | ||
| .header("Retry-With-New-Token", "true") | ||
| .build() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
널/빈 토큰 저장 및 ‘Bearer null’ 헤더 위험 + 민감정보 로그 노출
- 응답 토큰이 null/빈 문자열일 때 빈 값을 저장하고 ‘Bearer null’을 보낼 수 있습니다.
- 액세스 토큰을 로그에 그대로 남기는 건 보안 리스크입니다.
적용 diff:
@@
- // 새로운 토큰 저장
- tokenManager.saveAccessToken(tokenResponse.response?.accessToken ?: "")
- tokenManager.saveRefreshToken(tokenResponse.response?.refreshToken ?: "")
- Log.d("TokenAuthenticator", "토큰 재발급 성공: ${tokenResponse.response?.accessToken}")
+ // 새로운 토큰 저장 (검증)
+ val newAccess = tokenResponse.response?.accessToken?.takeIf { it.isNotBlank() }
+ val newRefresh = tokenResponse.response?.refreshToken?.takeIf { it.isNotBlank() }
+ if (newAccess == null || newRefresh == null) {
+ Log.w("TokenAuthenticator", "토큰 재발급 응답에 토큰이 없습니다.")
+ tokenManager.clearToken()
+ return@runBlocking null
+ }
+ tokenManager.saveAccessToken(newAccess)
+ tokenManager.saveRefreshToken(newRefresh)
+ Log.d("TokenAuthenticator", "토큰 재발급 성공") // 민감정보 비노출
@@
- .header("Authorization", "Bearer ${tokenResponse.response?.accessToken}")
+ .header("Authorization", "Bearer $newAccess")
.header("Retry-With-New-Token", "true")
.build()
@@
- // 토큰 재발급 실패 시 토큰 클리어
- Log.d("TokenAuthenticator", "토큰 재발급 실패: ${e.message}")
+ // 토큰 재발급 실패 시 토큰 클리어 (민감정보 비노출)
+ Log.d("TokenAuthenticator", "토큰 재발급 실패: ${e.message}")
tokenManager.clearToken()
nullAlso applies to: 57-62, 50-51
| private val _logoutEvent = MutableSharedFlow<Unit>() | ||
| val logoutEvent = _logoutEvent.asSharedFlow() | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SharedFlow 기본값으로 emit이 영구 대기할 수 있음 (OkHttp 스레드 정지 위험) + 타입 명시 제안
clearToken()에서 _logoutEvent.emit(Unit)은 수집자가 없을 때 suspend됩니다. 현재 TokenAuthenticator.authenticate()에서 runBlocking 내부에서 clearToken()을 호출하므로, 수집자가 없으면 OkHttp 네트워크 스레드가 멈출 수 있습니다. 버퍼를 주고 tryEmit으로 전환해 비차단으로 바꿔주세요. 아울러 공개 API 가독성을 위해 타입을 명시하면 좋습니다.
적용 diff:
@@
-import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
@@
- private val _logoutEvent = MutableSharedFlow<Unit>()
- val logoutEvent = _logoutEvent.asSharedFlow()
+ private val _logoutEvent = MutableSharedFlow<Unit>(replay = 0, extraBufferCapacity = 1)
+ val logoutEvent: SharedFlow<Unit> = _logoutEvent.asSharedFlow()
@@
- _logoutEvent.emit(Unit)
+ _logoutEvent.tryEmit(Unit)Also applies to: 51-57, 9-11
🤖 Prompt for AI Agents
In app/src/main/java/com/kuit/ourmenu/utils/auth/TokenManager.kt around lines
9-11, 19-21 and 51-57, the MutableSharedFlow is created with default settings
and clearToken() calls emit from a suspend context inside runBlocking, which can
block OkHttp threads; change the flow to a non-blocking buffered shared flow and
make public types explicit: declare private val _logoutEvent:
MutableSharedFlow<Unit> = MutableSharedFlow(replay = 0, extraBufferCapacity = 1)
and public val logoutEvent: SharedFlow<Unit> to improve readability, then
replace any calls to _logoutEvent.emit(Unit) with the non-suspending
_logoutEvent.tryEmit(Unit) in clearToken() (and any other emit sites) so
emission is non-blocking.
ikseong00
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
토끼가 말해준대로, reissue 시, 리퀘스트 객체를 생성해 만들어야할 것 같습니다!
반영 후에 merge 하겠습니다 |
🚀 이슈번호
✏️ 변경사항
📷 스크린샷
✍️ 사용법
🎸 기타
Summary by CodeRabbit