Skip to content

[Refactor/#194] UserProfile 데이터 관리 최적화#195

Open
wjdrjs00 wants to merge 6 commits intodevelopfrom
refactor/#194-userprofile-flow
Open

[Refactor/#194] UserProfile 데이터 관리 최적화#195
wjdrjs00 wants to merge 6 commits intodevelopfrom
refactor/#194-userprofile-flow

Conversation

@wjdrjs00
Copy link
Member

@wjdrjs00 wjdrjs00 commented Mar 10, 2026

[ PR Content ]

UserProfile 정보를 관리하는 방식을 기존의 일회성 Fetch 방식에서 SSOT 기반의 실시간 구독 방식으로 개선했습니다.

Related issue

Screenshot 📸

  • N/A

주요 변경 사항 및 근거

  1. 데이터 소스 분리 (Remote & Local)

    • 변경점: 기존 UserDataSourceUserRemoteDataSourceUserLocalDataSource로 분리.
    • 이유: 관심사 분리(SoC) 원칙에 따라 네트워크 통신과 데이터 저장을 명확히 구분합니다. UserLocalDataSourceMutableStateFlow를 통해 앱 생명주기 동안 유지되는 In-memory 캐시 역할을 수행합니다.
  2. 반응형 데이터 흐름 (Observe-to-Fetch)

    • 변경점: fetchUserProfile()(명령형) 방식을 observeUserProfile() (선언형/반응형) 방식으로 전환.
    • 근거: Cold Flow의 지연 실행(Lazy Execution) 특성을 활용했습니다. 사용자가 데이터를 구독(collect)하는 시점에 데이터가 없으면 자동으로 네트워크 요청을 트리거하고, 데이터가 있다면 즉시 캐시를 반환합니다. 이를 통해 각 화면(ViewModel)에서 명시적으로 Fetch 로직을 작성할 필요가 없어졌습니다.
  3. 레이스 컨디션 방지 (Mutex & Double-Check Locking)

    • 변경점: UserRepositoryImplMutex를 도입하여 중복 네트워크 요청을 차단.
    • 근거: 여러 화면에서 동시에 프로필을 구독할 경우 발생할 수 있는 중복 API 호출(Double Fetching)을 방지합니다. 락(Lock)을 획득한 후 다시 한번 캐시를 확인하는 패턴을 통해 자원 낭비를 최소화하고 스레드 안전성을 확보했습니다.
  4. UseCase 오케스트레이션 (Logout/Withdrawal)

    • 변경점: 로그아웃 및 회원탈퇴 UseCase에서 UserRepository.clearCache()를 함께 호출.
    • 근거: 인증 정보가 파기될 때 유저 데이터(프로필)도 함께 초기화되어야 한다는 데이터 정합성을 보장하기 위함입니다. 각 모듈의 독립성은 유지하면서 UseCase 레이어에서 비즈니스 로직을 통합 관리합니다.

Work Description

  • Architecture: RemoteDataSourceLocalDataSource를 분리하여 데이터 흐름의 명확성을 확보했습니다.
  • Reactive Flow: ObserveUserProfileUseCase를 구독하는 것만으로 데이터 로드와 갱신이 자동으로 이루어집니다.
  • Concurrency Control: Mutex를 활용한 Double-check locking 패턴을 적용하여 Race Condition을 해결했습니다.
  • Lifecycle Management: MutableStateFlow를 통해 앱 생명주기 동안 유효한 메모리 캐시를 유지하며, 로그아웃 시 안전하게 초기화됩니다.
  • Testing: UserRepositoryImplTest를 추가하여 다중 코루틴 환경에서의 중복 요청 방지 및 캐시 정합성을 검증했습니다.

To Reviewers 📢

  • 최근 flow기반의 반응형 방식이 관심이 생겨 알아보던 중 repository cache 구현이 용이하다고 도입해봤습니다! (개인적인 의견..)
  • 빛나길의 경우 현재 사용자 프로필의 변경포인트(ex. 프로필 수정)가 없습니다. 따라서 앱 사용중 최초 1회 이후 refetch 필요가 없다고 판단하여 위 방식으로 리펙토링 해봤습니다! (자세한 설명은 '주요 변경 사항 및 근거'를 확인해주세요!)
  • 궁금점이나 개선포인트가 있다면 리뷰 부탁드립니다~!

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 사용자 프로필을 반응형 스트림으로 제공하여 실시간 업데이트 감지 기능 추가
    • 사용자 프로필 데이터 로컬 캐싱으로 성능 개선
    • 로그아웃 및 탈퇴 시 캐시 자동 초기화 기능 추가
  • 테스트

    • 캐싱 로직 및 동시성 처리에 대한 포괄적인 테스트 추가

@wjdrjs00 wjdrjs00 requested a review from l5x5l March 10, 2026 18:12
@wjdrjs00 wjdrjs00 self-assigned this Mar 10, 2026
@wjdrjs00 wjdrjs00 added 🔨 Refactor 기존 기능 개선 🧤 대현 labels Mar 10, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

Walkthrough

UserProfile 데이터 관리를 로컬 캐시 기반 반응형 패턴으로 리팩토링했습니다. 단일 데이터소스를 로컬/원격으로 분리하고, 동기 API를 Flow 기반의 관찰 가능한 구조로 변환하며, 중복 요청 방지를 위한 Mutex 로직을 추가했습니다.

Changes

Cohort / File(s) Summary
DI 모듈 및 데이터소스 인터페이스
app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt, data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt, data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserRemoteDataSource.kt
단일 UserDataSource 바인딩을 UserLocalDataSource와 UserRemoteDataSource 두 개의 분리된 바인딩으로 교체. 로컬 캐시 인터페이스 추가.
데이터소스 구현체
data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserLocalDataSourceImpl.kt, data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserRemoteDataSourceImpl.kt
UserLocalDataSourceImpl 신규 생성 (StateFlow 기반 인메모리 캐시), UserDataSourceImpl을 UserRemoteDataSourceImpl로 이름 변경.
저장소 구현 및 테스트
data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt, data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt
동기 fetchUserProfile을 observeUserProfile Flow로 변환. Mutex 기반 중복 요청 방지 로직 추가. clearCache 메서드 추가. 캐싱, 동시성, 초기화 시나리오를 검증하는 126줄의 포괄적 단위 테스트 추가.
빌드 설정
data/build.gradle.kts
테스트 의존성 추가: androidx.junit, kotlin.coroutines.test.
도메인 저장소 인터페이스
domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt
fetchUserProfile() 제거, observeUserProfile(): Flow<Result> 추가, clearCache() 메서드 추가.
도메인 유스케이스
domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/ObserveUserProfileUseCase.kt
FetchUserProfileUseCase를 ObserveUserProfileUseCase로 이름 변경. 동기 반환을 Flow 기반 반응형 API로 변환.
인증 유스케이스
domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt, domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt
UserRepository 의존성 추가. 인증 성공 후 clearCache() 호출하는 부작용 추가.
프레젠테이션 뷰모델
presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt, presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt, presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt
FetchUserProfileUseCase를 ObserveUserProfileUseCase로 교체. 일회성 페치에서 repeatOnSubscription을 사용한 반응형 관찰로 전환.

Sequence Diagram(s)

sequenceDiagram
    participant VM as ViewModel
    participant UC as ObserveUserProfileUseCase
    participant Repo as UserRepository
    participant Local as LocalDataSource
    participant Remote as RemoteDataSource

    VM->>UC: observeUserProfile()
    UC->>Repo: observeUserProfile()
    
    Repo->>Repo: Acquire Mutex Lock
    Repo->>Local: Check userProfile
    
    alt Cache Empty (First Time)
        Repo->>Remote: fetchUserProfile()
        Remote-->>Repo: Result<UserProfileResponse>
        Repo->>Local: saveUserProfile()
        Local-->>Repo: ✓
    else Cache Exists
        Repo->>Local: Get cached userProfile
    end
    
    Repo->>Repo: Release Mutex Lock
    Repo->>Local: Observe userProfile StateFlow
    Local-->>Repo: emit UserProfile
    Repo-->>UC: emit Result<UserProfile>
    UC-->>VM: emit Result<UserProfile>
    VM->>VM: Update UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 로컬 캐시 춤을 추며, Flow는 흘러흘러
중복 요청은 안녕, Mutex가 지켜주네요
한 번의 API, 여럿이 함께 누려요
반응형 스트림, 신나는 리팩토링! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 제목 '[Refactor/#194] UserProfile 데이터 관리 최적화'는 PR의 주요 변경 사항인 사용자 프로필 데이터 관리 방식의 최적화를 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 템플릿의 주요 섹션(Related issue, Work Description, To Reviewers)을 포함하고 있으며, 변경 사항과 근거가 상세히 기술되어 있습니다.
Linked Issues check ✅ Passed PR의 모든 주요 변경 사항이 #194 이슈의 작업 목록과 일치합니다: UserLocalDataSource 구현, UserRemoteDataSource 분리, Flow 기반 observeUserProfile, Mutex를 통한 중복 요청 방지, 캐시 초기화 로직, 단위 테스트 작성 완료.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 #194 이슈의 범위 내에 있습니다. 테스트 의존성 추가, 데이터 소스 분리, Flow 기반 리포지토리 구현, UseCase 전환, ViewModel 업데이트는 모두 SSOT 기반의 반응형 패턴 도입이라는 목표에 부합합니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#194-userprofile-flow

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

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: 5

🧹 Nitpick comments (1)
data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt (1)

7-9: 인메모리 캐시 setter라면 suspend는 과한 계약입니다.

구현을 확인한 결과, saveUserProfile()은 단순히 _userProfile.update { userProfile }만 수행하고 있습니다. 실제 suspension point가 없으므로 suspend 키워드는 불필요하며, 일반 함수로 변경하는 것이 API 설계상 더 적절합니다. 현재 시그니처는 호출자를 불필요하게 코루틴 컨텍스트에 묶고 있습니다.

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

In
`@data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt`
around lines 7 - 9, The interface exposes saveUserProfile as suspend despite the
implementation only doing an in-memory update (_userProfile.update { userProfile
}) with no suspension points; change the signature of saveUserProfile from
suspend fun saveUserProfile(userProfile: UserProfile) to a regular fun
saveUserProfile(userProfile: UserProfile) in UserLocalDataSource (and update any
implementing class methods accordingly) and update any callers to remove
unnecessary coroutine context so the API no longer forces callers into suspend
contexts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt`:
- Around line 29-31: The fetchUserProfile flow can write stale data after
clearCache(); add a generation token to UserRepositoryImpl (e.g., a private
AtomicInteger or volatile Int) that you increment in clearCache(), then capture
the current generation before calling userRemoteDataSource.fetchUserProfile()
and, inside the onSuccess handler (the lambda that calls
userLocalDataSource.saveUserProfile(response.toDomain())), compare the captured
generation with the repository's current generation and only call
saveUserProfile if they match; update references to fetchUserProfile and
clearCache accordingly so any in-flight results are ignored when the generation
advanced.
- Around line 25-37: The current double-check in observeUserProfile
(userLocalDataSource.userProfile + fetchMutex) only prevents duplicate remote
calls on success; if the first fetch fails the cache stays null and each waiting
subscriber re-enters the mutex and triggers another fetch. Fix by introducing a
shared in-flight holder (e.g., a CompletableDeferred or Deferred<Result<...>>
named inFlightFetch) that you set before calling
userRemoteDataSource.fetchUserProfile(), have all subscribers await that single
deferred, and clear it after completion; on success call
userLocalDataSource.saveUserProfile(response.toDomain()) and emit the result,
and on failure emit the failure to all awaiters as well. Update
observeUserProfile to check/await inFlightFetch when cache is null and add a
concurrency test covering failed-first-request to ensure only one remote call is
made and the failure is shared.

In
`@domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt`:
- Around line 7-8: The current contract doesn't allow propagating cache
invalidation to existing collectors; change observeUserProfile() and the
implementation to model "no profile" (either make observeUserProfile():
Flow<Result<UserProfile?>> or introduce a sealed state like UserProfileState to
represent Unauthenticated/Empty), then update UserRepositoryImpl (remove
filterNotNull() on the local cache flow) so clearCache() actively emits the
empty/unauthenticated state into the shared flow/stream; ensure clearCache()
triggers emission of that empty state so live collectors receive the
invalidation.

In
`@presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt`:
- Around line 251-260: The subscription in HomeViewModel uses
repeatOnSubscription + observeUserProfileUseCase() while incrementing
loadingCount once then decrementing on every emission, which can drive
loadingCount negative; change the logic so the initial increment is matched by a
single decrement (e.g., only decrement once after the first emission) or
introduce a dedicated subscription loading flag (e.g., userProfileLoading)
instead of reusing loadingCount; update the reduce calls inside the
observeUserProfileUseCase().collect block (and the initial repeatOnSubscription
increment) to use the chosen approach so each subscription start has a balanced
decrement or uses an isolated boolean for this subscription.

In
`@presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt`:
- Around line 24-27: In MyPageViewModel's observeUserProfileUseCase collector
the code sets profileUrl to the literal string "profileUrl"; change reduce {
state.copy(name = it.nickname, profileUrl = "profileUrl") } to assign the real
value from the DTO/Domain object (e.g., it.profileUrl or it.profileImageUrl) or,
if that field can be null/empty, preserve the existing value by using
state.profileUrl when the incoming value is absent; update the mapping in the
observeUserProfileUseCase collection block and ensure reduce/state.copy uses the
actual property name instead of the hardcoded string.

---

Nitpick comments:
In
`@data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt`:
- Around line 7-9: The interface exposes saveUserProfile as suspend despite the
implementation only doing an in-memory update (_userProfile.update { userProfile
}) with no suspension points; change the signature of saveUserProfile from
suspend fun saveUserProfile(userProfile: UserProfile) to a regular fun
saveUserProfile(userProfile: UserProfile) in UserLocalDataSource (and update any
implementing class methods accordingly) and update any callers to remove
unnecessary coroutine context so the API no longer forces callers into suspend
contexts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 07cbba97-2081-4aeb-a50e-2a9871479390

📥 Commits

Reviewing files that changed from the base of the PR and between 55c8f13 and e5c743e.

📒 Files selected for processing (15)
  • app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt
  • data/build.gradle.kts
  • data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt
  • data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserRemoteDataSource.kt
  • data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserLocalDataSourceImpl.kt
  • data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserRemoteDataSourceImpl.kt
  • data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt
  • data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/ObserveUserProfileUseCase.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt

Comment on lines +25 to +37
override fun observeUserProfile(): Flow<Result<UserProfile>> = flow {
if (userLocalDataSource.userProfile.value == null) {
fetchMutex.withLock {
if (userLocalDataSource.userProfile.value == null) {
userRemoteDataSource.fetchUserProfile()
.onSuccess { response ->
userLocalDataSource.saveUserProfile(response.toDomain())
}
.onFailure {
emit(Result.failure(it))
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

실패한 첫 요청 뒤에 대기 중 구독자들이 순차 재호출합니다.

Line 26-28의 double-check는 성공 케이스에서만 중복 억제가 됩니다. 첫 remote fetch가 실패하면 캐시는 계속 null이라, mutex를 기다리던 각 구독자가 락을 순서대로 잡으면서 Line 29를 다시 호출합니다. 장애 상황에서 구독 수만큼 backend 호출이 터질 수 있으니, in-flight 결과를 성공/실패 모두 공유하도록 바꿔야 합니다. 이 경로를 고정하는 실패-path 동시성 테스트도 같이 있으면 좋겠습니다.

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

In
`@data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt`
around lines 25 - 37, The current double-check in observeUserProfile
(userLocalDataSource.userProfile + fetchMutex) only prevents duplicate remote
calls on success; if the first fetch fails the cache stays null and each waiting
subscriber re-enters the mutex and triggers another fetch. Fix by introducing a
shared in-flight holder (e.g., a CompletableDeferred or Deferred<Result<...>>
named inFlightFetch) that you set before calling
userRemoteDataSource.fetchUserProfile(), have all subscribers await that single
deferred, and clear it after completion; on success call
userLocalDataSource.saveUserProfile(response.toDomain()) and emit the result,
and on failure emit the failure to all awaiters as well. Update
observeUserProfile to check/await inFlightFetch when cache is null and add a
concurrency test covering failed-first-request to ensure only one remote call is
made and the failure is shared.

Comment on lines +29 to +31
userRemoteDataSource.fetchUserProfile()
.onSuccess { response ->
userLocalDataSource.saveUserProfile(response.toDomain())
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

clearCache()가 진행 중인 fetch 결과를 무효화하지 못합니다.

Line 29-31의 요청이 이미 시작된 상태에서 Line 47-48이 실행돼도, 나중에 fetch가 성공하면 이전 계정의 프로필을 다시 캐시에 써버립니다. 그러면 로그아웃/회원탈퇴 직후에도 stale 데이터가 남고, 다음 구독자가 이전 사용자 프로필을 읽는 교차 계정 노출까지 생길 수 있습니다. clearCache() 시점마다 세대(version/token)를 올리고 저장 직전에 같은 세대인지 확인하거나, in-flight fetch를 취소할 수 있어야 합니다.

Also applies to: 47-48

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

In
`@data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt`
around lines 29 - 31, The fetchUserProfile flow can write stale data after
clearCache(); add a generation token to UserRepositoryImpl (e.g., a private
AtomicInteger or volatile Int) that you increment in clearCache(), then capture
the current generation before calling userRemoteDataSource.fetchUserProfile()
and, inside the onSuccess handler (the lambda that calls
userLocalDataSource.saveUserProfile(response.toDomain())), compare the captured
generation with the repository's current generation and only call
saveUserProfile if they match; update references to fetchUserProfile and
clearCache accordingly so any in-flight results are ignored when the generation
advanced.

Comment on lines +7 to +8
fun observeUserProfile(): Flow<Result<UserProfile>>
fun clearCache()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

현재 계약으로는 캐시 무효화가 기존 구독자에게 전파되지 않습니다.

clearCache()를 추가했지만 observeUserProfile(): Flow<Result<UserProfile>>는 "프로필 없음" 상태를 표현할 수 없습니다. 제공된 data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt:18-37 스니펫도 로컬 캐시를 filterNotNull() 한 뒤에만 내보내고 있어서, clearCache() 후 살아 있는 collector는 아무 이벤트도 받지 못하고 이전 프로필을 계속 들고 있을 수 있습니다. 이 계약은 UserProfile? 또는 별도 sealed state처럼 비인증/초기화 상태까지 모델링하도록 바꾸는 편이 안전합니다.

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

In
`@domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt`
around lines 7 - 8, The current contract doesn't allow propagating cache
invalidation to existing collectors; change observeUserProfile() and the
implementation to model "no profile" (either make observeUserProfile():
Flow<Result<UserProfile?>> or introduce a sealed state like UserProfileState to
represent Unauthenticated/Empty), then update UserRepositoryImpl (remove
filterNotNull() on the local cache flow) so clearCache() actively emits the
empty/unauthenticated state into the shared flow/stream; ensure clearCache()
triggers emission of that empty state so live collectors receive the
invalidation.

Comment on lines +251 to +260
repeatOnSubscription {
reduce { state.copy(loadingCount = state.loadingCount + 1) }
observeUserProfileUseCase().collect { result ->
result.fold(
onSuccess = {
reduce { state.copy(userNickname = it.nickname, loadingCount = state.loadingCount - 1) }
},
onFailure = {
Log.e("HomeViewModel", "유저 정보 가져오기 실패: ${it.message}")
reduce { state.copy(loadingCount = state.loadingCount - 1) }
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

장기 구독에 loadingCount 증감 로직을 그대로 붙이면 상태가 깨집니다.

Line 252에서는 1회만 증가시키는데, Line 256/260에서는 구독 중 들어오는 모든 emission마다 감소합니다. 이 Flow는 SSOT 구독이라 캐시 재방출이나 후속 업데이트가 오면 loadingCount가 음수가 되고, 첫 emission 전에 취소되면 증가만 남습니다. 이후 다른 로딩이 시작돼도 스피너가 안 뜰 수 있으니, 초기 1회만 감소시키거나 이 구독용 로딩 상태를 별도로 분리하는 편이 안전합니다.

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

In
`@presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt`
around lines 251 - 260, The subscription in HomeViewModel uses
repeatOnSubscription + observeUserProfileUseCase() while incrementing
loadingCount once then decrementing on every emission, which can drive
loadingCount negative; change the logic so the initial increment is matched by a
single decrement (e.g., only decrement once after the first emission) or
introduce a dedicated subscription loading flag (e.g., userProfileLoading)
instead of reusing loadingCount; update the reduce calls inside the
observeUserProfileUseCase().collect block (and the initial repeatOnSubscription
increment) to use the chosen approach so each subscription start has a balanced
decrement or uses an isolated boolean for this subscription.

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

Labels

🔨 Refactor 기존 기능 개선 🧤 대현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] UserProfile 데이터 관리 최적화 (SSOT 및 반응형 패턴 도입)

1 participant