Skip to content

[FEAT/#245] SSE BackOff 적용 및 연결 상태 관리 리팩 - #312

Merged
seungjunGong merged 16 commits into
developfrom
feat/#245-sse_backoff
Apr 26, 2026
Merged

[FEAT/#245] SSE BackOff 적용 및 연결 상태 관리 리팩#312
seungjunGong merged 16 commits into
developfrom
feat/#245-sse_backoff

Conversation

@seungjunGong

@seungjunGong seungjunGong commented Apr 9, 2026

Copy link
Copy Markdown
Member

Related issue 🛠

Work Description ✏️

  • SseManager에 SSE 재연결 백오프 로직 추가
    • 지수 backoff + full jitter 적용
    • minDelay = 1s, maxDelay = 120s, maxAttempts = 30 (평균 약 1분 간격으로 최대 약 50분 내 재시도)
  • 앱 foreground/background 및 로그인 상태 변화를 observe 하여 연결 상태 반영
  • EventRemoteDataSourceImpl 동시성 보강
    • eventSource 접근을 동일 락(synchronized)으로 보호
  • 기존 토큰 재발급 로직을 SSE 흐름에 연결하기 위해 TokenReissueUseCase 기반으로 재발급 경로 통합
    • 원래 SseManger 에서 재발급 로직을 호출하려고 통합 했으나,, Authenticator 로 단일 진입점으로 사용하도록 변경
  • 로그인 상태 관리를 SseManager 임시 관리 방식에서 AuthManager 기반 상태 관리로 정리

Screenshot 📸

Backoff 재연결 검증 로그
토큰 재발급 검증 로그

Uncompleted Tasks 😅

  • 백오프/재연결 단위 테스트 추가
  • 재시도 한도 초과 시 사용자 노출 UX(배너/토스트) 정책 확정

To Reviewers 📢

  • 이번 변경은 QA 이슈 수정이라기보다, SSE 재연결 안정성을 높이기 위한 구조 개선입니다.
  • 토큰 재발급은 강제 로그아웃을 통해 토큰을 만료시킨 시나리오에서 정상 동작을 확인했습니다.
    • 로그아웃(강제 토큰 만료) -> background -> forground(재연결 시도) -> 토큰 만료 -> 재발급 -> 토큰만료로 인해 로그인 화면 이동
  • backoff는 네트워크 차단/복구 상황에서 SSE 이벤트 재수신이 정상 동작하는 것을 확인했습니다.

기존의 Token 재발급 로직을 SSE 에 붙이는 과정에서 TokenUseCase 를 만들어서 통합하고
-> 중복 재호출 가능성을 줄이고자 Authenticator 를 사용해서 단일진입점을 만들었습니다.
로그인 상태 관리를 위해 임시로 SseManger 에서 임시로 관리하던 로그인 상태 로직을 AuthManger 로 이동시켰습니다.

작업하다보니 수정 사항이 많아졌네요..😅 양해부탁드립니다.

Summary by CodeRabbit

  • 버그 수정

    • 앱 포그라운드/백그라운드 전환 시 실시간(SSE) 연결 관리 안정화
    • 연결 오류 시 지터 기반 지연 및 시도 제한을 통한 재접속 개선
    • 토큰 재발급 및 자동 로그인 실패 처리 강화로 세션 안정성 향상
    • 강제 로그아웃 트리거 처리 개선으로 로그인 상태 동기화 정확도 향상
  • 리팩토링

    • 인증·실시간 연결 관련 내부 흐름과 상태 노출 방식 정리로 유지보수성 향상

- `AuthManager`에 유저 로그인 상태(`isUserLoggedIn`) 및 강제 로그아웃 이벤트(`forceLogoutEvent`) 추가
- `AuthManagerImpl`에서 `AtomicBoolean`을 사용하여 중복 인증 실패 처리 방지 가드 구현
- `SseManager`가 `AuthManager`의 로그인 상태를 구독하여 연결을 자동으로 관리하도록 수정 (기존 ViewModel에서의 직접 호출 제거)
- `TokenAuthenticator` 및 각 ViewModel(`Login`, `SignUp`, `Splash`, `MyPage`, `Withdraw`)에서 `SseManager` 의존성을 `AuthManager`로 교체 및 관련 메서드 호출 수정
- `MainActivity`에서 `authEvent` 대신 `forceLogoutEvent`를 구독하도록 변경
- [FEAT] Mutex를 활용하여 동시 토큰 재발급 요청을 처리하는 `TokenReissueUseCase` 추가
- [REFACTOR] `SplashViewModel`의 토큰 재발급 로직을 `TokenReissueUseCase`로 이관
- [REFACTOR] `TokenAuthenticator`에서 중복되는 재발급 로직을 `TokenReissueUseCase` 사용으로 수정하여 단순화
- SSE 연결 오류 시 HTTP 상태 코드를 전달할 수 있도록 `SseConnectionState.Error` 필드 추가
- `SseManager` 내 401 에러 감지 시 `TokenReissueUseCase`를 통한 토큰 재발급 처리 추가
- 포그라운드/백그라운드 및 로그인 상태를 고려한 SSE 연결 관리 로직 최적화 (Mutex 활용)
- `MainActivity`의 생명주기 메서드명을 `onAppForegrounded`, `onAppBackgrounded`로 변경하여 의도 명확화
- `EventRepository` 및 `EventRemoteDataSource` 인터페이스에 `connectionState` 노출 관점 추가
- SSE 연결 상태(Error, Disconnected)에 따른 재연결 스케줄링 추가
- Exponential Backoff 및 Full Jitter 알고리즘을 적용한 지연 시간 계산 로직 도입
- 최대 재연결 시도 횟수(30회) 및 최대 지연 시간(120초) 제한 설정
- 토큰 재발급 또는 앱 상태(Background)에 따른 연결 관리 로직 보완 및 Coroutine Job 제어 추가
@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown

Walkthrough

토큰 재발급 책임을 TokenReissueUseCase로 이전하고, 인증 상태 API를 isUserLoggedIn/forceLogoutEvent로 변경했습니다. SSE 연결은 앱 포그라운드·로그인 기반 자동 관리 및 지수 백오프 재연결로 리팩토링되었고, Event 데이터 소스·레포 및 네트워크 DI가 이에 맞게 조정되었습니다.

Changes

Cohort / File(s) Summary
Token reissue & authenticator
app/src/main/java/com/smashing/app/core/network/TokenAuthenticator.kt, app/src/main/java/com/smashing/app/domain/usecase/auth/TokenReissueUseCase.kt, app/src/main/java/com/smashing/app/data/repository/impl/AuthRepositoryImpl.kt
재발급 로직을 TokenReissueUseCase로 이동(동시성 Mutex 적용). TokenAuthenticator는 UseCase 호출만 수행하도록 변경. 리포지토리에서 재발급 응답 토큰을 영구 저장하도록 추가.
Auth manager API & impl
app/src/main/java/com/smashing/app/core/network/token/AuthManager.kt, app/src/main/java/com/smashing/app/core/network/token/AuthManagerImpl.kt
authEvent 제거, val isUserLoggedIn: StateFlow<Boolean>val forceLogoutEvent: SharedFlow<Unit> 도입. onUserLoggedIn(), onUserLoggedOut(), onAuthFailure() 추가 및 인증 실패의 일회성 처리 가드 구현.
SSE connection state & manager
app/src/main/java/com/smashing/app/core/network/sse/SseConnectionState.kt, app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt
SseConnectionState.ErrorstatusCode: Int? 사용, Retryingattempt/delayMs로 변경. SseManager는 수동 connect/disconnect 제거, onAppForegrounded()/onAppBackgrounded() 추가, 로그인 상태와 연동된 자동 생명주기 및 지수 백오프 재연결 구현.
SSE data source & repo
app/src/main/java/com/smashing/app/data/remote/datasource/api/EventRemoteDatasource.kt, app/src/main/java/com/smashing/app/data/remote/datasource/impl/EventRemoteDataSourceImpl.kt, app/src/main/java/com/smashing/app/data/repository/api/EventRepository.kt, app/src/main/java/com/smashing/app/data/repository/impl/EventRepositoryImpl.kt
connectionState: StateFlow<SseConnectionState> 공개 추가. DataSourceImpl에 동기화 락과 콜백의 stale-source 검사 추가, 실패 시 상태에 statusCode 전달. Repository는 connectionState 위임, 이벤트 파싱/emit 로직을 분리하고 SharedFlow 노출을 안전하게 변경.
Network DI
app/src/main/java/com/smashing/app/core/network/di/NetworkModule.kt
SSE 전용 OkHttpClient에 TokenAuthenticator 주입 및 .authenticator(authenticator) 등록.
UI ViewModels wiring
app/src/main/java/com/smashing/app/presentation/.../*ViewModel.kt
app/src/main/java/com/smashing/app/presentation/login/LoginViewModel.kt, .../signup/SignUpViewModel.kt, .../mypage/MyPageViewModel.kt, .../withdraw/WithdrawViewModel.kt, .../splash/SplashViewModel.kt
다수 ViewModel의 DI를 SseManagerAuthManager로 교체. 로그인/로그아웃/회원가입/탈퇴 성공 경로에서 authManager.onUserLoggedIn()/onUserLoggedOut() 호출. SplashViewModelTokenReissueUseCase를 직접 호출하도록 변경.
Activity lifecycle
app/src/main/java/com/smashing/app/presentation/main/MainActivity.kt
인증 이벤트 수신을 authManager.forceLogoutEvent로 변경. SSE 생명주기 호출을 connect()/disconnect()에서 onAppForegrounded()/onAppBackgrounded()로 전환; 기존 onDestroy() 기반 disconnect 로직 제거.
Release notes
fastlane/notes/qa/debug.txt
QA 배포 노트 업데이트(디버그 항목 교체).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • vahkjsdf
  • ShinHyeongcheol
  • oilbeaneda
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경 사항인 SSE 백오프 재연결 로직 추가와 연결 상태 관리 리팩토링을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed PR이 연결된 이슈 #245의 두 가지 목표(SSE BackOff 전략 적용, 재연결 로직 로그인 flow에 맞게 수정)를 모두 충족합니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 SSE 재연결 백오프 로직, 인증 상태 관리, 그리고 동시성 보강이라는 범위 내에 있으며, 관련 이슈의 목표와 일치합니다.
Description check ✅ Passed PR 설명이 템플릿 구조를 대부분 따르고 있으며, 관련 이슈, 작업 내용, 스크린샷, 미완료 작업, 리뷰어 주석이 모두 포함되어 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#245-sse_backoff

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@seungjunGong seungjunGong changed the title Feat/#245 sse backoff [FEAT/#245] SSE BackOff 적용 및 연결 상태 관리 리팩 Apr 9, 2026
@seungjunGong seungjunGong added the ✨ feat 새로운 기능 추가 label Apr 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
app/src/main/java/com/smashing/app/presentation/main/MainActivity.kt (1)

68-73: onDestroy()에서 onAppBackgrounded() 호출이 중복될 수 있습니다.

onStop()은 항상 onDestroy() 이전에 호출되므로, onDestroy()에서 다시 onAppBackgrounded()를 호출하면 중복 실행됩니다. SseManager의 mutex 보호로 인해 기능상 문제는 없지만, 불필요한 코루틴 실행과 락 획득이 발생합니다.

onDestroy()onAppBackgrounded() 호출을 제거하는 것을 권장합니다.

♻️ 중복 호출 제거 제안
     override fun onDestroy() {
         super.onDestroy()
-        if (!isChangingConfigurations) {
-            sseManager.onAppBackgrounded()
-        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/smashing/app/presentation/main/MainActivity.kt` around
lines 68 - 73, onDestroy currently calls sseManager.onAppBackgrounded() guarded
by isChangingConfigurations, but onStop already calls
sseManager.onAppBackgrounded(), causing duplicate execution; remove the
sseManager.onAppBackgrounded() call (and its isChangingConfigurations guard)
from MainActivity.onDestroy() so the backgrounding logic only runs from
onStop(), leaving onStop(), sseManager.onAppBackgrounded(), and
isChangingConfigurations elsewhere unchanged.
app/src/main/java/com/smashing/app/data/remote/datasource/impl/EventRemoteDataSourceImpl.kt (1)

85-90: onClosed에서 상태 업데이트 순서를 검토해 주세요.

현재 eventSourcenull로 설정한 후 synchronized 블록 밖에서 _connectionState.value를 업데이트합니다. connect()가 동시에 호출되면 Disconnected 상태 설정 전에 새 연결이 시작될 수 있습니다.

일관성을 위해 onFailure와 동일한 패턴(상태 업데이트 후 synchronized 블록 내에서 null 처리)을 고려해 보세요.

♻️ 순서 일관성 제안
                 override fun onClosed(eventSource: EventSource) {
+                    _connectionState.value = SseConnectionState.Disconnected
                     synchronized(eventSourceLock) {
                         this@EventRemoteDataSourceImpl.eventSource = null
                     }
-                    _connectionState.value = SseConnectionState.Disconnected
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/smashing/app/data/remote/datasource/impl/EventRemoteDataSourceImpl.kt`
around lines 85 - 90, onClosed currently nulls
this@EventRemoteDataSourceImpl.eventSource inside the
synchronized(eventSourceLock) block and then updates _connectionState.value to
SseConnectionState.Disconnected outside it, which allows connect() to race in
and replace the source before the state flips; change the sequence to match
onFailure: first set _connectionState.value = SseConnectionState.Disconnected,
then inside synchronized(eventSourceLock) set
this@EventRemoteDataSourceImpl.eventSource = null so the state and eventSource
are updated atomically relative to connect()—update the onClosed implementation
accordingly, referencing onClosed, eventSourceLock,
this@EventRemoteDataSourceImpl.eventSource, _connectionState, onFailure, and
connect().
app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt (2)

240-244: 타입 일관성 개선 고려

MAX_RECONNECT_DELAY_MSDouble(120_000.0)로 선언되어 있고 min() 결과를 toLong()으로 변환하고 있습니다. 타입 일관성을 위해 Long 타입을 사용하고 coerceAtMost를 활용하는 것이 더 명확합니다.

♻️ 제안된 수정
     private fun calculateReconnectDelayMs(attempt: Int): Long {
-        val exponentialDelay = MIN_RECONNECT_DELAY_MS * 2.0.pow(attempt.toDouble())
-        val cappedDelay = min(MAX_RECONNECT_DELAY_MS, exponentialDelay).toLong()
+        val exponentialDelay = MIN_RECONNECT_DELAY_MS shl attempt
+        val cappedDelay = exponentialDelay.coerceAtMost(MAX_RECONNECT_DELAY_MS)
         return Random.nextLong(0L, cappedDelay + 1L)
     }
 
     companion object {
         private const val TAG = "SSE LOG"
         private const val AUTH_FAILURE_CODE = 401
         private const val MIN_RECONNECT_DELAY_MS = 1_000L
-        private const val MAX_RECONNECT_DELAY_MS = 120_000.0
+        private const val MAX_RECONNECT_DELAY_MS = 120_000L
         private const val MAX_RECONNECT_ATTEMPTS = 30
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt` around
lines 240 - 244, The function calculateReconnectDelayMs uses Double math and
min() then toLong(), causing type inconsistency; change MAX_RECONNECT_DELAY_MS
(and any related constants) to Long (e.g., 120_000L), compute exponentialDelay
as Long (use shifting or Long-based calculation), then replace min(...) with
kotlin's coerceAtMost on Long values (e.g., cappedDelay =
exponentialDelay.coerceAtMost(MAX_RECONNECT_DELAY_MS)) and return
Random.nextLong(0L, cappedDelay + 1L); update MIN_RECONNECT_DELAY_MS /
MAX_RECONNECT_DELAY_MS types and any uses accordingly to maintain Long
consistency.

111-128: tokenReissueUseCase 호출 시 previousAccessToken 전달 고려

tokenReissueUseCase()를 파라미터 없이 호출하고 있습니다. TokenReissueUseCase의 double-check 최적화는 previousAccessToken을 사용하여 이미 재발급된 토큰을 감지합니다. 현재 액세스 토큰을 전달하면 TokenAuthenticator와의 동시 호출 시 불필요한 재발급 요청을 방지할 수 있습니다.

♻️ 제안된 수정
     private fun handleSseAuthFailure() {
         scope.launch {
             if (!tryStartReissue()) return@launch
 
-            val reissueResult = tokenReissueUseCase()
+            val currentAccessToken = tokenDataSource.getAccessToken()
+            val reissueResult = tokenReissueUseCase(previousAccessToken = currentAccessToken)
 
             mutex.withLock {

이를 위해 LocalTokenDataSource를 생성자에 주입해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt` around
lines 111 - 128, In handleSseAuthFailure(), pass the current stored access token
into tokenReissueUseCase to enable its double-check optimization: inject
LocalTokenDataSource into SseManager (add it to the constructor and store it)
and call tokenReissueUseCase(previousAccessToken =
localTokenDataSource.getAccessToken()) instead of tokenReissueUseCase() inside
the launched coroutine; keep existing synchronization around tryStartReissue,
mutex.withLock, isReissuingToken toggling, and the reconnect logic
(reconnectAttempt reset, cancel reconnectJob, eventRepository.connect())
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/smashing/app/core/network/token/AuthManagerImpl.kt`:
- Around line 28-43: The transitions between onUserLoggedIn, onUserLoggedOut and
onAuthFailure are not atomic and a late onAuthFailure can flip _isUserLoggedIn
and emit _forceLogoutEvent for a new session; fix by introducing a single shared
lock/mutex and perform the state changes for authFailureHandled, _isUserLoggedIn
and the _forceLogoutEvent emission inside that lock in onUserLoggedIn,
onUserLoggedOut and onAuthFailure so the three methods are synchronized;
specifically ensure onAuthFailure uses the same lock when calling
authFailureHandled.compareAndSet(...), setting _isUserLoggedIn.value = false and
tryEmit(Unit), and that onUserLoggedIn sets authFailureHandled and
_isUserLoggedIn under the same lock to prevent races.

In
`@app/src/main/java/com/smashing/app/domain/usecase/auth/TokenReissueUseCase.kt`:
- Around line 37-49: The null-previousAccessToken path bypasses the
duplicate-reissue guard because isAlreadyReissued treats null/blank as "not
reissued"; update the logic so getReissuedAccessToken(previousAccessToken:
String?) (and/or isAlreadyReissued) first checks the
cached/lastReissuedAccessToken regardless of whether previousAccessToken is null
and returns that cached token when present, so concurrent invoke() calls
(including the SplashViewModel path) reuse an in-flight or already produced
token; ensure the mutex.withLock block still guards postTokenReissue() and that
postTokenReissue() sets the same cached lastReissuedAccessToken that
getReissuedAccessToken reads.

---

Nitpick comments:
In `@app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt`:
- Around line 240-244: The function calculateReconnectDelayMs uses Double math
and min() then toLong(), causing type inconsistency; change
MAX_RECONNECT_DELAY_MS (and any related constants) to Long (e.g., 120_000L),
compute exponentialDelay as Long (use shifting or Long-based calculation), then
replace min(...) with kotlin's coerceAtMost on Long values (e.g., cappedDelay =
exponentialDelay.coerceAtMost(MAX_RECONNECT_DELAY_MS)) and return
Random.nextLong(0L, cappedDelay + 1L); update MIN_RECONNECT_DELAY_MS /
MAX_RECONNECT_DELAY_MS types and any uses accordingly to maintain Long
consistency.
- Around line 111-128: In handleSseAuthFailure(), pass the current stored access
token into tokenReissueUseCase to enable its double-check optimization: inject
LocalTokenDataSource into SseManager (add it to the constructor and store it)
and call tokenReissueUseCase(previousAccessToken =
localTokenDataSource.getAccessToken()) instead of tokenReissueUseCase() inside
the launched coroutine; keep existing synchronization around tryStartReissue,
mutex.withLock, isReissuingToken toggling, and the reconnect logic
(reconnectAttempt reset, cancel reconnectJob, eventRepository.connect())
unchanged.

In
`@app/src/main/java/com/smashing/app/data/remote/datasource/impl/EventRemoteDataSourceImpl.kt`:
- Around line 85-90: onClosed currently nulls
this@EventRemoteDataSourceImpl.eventSource inside the
synchronized(eventSourceLock) block and then updates _connectionState.value to
SseConnectionState.Disconnected outside it, which allows connect() to race in
and replace the source before the state flips; change the sequence to match
onFailure: first set _connectionState.value = SseConnectionState.Disconnected,
then inside synchronized(eventSourceLock) set
this@EventRemoteDataSourceImpl.eventSource = null so the state and eventSource
are updated atomically relative to connect()—update the onClosed implementation
accordingly, referencing onClosed, eventSourceLock,
this@EventRemoteDataSourceImpl.eventSource, _connectionState, onFailure, and
connect().

In `@app/src/main/java/com/smashing/app/presentation/main/MainActivity.kt`:
- Around line 68-73: onDestroy currently calls sseManager.onAppBackgrounded()
guarded by isChangingConfigurations, but onStop already calls
sseManager.onAppBackgrounded(), causing duplicate execution; remove the
sseManager.onAppBackgrounded() call (and its isChangingConfigurations guard)
from MainActivity.onDestroy() so the backgrounding logic only runs from
onStop(), leaving onStop(), sseManager.onAppBackgrounded(), and
isChangingConfigurations elsewhere unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 21166fe5-7eaa-4c79-aebc-8253785572b5

📥 Commits

Reviewing files that changed from the base of the PR and between a56ecdf and 99426fc.

📒 Files selected for processing (18)
  • app/src/main/java/com/smashing/app/core/network/TokenAuthenticator.kt
  • app/src/main/java/com/smashing/app/core/network/sse/SseConnectionState.kt
  • app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt
  • app/src/main/java/com/smashing/app/core/network/token/AuthManager.kt
  • app/src/main/java/com/smashing/app/core/network/token/AuthManagerImpl.kt
  • app/src/main/java/com/smashing/app/data/remote/datasource/api/EventRemoteDatasource.kt
  • app/src/main/java/com/smashing/app/data/remote/datasource/impl/EventRemoteDataSourceImpl.kt
  • app/src/main/java/com/smashing/app/data/repository/api/EventRepository.kt
  • app/src/main/java/com/smashing/app/data/repository/impl/AuthRepositoryImpl.kt
  • app/src/main/java/com/smashing/app/data/repository/impl/EventRepositoryImpl.kt
  • app/src/main/java/com/smashing/app/domain/usecase/auth/TokenReissueUseCase.kt
  • app/src/main/java/com/smashing/app/presentation/login/LoginViewModel.kt
  • app/src/main/java/com/smashing/app/presentation/main/MainActivity.kt
  • app/src/main/java/com/smashing/app/presentation/mypage/MyPageViewModel.kt
  • app/src/main/java/com/smashing/app/presentation/signup/SignUpViewModel.kt
  • app/src/main/java/com/smashing/app/presentation/splash/SplashViewModel.kt
  • app/src/main/java/com/smashing/app/presentation/withdraw/WithdrawViewModel.kt
  • fastlane/notes/qa/debug.txt

Comment thread app/src/main/java/com/smashing/app/core/network/token/AuthManagerImpl.kt Outdated
Comment thread app/src/main/java/com/smashing/app/domain/usecase/auth/TokenReissueUseCase.kt Outdated
- `handleRawEvent`, `parseEvent` 메서드 추출을 통한 관심사 분리
- `_events`를 `asSharedFlow()`로 캡슐화
- `@param:ApplicationScope` 어노테이션 타겟 명시 수정
- 불필요한 null 처리 및 흐름 제어 로직 간소화
- [feat] SSE용 OkHttpClient에 `TokenAuthenticator`를 추가하여 401 에러 시 자동 토큰 재발급 연동
- [refactor] `TokenReissueUseCase`의 중복 재발급 방지 로직을 단순화하고 `mutex` 기반 동기화로 변경
- [refactor] `SseManager` 내 직접적인 토큰 재발급 처리 로직을 제거하고, `Authenticator`를 통한 인증 흐름으로 통합
- [refactor] `EventRemoteDataSourceImpl`의 SSE 리스너를 내부 클래스로 분리하고, 현재 연결 소스(EventSource) 검증 로직 추가
- [style] `SseConnectionState` 모델의 프로퍼티 구성을 재연결 시도 횟수 및 지연 시간 중심으로 변경
- [chore] 재연결 관련 백오프 계산식의 타입 캐스팅 및 상수 값 정리
@seungjunGong

seungjunGong commented Apr 10, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@TEAM-SMASHING TEAM-SMASHING deleted a comment from coderabbitai Bot Apr 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
app/src/main/java/com/smashing/app/domain/usecase/auth/TokenReissueUseCase.kt (1)

33-45: Mutex가 동시 실행은 막지만, 대기 중인 호출의 중복 재발급은 방지하지 못합니다.

현재 구현에서 여러 호출이 동시에 invoke()를 호출하면:

  1. 첫 번째 호출이 락을 획득하고 재발급 성공
  2. 대기 중이던 두 번째 호출이 락을 획득하고 또다시 재발급 수행

Refresh token rotation을 사용하는 서버에서는 불필요한 API 호출과 토큰 충돌이 발생할 수 있습니다. 성공한 재발급 결과를 짧은 TTL로 캐싱하거나, Deferred를 공유하여 대기 중인 호출이 동일 결과를 재사용하도록 개선을 권장합니다.

♻️ Deferred 공유 방식 예시
 `@Singleton`
 class TokenReissueUseCase `@Inject` constructor(
     private val authRepository: AuthRepository,
     private val tokenDataSource: LocalTokenDataSource,
     private val authManager: AuthManager,
 ) {
     private val mutex = Mutex()
+    private var inFlightReissue: Deferred<Result<String>>? = null

     suspend operator fun invoke(): Result<String> = mutex.withLock {
+        // 진행 중인 재발급이 있으면 결과 재사용
+        inFlightReissue?.let { deferred ->
+            if (deferred.isActive) {
+                return@withLock deferred.await()
+            }
+        }
+
         val refreshToken = tokenDataSource.getRefreshToken()
             ?: return handleFailure(IllegalStateException("Refresh token is null"))

-        return authRepository.postTokenReissue(PostTokenReissueRequest(refreshToken))
+        val deferred = scope.async {
+            authRepository.postTokenReissue(PostTokenReissueRequest(refreshToken))
+                .map { it.accessToken }
+        }
+        inFlightReissue = deferred
+
+        return deferred.await()
             .onSuccess {
                 Timber.tag(TAG).d("토큰 재발급 성공")
             }
-            .map { it.accessToken }
             .onFailure { error ->
                 handleFailure(error)
+            }.also {
+                inFlightReissue = null
             }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/smashing/app/domain/usecase/auth/TokenReissueUseCase.kt`
around lines 33 - 45, The invoke() in TokenReissueUseCase currently uses
mutex.withLock but still allows queued callers to perform duplicate reissue
calls; fix this by introducing a shared in-flight marker (e.g., a nullable
Deferred<Result<String>> field like ongoingReissue) that you read/write under
the existing mutex: if ongoingReissue exists return/await it, otherwise create a
new async Deferred that executes
authRepository.postTokenReissue(PostTokenReissueRequest(refreshToken)) (and maps
to accessToken), store it in ongoingReissue, await its result for callers, and
clear ongoingReissue on completion/failure; ensure
tokenDataSource.getRefreshToken and error handling remain unchanged and access
to ongoingReissue is protected by the same mutex.
app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt (1)

140-143: 최대 재시도 도달 시 사용자 알림이 누락되어 있습니다.

PR 설명에서 "retry-limit user notification" UX 정책이 미결로 언급되어 있습니다. 현재는 로그만 남기고 재시도를 중단하는데, 사용자에게 연결 실패 상태를 알리는 이벤트 emit을 추가하는 것을 권장합니다.

이 기능을 추적할 새 이슈를 생성해 드릴까요?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt` around
lines 140 - 143, When reconnectAttempt >= MAX_RECONNECT_ATTEMPTS in SseManager
you currently only log and return; add a user-facing notification by emitting a
connection-failure event before returning. Update the block that contains
reconnectAttempt, MAX_RECONNECT_ATTEMPTS and Timber.tag(TAG).e to call the
manager's event emitter (or add a new
emitConnectionFailure/notifyConnectionFailed method on SseManager) and include
contextual details (reconnectAttempt and MAX_RECONNECT_ATTEMPTS) in the event
payload so listeners/UI can show a retry-limit notification to the user.
app/src/main/java/com/smashing/app/data/remote/datasource/impl/EventRemoteDataSourceImpl.kt (1)

63-72: onEvent에서 lock 없이 isCurrentSource를 호출하는 점이 다른 콜백과 불일치합니다.

onOpen, onFailure, onClosed는 모두 synchronized(eventSourceLock) 블록 내에서 isCurrentSource 체크를 수행하지만, onEvent는 락 없이 호출합니다. @Volatile 덕분에 가시성은 보장되지만, 체크와 emit 사이에 disconnect()가 호출되면 이미 해제된 연결의 이벤트가 emit될 수 있습니다.

실제 영향은 미미하나(이미 수신된 이벤트의 benign emit), 일관성을 위해 동기화를 추가하거나 의도적 설계라면 주석으로 명시하는 것을 권장합니다.

♻️ 일관성을 위한 선택적 수정
 override fun onEvent(
     eventSource: EventSource,
     id: String?,
     type: String?,
     data: String,
 ) {
-    if (!isCurrentSource(eventSource)) return
-    val eventName = type ?: return
-    _rawEvents.tryEmit(RawEventResponse(eventName = eventName, data = data))
+    synchronized(eventSourceLock) {
+        if (!isCurrentSource(eventSource)) return
+        val eventName = type ?: return
+        _rawEvents.tryEmit(RawEventResponse(eventName = eventName, data = data))
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/smashing/app/data/remote/datasource/impl/EventRemoteDataSourceImpl.kt`
around lines 63 - 72, onEvent currently calls isCurrentSource without holding
eventSourceLock, unlike onOpen/onFailure/onClosed; wrap the isCurrentSource
check and the subsequent _rawEvents.tryEmit(RawEventResponse(...)) inside a
synchronized(eventSourceLock) block to prevent emitting events for a source that
may be disconnected concurrently (or if you intend no lock, add a clear comment
explaining the race is benign). Update the onEvent implementation to use
synchronized(eventSourceLock) around the isCurrentSource check and emit,
referencing the onEvent method, isCurrentSource(), eventSourceLock,
_rawEvents.tryEmit(...), and disconnect() for context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/smashing/app/core/network/di/NetworkModule.kt`:
- Around line 140-145: AuthInterceptor currently uses addHeader(AUTHORIZATION,
...) which can create duplicate Authorization headers versus
TokenAuthenticator's header(AUTHORIZATION, ...); update AuthInterceptor (the
class/method that sets the AUTHORIZATION header) to use header(AUTHORIZATION,
...) instead of addHeader so the Authorization header is replaced rather than
appended, keeping behavior consistent with TokenAuthenticator and avoiding
duplicate auth headers.

In `@app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt`:
- Around line 64-80: In observeConnectionState(), change the Error branch to
inspect SseConnectionState.Error.statusCode: if the error is a 401, do not call
scheduleReconnectLocked; instead invoke the token-refresh flow via the existing
TokenAuthenticator or AuthManager (e.g., call TokenAuthenticator.refreshToken()
or AuthManager.handle401()) inside the same mutex.withLock scope (or ensure
mutual exclusion) and only schedule a reconnect if the refresh fails or for
non-401 errors; preserve the existing resetReconnectLocked behavior for
Connected states and keep scheduleReconnectLocked for Disconnected and non-401
Error cases.

---

Nitpick comments:
In `@app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt`:
- Around line 140-143: When reconnectAttempt >= MAX_RECONNECT_ATTEMPTS in
SseManager you currently only log and return; add a user-facing notification by
emitting a connection-failure event before returning. Update the block that
contains reconnectAttempt, MAX_RECONNECT_ATTEMPTS and Timber.tag(TAG).e to call
the manager's event emitter (or add a new
emitConnectionFailure/notifyConnectionFailed method on SseManager) and include
contextual details (reconnectAttempt and MAX_RECONNECT_ATTEMPTS) in the event
payload so listeners/UI can show a retry-limit notification to the user.

In
`@app/src/main/java/com/smashing/app/data/remote/datasource/impl/EventRemoteDataSourceImpl.kt`:
- Around line 63-72: onEvent currently calls isCurrentSource without holding
eventSourceLock, unlike onOpen/onFailure/onClosed; wrap the isCurrentSource
check and the subsequent _rawEvents.tryEmit(RawEventResponse(...)) inside a
synchronized(eventSourceLock) block to prevent emitting events for a source that
may be disconnected concurrently (or if you intend no lock, add a clear comment
explaining the race is benign). Update the onEvent implementation to use
synchronized(eventSourceLock) around the isCurrentSource check and emit,
referencing the onEvent method, isCurrentSource(), eventSourceLock,
_rawEvents.tryEmit(...), and disconnect() for context.

In
`@app/src/main/java/com/smashing/app/domain/usecase/auth/TokenReissueUseCase.kt`:
- Around line 33-45: The invoke() in TokenReissueUseCase currently uses
mutex.withLock but still allows queued callers to perform duplicate reissue
calls; fix this by introducing a shared in-flight marker (e.g., a nullable
Deferred<Result<String>> field like ongoingReissue) that you read/write under
the existing mutex: if ongoingReissue exists return/await it, otherwise create a
new async Deferred that executes
authRepository.postTokenReissue(PostTokenReissueRequest(refreshToken)) (and maps
to accessToken), store it in ongoingReissue, await its result for callers, and
clear ongoingReissue on completion/failure; ensure
tokenDataSource.getRefreshToken and error handling remain unchanged and access
to ongoingReissue is protected by the same mutex.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 250bfeb3-a412-4f71-8b92-3f8894ecdcd8

📥 Commits

Reviewing files that changed from the base of the PR and between 99426fc and 9e2be07.

📒 Files selected for processing (9)
  • app/src/main/java/com/smashing/app/core/network/TokenAuthenticator.kt
  • app/src/main/java/com/smashing/app/core/network/di/NetworkModule.kt
  • app/src/main/java/com/smashing/app/core/network/sse/SseConnectionState.kt
  • app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt
  • app/src/main/java/com/smashing/app/core/network/token/AuthManagerImpl.kt
  • app/src/main/java/com/smashing/app/data/remote/datasource/impl/EventRemoteDataSourceImpl.kt
  • app/src/main/java/com/smashing/app/data/repository/impl/EventRepositoryImpl.kt
  • app/src/main/java/com/smashing/app/domain/usecase/auth/TokenReissueUseCase.kt
  • app/src/main/java/com/smashing/app/presentation/main/MainActivity.kt
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/src/main/java/com/smashing/app/presentation/main/MainActivity.kt
  • app/src/main/java/com/smashing/app/core/network/TokenAuthenticator.kt
  • app/src/main/java/com/smashing/app/core/network/sse/SseConnectionState.kt
  • app/src/main/java/com/smashing/app/data/repository/impl/EventRepositoryImpl.kt

Comment thread app/src/main/java/com/smashing/app/core/network/di/NetworkModule.kt
Comment thread app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt (1)

67-73: ⚠️ Potential issue | 🟠 Major

401은 재연결이 아니라 재발급 경로로 보내야 합니다.

Line 72는 ErrorDisconnected를 같은 분기로 묶어서, 만료 토큰도 일반 transport 오류처럼 최대 30회 backoff retry 하게 만듭니다. PR 목표대로라면 statusCode == 401은 기존 토큰 재발급 경로로 분기하고, 그 실패 때만 재연결 또는 로그아웃을 결정해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt` around
lines 67 - 73, The current when(state) groups SseConnectionState.Error and
Disconnected together causing 401 errors to be treated as transport retries;
change the branch so SseConnectionState.Error checks the underlying HTTP/status
code (e.g., error.statusCode == 401) and route that case to the token
re-issuance flow (call the existing refresh token function—e.g.,
refreshAuthToken() or the token reissue handler); only if the token refresh
fails should you fall back to scheduleReconnectLocked() or trigger logout, and
for non-401 errors continue to call mutex.withLock { scheduleReconnectLocked() }
as before; keep resetReconnectLocked() for SseConnectionState.Connected.
🧹 Nitpick comments (1)
app/src/main/java/com/smashing/app/data/remote/datasource/impl/EventRemoteDataSourceImpl.kt (1)

63-72: onEvent()에서 동기화 누락 확인 필요

다른 콜백(onOpen, onFailure, onClosed)과 달리 onEvent()synchronized(eventSourceLock) 블록 없이 isCurrentSource()를 호출합니다. _rawEvents.tryEmit()이 스레드 세이프하므로 심각한 문제는 아니지만, 일관성을 위해 동기화 추가를 고려해 보세요.

♻️ 일관성을 위한 동기화 추가
 override fun onEvent(
     eventSource: EventSource,
     id: String?,
     type: String?,
     data: String,
 ) {
+    synchronized(eventSourceLock) {
         if (!isCurrentSource(eventSource)) return
         val eventName = type ?: return
         _rawEvents.tryEmit(RawEventResponse(eventName = eventName, data = data))
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/smashing/app/data/remote/datasource/impl/EventRemoteDataSourceImpl.kt`
around lines 63 - 72, onEvent currently calls isCurrentSource and
_rawEvents.tryEmit without the synchronized(eventSourceLock) used by
onOpen/onFailure/onClosed; wrap the body of onEvent in a
synchronized(eventSourceLock) block so the eventSource identity check
(isCurrentSource(eventSource)) and the emit
(_rawEvents.tryEmit(RawEventResponse(...))) execute under the same lock to
maintain consistency with other callbacks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/smashing/app/data/repository/impl/AuthRepositoryImpl.kt`:
- Around line 79-84: 현재 response.toTokenReissueModel()로 만든 tokenReissueModel을 검증
없이 tokenDataStore.setTokens(...)로 바로 저장하면 빈 문자열(또는 null) 응답 시 기존 유효 토큰이 덮어써져 인증
실패 루프가 발생할 수 있습니다; 따라서 tokenReissueModel.accessToken과
tokenReissueModel.refreshToken이 비어있지 않은지(또는 null이 아닌지) 검사한 뒤 유효한 값이 하나라도 있으면 해당
토큰만 업데이트하거나(예: access가 유효하면 access만, refresh가 유효하면 refresh만) 둘 다 비어있다면
setTokens를 호출하지 않고 오류를 기록하거나 예외를 던져 기존 토큰을 보존하도록 수정하세요; 관련 식별자:
response.toTokenReissueModel(), tokenReissueModel.accessToken,
tokenReissueModel.refreshToken, tokenDataStore.setTokens().

In
`@app/src/main/java/com/smashing/app/data/repository/impl/EventRepositoryImpl.kt`:
- Around line 49-53: The code silently drops unsupported event names and always
logs "event emitted" even if emission fails; update the handling around
SseEventType.fromEventName, parseEvent, and _events.tryEmit so that when
SseEventType.fromEventName(raw.eventName) returns null you log a clear warning
including the raw.eventName (and optionally raw.data) indicating
unknown/unsupported event, and after calling _events.tryEmit(event) check its
Boolean/Result return value and log success vs failure (including event details
and reason/context) instead of unconditionally logging "event emitted" via
Timber.tag(SSE_LOG_TAG).d; ensure parseEvent failures are also logged similarly
before returning.

In `@fastlane/notes/qa/debug.txt`:
- Around line 1-3: The release note is too generic and misleading (mentions "서버
명세 수정" while the PR mainly changes client SSE reconnection), so update the QA
release note (the "1차 스프린트 QA 용 배포 26.04.03" entry) to list concrete changes and
tester checks: add bullets for SSE exponential backoff behavior (1s–120s, up to
30 retries), the 401 token refresh flow trigger and expected outcome, and SSE
connection management across app foreground/background and login state, and
remove or correct the "서버 명세 수정" line to reflect client-side SSE reconnection
improvements.

---

Duplicate comments:
In `@app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt`:
- Around line 67-73: The current when(state) groups SseConnectionState.Error and
Disconnected together causing 401 errors to be treated as transport retries;
change the branch so SseConnectionState.Error checks the underlying HTTP/status
code (e.g., error.statusCode == 401) and route that case to the token
re-issuance flow (call the existing refresh token function—e.g.,
refreshAuthToken() or the token reissue handler); only if the token refresh
fails should you fall back to scheduleReconnectLocked() or trigger logout, and
for non-401 errors continue to call mutex.withLock { scheduleReconnectLocked() }
as before; keep resetReconnectLocked() for SseConnectionState.Connected.

---

Nitpick comments:
In
`@app/src/main/java/com/smashing/app/data/remote/datasource/impl/EventRemoteDataSourceImpl.kt`:
- Around line 63-72: onEvent currently calls isCurrentSource and
_rawEvents.tryEmit without the synchronized(eventSourceLock) used by
onOpen/onFailure/onClosed; wrap the body of onEvent in a
synchronized(eventSourceLock) block so the eventSource identity check
(isCurrentSource(eventSource)) and the emit
(_rawEvents.tryEmit(RawEventResponse(...))) execute under the same lock to
maintain consistency with other callbacks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9db226d8-37e9-4c14-8e1f-1e90e27d8482

📥 Commits

Reviewing files that changed from the base of the PR and between eb3cfbd and 60ec660.

📒 Files selected for processing (19)
  • app/src/main/java/com/smashing/app/core/network/TokenAuthenticator.kt
  • app/src/main/java/com/smashing/app/core/network/di/NetworkModule.kt
  • app/src/main/java/com/smashing/app/core/network/sse/SseConnectionState.kt
  • app/src/main/java/com/smashing/app/core/network/sse/SseManager.kt
  • app/src/main/java/com/smashing/app/core/network/token/AuthManager.kt
  • app/src/main/java/com/smashing/app/core/network/token/AuthManagerImpl.kt
  • app/src/main/java/com/smashing/app/data/remote/datasource/api/EventRemoteDatasource.kt
  • app/src/main/java/com/smashing/app/data/remote/datasource/impl/EventRemoteDataSourceImpl.kt
  • app/src/main/java/com/smashing/app/data/repository/api/EventRepository.kt
  • app/src/main/java/com/smashing/app/data/repository/impl/AuthRepositoryImpl.kt
  • app/src/main/java/com/smashing/app/data/repository/impl/EventRepositoryImpl.kt
  • app/src/main/java/com/smashing/app/domain/usecase/auth/TokenReissueUseCase.kt
  • app/src/main/java/com/smashing/app/presentation/login/LoginViewModel.kt
  • app/src/main/java/com/smashing/app/presentation/main/MainActivity.kt
  • app/src/main/java/com/smashing/app/presentation/mypage/MyPageViewModel.kt
  • app/src/main/java/com/smashing/app/presentation/signup/SignUpViewModel.kt
  • app/src/main/java/com/smashing/app/presentation/splash/SplashViewModel.kt
  • app/src/main/java/com/smashing/app/presentation/withdraw/WithdrawViewModel.kt
  • fastlane/notes/qa/debug.txt

Comment on lines +79 to +84
val tokenReissueModel = response.toTokenReissueModel()

response.toTokenReissueModel()
tokenDataStore.setTokens(
accessToken = tokenReissueModel.accessToken,
refreshToken = tokenReissueModel.refreshToken,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

재발급 토큰 공백값 저장을 막아주세요.

Line 81-Line 84에서 토큰 유효성 검증 없이 저장하면, 비정상 응답(빈 토큰) 시 기존 유효 토큰을 덮어써 인증 실패 루프가 발생할 수 있습니다.

🔧 제안 수정안
             val tokenReissueModel = response.toTokenReissueModel()
+            require(tokenReissueModel.accessToken.isNotBlank() && tokenReissueModel.refreshToken.isNotBlank()) {
+                "Reissued token is blank"
+            }
 
             tokenDataStore.setTokens(
                 accessToken = tokenReissueModel.accessToken,
                 refreshToken = tokenReissueModel.refreshToken,
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/smashing/app/data/repository/impl/AuthRepositoryImpl.kt`
around lines 79 - 84, 현재 response.toTokenReissueModel()로 만든 tokenReissueModel을
검증 없이 tokenDataStore.setTokens(...)로 바로 저장하면 빈 문자열(또는 null) 응답 시 기존 유효 토큰이 덮어써져
인증 실패 루프가 발생할 수 있습니다; 따라서 tokenReissueModel.accessToken과
tokenReissueModel.refreshToken이 비어있지 않은지(또는 null이 아닌지) 검사한 뒤 유효한 값이 하나라도 있으면 해당
토큰만 업데이트하거나(예: access가 유효하면 access만, refresh가 유효하면 refresh만) 둘 다 비어있다면
setTokens를 호출하지 않고 오류를 기록하거나 예외를 던져 기존 토큰을 보존하도록 수정하세요; 관련 식별자:
response.toTokenReissueModel(), tokenReissueModel.accessToken,
tokenReissueModel.refreshToken, tokenDataStore.setTokens().

Comment thread fastlane/notes/qa/debug.txt

@oilbeaneda oilbeaneda left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

와우띵가링가링 .. 어려운 sse 보고 더 공부해보겠슨니다 수고많으셨습니다!

@ShinHyeongcheol ShinHyeongcheol left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

호오 한동안 틈틈히 봤는데, 흠 잡을 곳을 찾을 수가 없다!!
코드 리뷰 보다 synchronized(stateLock)를 처음 알게 되어서 관련해서 궁금한 점 남겨두었습니다!!

* 로그아웃 전환(forceLogoutEvent emit)은 최초 1회만 허용한다.
* 사용자가 다시 로그인하면 false로 리셋한다.
*/
private val stateLock = Any()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

p3: 요거 synchronized(stateLock)을 사용한 부분 처음 알게 된 내용이라 인상적이었어요!!
궁금한 점은 stateLock을 사용할 때, Any()를 사용해주는 이유는 무엇일까요?? 다른 값으로 초기화 해주면 안될까요?
이전 커밋을 보면 AtomicBoolean을 사용해서 관리하는 것으로 보았고, 두 방식의 차이점은 하나의 값을 다룰지와 여러 값을 관리할지 일 것 같은데 AtomicBoolean을 여러개 사용하는 방식과 비교했을 때 현 방법의 장점은 무엇이라고 생각하나요??

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Any()를 사용한 이유는 this나 다른 공유 객체를 lock으로 사용할 경우, 예상치 못한 곳에서 동일한 lock을 잡아 문제가 생길 수 있기 때문이에요. Effective Kotlin에서도 Any를 사용하는 예제가 있어서 이를 사용했고 private으로 숨긴 전용 lock 객체 생성했다에 초점을 맞추면 좋을거 같아요.

AtomicBoolean은 말씀하신 것처럼 단일 값의 원자적 변경에는 매우 적합해요.
다만 관리해야 할 상태가 2~3개 이상이고 이를 일관되게 변경해야 하는 경우에는 atomic 변수 여러 개만으로는 한 값만 먼저 바뀐 중간 상태가 외부에 노출될 수 있어요.

반면 synchronized를 사용하면 관련 상태 변경을 하나의 트랜잭션처럼 묶어 처리할 수 있어서, 순서와 일관성을 보장하기 쉬워요.

정리하면 장점은 다음과 같아요.

  1. 여러 상태의 불변식 유지가 쉬워요.
  2. 읽기/쓰기 경계가 명확해 코드 의도가 잘 드러나요.
  3. 이후 상태가 늘어나도 동기화 전략을 크게 바꾸지 않아도 돼요.

@vahkjsdf vahkjsdf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

우와 수정하시느라 수고 많으셨어요! 이것저것 공부할게 생겼네용 열심히 따라가보겠습니다!!

@seungjunGong
seungjunGong merged commit 133d3f2 into develop Apr 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] SSE BackOff 전략 적용

4 participants