Skip to content

Conversation

@casper-jr
Copy link
Member

@casper-jr casper-jr commented Sep 2, 2025

🚀 이슈번호

✏️ 변경사항

  • 토큰 재발급 요청시 사용할 Service를 분리하여 무한루프 방지
  • 기존 토큰 재발급 로직 수정
  • 토큰 재발급 실패시 로그아웃 및 Activity 재시작 구현

📷 스크린샷

✍️ 사용법

🎸 기타

Summary by CodeRabbit

  • 신기능
    • 세션 만료 또는 로그아웃 발생 시 앱이 즉시 초기 화면으로 안전하게 복귀하도록 처리하여 사용자 경험 개선.
  • 버그 수정
    • 토큰 재발급 실패 시 무한 재시도되는 문제를 방지하고, 예외 처리 강화를 통해 비정상 로그인 상태 지속을 차단.
  • 리팩터링
    • 인증 토큰 재발급 흐름을 정비하여 안정성과 신뢰도를 높임.

@casper-jr casper-jr self-assigned this Sep 2, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 2, 2025

Walkthrough

토큰 재발급 흐름을 TokenService로 분리하고, TokenAuthenticator에서 재발급/실패 처리 및 재시도 제어를 구현. TokenManager에 logout 이벤트(SharedFlow)를 추가하고, MainActivity가 이를 구독해 앱을 재시작하는 로그아웃 플로우를 연결. AuthService/AuthRepository의 reissueToken 메서드는 제거.

Changes

Cohort / File(s) Summary
토큰 재발급 인프라 추가/변경
app/src/main/java/com/kuit/ourmenu/utils/auth/TokenAuthenticator.kt, app/src/main/java/com/kuit/ourmenu/data/service/TokenService.kt, app/src/main/java/com/kuit/ourmenu/utils/auth/TokenManager.kt
Authenticator에서 Retrofit 기반 TokenService로 재발급 호출, 무한 재시도 방지 헤더 추가, 성공 시 토큰 저장/요청 재시도, 실패 시 토큰 삭제 및 로그아웃 이벤트 발행. TokenManagerlogoutEvent 공개.
API 분리 및 정리
app/src/main/java/com/kuit/ourmenu/data/service/AuthService.kt, app/src/main/java/com/kuit/ourmenu/data/repository/AuthRepository.kt
reissueToken 엔드포인트/메서드 제거. 재발급은 TokenService로 이전.
UI 로그아웃 반응 처리
app/src/main/java/com/kuit/ourmenu/MainActivity.kt
TokenManager 주입 및 logoutEvent 구독. 이벤트 수신 시 태스크 스택 클리어 후 MainActivity 재시작.

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 재시작(태스크 클리어)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
데이터스토어를 통한 토큰 관리 구현 (#46) 데이터스토어 구현 여부는 본 변경에서 확인 불가.
로그인 후 토큰 저장 (#46) 로그인 저장 로직은 이번 변경 범위에 포함되지 않음.
access token 재발급 로직 작성 (#46)
재발급 실패 시 로그아웃 처리 (#46)

Assessment against linked issues: Out-of-scope changes

(없음)

Poem

토큰 달, 밤하늘에 새로이 떴네 🌙
실패 바람 불어오면, 조용히 문을 닫고
앱은 다시 숨 고르고, 처음처럼 시작해—
작디작은 Unit 신호에 춤추는 화면 💫
리프레시의 길, 한 줄로 곱게 이어졌다.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a 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.

📥 Commits

Reviewing files that changed from the base of the PR and between cce63d7 and 320e624.

📒 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: 무한 재시도 방지 가드 좋습니다

재시도 플래그 헤더로 루프 차단하는 접근 적절합니다. 👍

Comment on lines +10 to +12
suspend fun reissueToken(
@Body refreshToken: String
): BaseResponse<ReissueTokenResponse>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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).

Comment on lines +43 to +46
val tokenResponse = tokenService.reissueToken(
refreshToken = refreshToken
)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Comment on lines +48 to 56
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()
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

널/빈 토큰 저장 및 ‘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()
                 null

Also applies to: 57-62, 50-51

Comment on lines +19 to +21
private val _logoutEvent = MutableSharedFlow<Unit>()
val logoutEvent = _logoutEvent.asSharedFlow()

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Copy link
Collaborator

@ikseong00 ikseong00 left a comment

Choose a reason for hiding this comment

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

토끼가 말해준대로, reissue 시, 리퀘스트 객체를 생성해 만들어야할 것 같습니다!

@casper-jr
Copy link
Member Author

토끼가 말해준대로, reissue 시, 리퀘스트 객체를 생성해 만들어야할 것 같습니다!

반영 후에 merge 하겠습니다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] TokenManager 구현

2 participants