Skip to content

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

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

[Refactor/#194] UserProfile 데이터 관리 최적화#195
wjdrjs00 wants to merge 7 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

  • 새로운 기능

    • 사용자 프로필을 Flow 기반의 반응형 스트림으로 제공해 실시간 업데이트 감지 지원
    • 로컬/원격 분리 데이터소스 도입 및 로컬 캐싱으로 성능 향상
    • 프로필 조회용 동시성 보호 및 캐시 무효화(clearCache) API 추가
  • 행동 변경

    • 로그아웃·탈퇴 시 캐시 자동 초기화 동작 추가
  • 테스트

    • 캐싱 및 동시성 시나리오에 대한 포괄적 단위 테스트 추가
  • 설정

    • 테스트용 의존성 추가 (테스트 환경 강화)

@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 관리를 로컬 인메모리 캐시와 원격 데이터소스로 분리하고, 동기 호출을 Flow 기반 관찰형 API로 전환했습니다. Repository에 Mutex 기반 중복 요청 방지와 캐시 갱신/무효화 로직이 추가되었고, 관련 유닛테스트와 DI 바인딩이 갱신되었습니다.

Changes

Cohort / File(s) Summary
DI 모듈
app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt
단일 UserDataSource 바인딩을 UserLocalDataSourceUserRemoteDataSource로 분리한 바인딩 변경.
데이터소스 인터페이스
data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt, data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserRemoteDataSource.kt
로컬 인터페이스 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
인메모리 StateFlow 기반 UserLocalDataSourceImpl 신규 추가, 기존 구현체 이름을 UserRemoteDataSourceImpl로 변경(인터페이스 구현 변경).
저장소(Repository)
data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt
동기 fetchUserProfileobserveUserProfile(): Flow<Result<UserProfile>>로 전환. getUserProfile(), clearCache() 추가, Mutex로 중복 원격 호출 방지 및 로컬 캐시 갱신 로직 도입. 클래스에 @Singleton 추가.
저장소 테스트
data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt
로컬/원격 페이크 데이터소스를 사용한 Flow 관찰, 캐시 동작, 동시성(중복 요청 방지) 및 clearCache 시나리오를 검증하는 단위테스트 추가.
도메인 인터페이스 및 유스케이스
domain/src/main/java/.../user/repository/UserRepository.kt, domain/src/main/java/.../user/usecase/*.kt
Repository API에서 fetchUserProfile() 제거, observeUserProfile(): Flow<Result<UserProfile>>, getUserProfile(), clearCache() 추가. UseCase들 이름/반환형(Flow) 및 호출부를 이에 맞게 변경 (ObserveUserProfileUseCase, GetUserProfileUseCase).
인증 유스케이스
domain/src/main/java/.../auth/usecase/LogoutUseCase.kt, domain/src/main/java/.../auth/usecase/WithdrawalUseCase.kt
로그아웃/회원탈퇴 성공 시 userRepository.clearCache()를 호출하도록 UserRepository 의존성 추가.
프레젠테이션 변경
presentation/src/main/java/com/threegap/bitnagil/presentation/screen/.../*.kt
화면 ViewModel들이 일회성 페치에서 ObserveUserProfileUseCase의 Flow를 repeatOnSubscription으로 구독하도록 변경(생명주기 구독 패턴 적용).
빌드 설정
data/build.gradle.kts
테스트용 의존성 추가: androidx.junit, kotlinx-coroutines-test.

Sequence Diagram(s)

sequenceDiagram
    participant VM as ViewModel
    participant UC as ObserveUserProfileUseCase
    participant Repo as UserRepository
    participant Local as UserLocalDataSource
    participant Remote as UserRemoteDataSource

    VM->>UC: observeUserProfile()
    UC->>Repo: observeUserProfile()
    Repo->>Repo: acquire Mutex
    Repo->>Local: check userProfile StateFlow
    alt cache empty
        Repo->>Remote: fetchUserProfile()
        Remote-->>Repo: Result<UserProfileResponse>
        Repo->>Local: saveUserProfile(domainModel)
    else cache present
        Repo->>Local: read cached UserProfile
    end
    Repo->>Repo: release Mutex
    Local-->>Repo: emit userProfile StateFlow
    Repo-->>UC: emit Result<UserProfile>
    UC-->>VM: emit Result<UserProfile>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 로컬에 품은 프로필 빛나네
한 번의 호출, 모두가 함께 보네
Mutex가 지키고 Flow가 흘러와
캐시를 비우면 새바람 불어오네 🎋

🚥 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 PR 제목은 주요 변경사항인 UserProfile 데이터 관리 최적화를 명확하게 반영하고 있으며, 실제 코드 변경과도 일치합니다.
Description check ✅ Passed PR 설명은 Related issue, Work Description, To Reviewers 등 필요한 섹션을 모두 포함하고 있으며, 변경 사항에 대한 상세한 근거와 설명을 제공합니다.
Linked Issues check ✅ Passed PR의 모든 코드 변경사항이 #194 이슈의 요구사항을 충족합니다: 로컬/원격 데이터소스 분리, Flow 기반 반응형 구조, Mutex를 통한 동시성 제어, 캐시 초기화 로직, 단위 테스트 작성이 모두 구현되었습니다.
Out of Scope Changes check ✅ Passed PR의 모든 변경사항이 #194 이슈의 범위 내에 있습니다. 데이터 소스 분리, Repository 개편, UseCase 오케스트레이션, ViewModel 업데이트, 테스트 작성 모두 UserProfile 데이터 관리 최적화 목표에 직접 연관되어 있습니다.

✏️ 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

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

Copy link
Contributor

Choose a reason for hiding this comment

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

[1] OnBoarindgViewModel에서는 flow를 수집하는 방식보다는 기존 일회성으로 정보를 조회하여 사용하기에, 기존 방식처럼 일회성으로 UserProfile을 조회하는 함수도 1개 필요할 것 같습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

요 부분은 first()를 통해 수집 후 cancel 하면 큰 문제는 없겠다 판단했었는데, 역할을 분리해서 각각 별도로 구성해보겠습니다~

Copy link
Member Author

Choose a reason for hiding this comment

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

반영 완료 -> fae4788

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.

♻️ Duplicate comments (4)
data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt (3)

52-55: ⚠️ Potential issue | 🟠 Major

clearCache() 호출 후에도 진행 중인 fetch가 stale 데이터를 저장할 수 있습니다.

clearCache()가 호출되어도 이미 시작된 fetch 요청은 완료 후 이전 계정의 프로필을 캐시에 다시 저장합니다. 로그아웃/회원탈퇴 직후 교차 계정 데이터 노출 위험이 있습니다.

generation token(버전 카운터)을 도입하여 fetch 시작 시점의 generation과 저장 시점의 generation이 일치하는 경우에만 saveUserProfile()을 호출하도록 개선이 필요합니다.

,

🤖 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 52 - 55, Introduce a generation/version counter in
UserRepositoryImpl (e.g., an AtomicInteger field) and increment it inside
clearCache(); when calling userRemoteDataSource.fetchUserProfile() capture the
current generation into a local variable, and in the onSuccess lambda compare
that captured generation to the current counter—only call
userLocalDataSource.saveUserProfile(response.toDomain()) if they match; this
prevents in-flight fetches started before clearCache() from writing stale
profiles. Ensure the counter is thread-safe (AtomicInteger) so increments and
comparisons are safe across async callbacks.

31-35: ⚠️ Potential issue | 🟠 Major

clearCache() 후 기존 collector가 무효화 이벤트를 수신하지 못합니다.

Line 33의 filterNotNull()로 인해 캐시가 null로 초기화되어도 기존 collector는 아무 이벤트도 받지 못하고 이전 프로필을 계속 유지합니다.

Flow<Result<UserProfile?>> 또는 sealed state 패턴으로 "프로필 없음" 상태를 명시적으로 모델링하면 캐시 무효화가 기존 구독자에게 전파됩니다.

,

🤖 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 31 - 35, The current emitAll in UserRepositoryImpl filters out
nulls via userLocalDataSource.userProfile.filterNotNull(), so after clearCache()
existing collectors never see the "no profile" event; remove filterNotNull() and
map the nullable profile directly to a Result (e.g.,
userLocalDataSource.userProfile.map { Result.success(it) }) so the Flow is
Result<UserProfile?> and null (cache invalidation) is propagated to subscribers,
or alternatively replace with an explicit sealed state mapping that models
"NoProfile" vs "Profile" to ensure clearCache() events reach existing
collectors.

46-57: ⚠️ Potential issue | 🟠 Major

실패 시 중복 네트워크 요청 가능성이 여전히 존재합니다.

현재 double-check locking 패턴은 성공 케이스에서만 중복 요청을 방지합니다. 첫 번째 remote fetch가 실패하면 캐시가 null 상태로 유지되어, mutex를 대기 중이던 구독자들이 순차적으로 다시 fetch를 시도합니다.

Deferred<Result<UserProfile>>를 사용하여 in-flight 요청 결과를 성공/실패 모두 공유하도록 개선하면 장애 상황에서의 불필요한 backend 호출을 방지할 수 있습니다.

,

🤖 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 46 - 57, fetchAndCacheIfNeeded currently only prevents duplicate
requests on success; to share in-flight results (success or failure) create a
shared Deferred<Result<UserProfile>> field (e.g., inFlightFetch:
Deferred<Result<UserProfile>>?) and use fetchMutex to set it once when starting
a remote fetch, so subsequent callers return/await that same Deferred instead of
re-triggering fetchUserProfile; inside fetchAndCacheIfNeeded, check
userLocalDataSource.userProfile first, then inside fetchMutex if inFlightFetch
exists return/await it, otherwise assign inFlightFetch = coroutineScope.async {
userRemoteDataSource.fetchUserProfile().also { if (it.isSuccess)
userLocalDataSource.saveUserProfile(it.getOrThrow().toDomain()) } .map {
it.toDomain() } } and finally ensure you clear inFlightFetch on completion (use
invokeOnCompletion or try/finally) and handle exceptions/cancellation so callers
receive the same Result for both success and failure.
domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt (1)

7-9: ⚠️ Potential issue | 🟠 Major

인터페이스 계약이 캐시 무효화 상태를 표현하지 못합니다.

observeUserProfile()Flow<Result<UserProfile>>를 반환하므로 "프로필 없음" 상태를 나타낼 수 없습니다. clearCache() 호출 시 기존 collector에게 무효화를 전파하려면 Flow<Result<UserProfile?>> 또는 sealed class로 상태를 모델링해야 합니다.

,

🤖 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 - 9, The interface cannot represent a "no profile / cache
invalidated" state because observeUserProfile() returns
Flow<Result<UserProfile>>; change the contract to convey absence by updating
observeUserProfile() to return Flow<Result<UserProfile?>> (or replace
Result<UserProfile> with a sealed union like UserProfileState {
Success(UserProfile), Empty, Error(...) }) and similarly adjust getUserProfile()
to suspend fun getUserProfile(): Result<UserProfile?> or
Result<UserProfileState>; update implementations of observeUserProfile(),
getUserProfile(), and clearCache() so clearCache() emits the new
"empty/invalidated" state to existing collectors (e.g., emit
Result.Success(null) or the sealed Empty state) and update callers to handle the
nullable/sealed state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt`:
- Around line 52-55: Introduce a generation/version counter in
UserRepositoryImpl (e.g., an AtomicInteger field) and increment it inside
clearCache(); when calling userRemoteDataSource.fetchUserProfile() capture the
current generation into a local variable, and in the onSuccess lambda compare
that captured generation to the current counter—only call
userLocalDataSource.saveUserProfile(response.toDomain()) if they match; this
prevents in-flight fetches started before clearCache() from writing stale
profiles. Ensure the counter is thread-safe (AtomicInteger) so increments and
comparisons are safe across async callbacks.
- Around line 31-35: The current emitAll in UserRepositoryImpl filters out nulls
via userLocalDataSource.userProfile.filterNotNull(), so after clearCache()
existing collectors never see the "no profile" event; remove filterNotNull() and
map the nullable profile directly to a Result (e.g.,
userLocalDataSource.userProfile.map { Result.success(it) }) so the Flow is
Result<UserProfile?> and null (cache invalidation) is propagated to subscribers,
or alternatively replace with an explicit sealed state mapping that models
"NoProfile" vs "Profile" to ensure clearCache() events reach existing
collectors.
- Around line 46-57: fetchAndCacheIfNeeded currently only prevents duplicate
requests on success; to share in-flight results (success or failure) create a
shared Deferred<Result<UserProfile>> field (e.g., inFlightFetch:
Deferred<Result<UserProfile>>?) and use fetchMutex to set it once when starting
a remote fetch, so subsequent callers return/await that same Deferred instead of
re-triggering fetchUserProfile; inside fetchAndCacheIfNeeded, check
userLocalDataSource.userProfile first, then inside fetchMutex if inFlightFetch
exists return/await it, otherwise assign inFlightFetch = coroutineScope.async {
userRemoteDataSource.fetchUserProfile().also { if (it.isSuccess)
userLocalDataSource.saveUserProfile(it.getOrThrow().toDomain()) } .map {
it.toDomain() } } and finally ensure you clear inFlightFetch on completion (use
invokeOnCompletion or try/finally) and handle exceptions/cancellation so callers
receive the same Result for both success and failure.

In
`@domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt`:
- Around line 7-9: The interface cannot represent a "no profile / cache
invalidated" state because observeUserProfile() returns
Flow<Result<UserProfile>>; change the contract to convey absence by updating
observeUserProfile() to return Flow<Result<UserProfile?>> (or replace
Result<UserProfile> with a sealed union like UserProfileState {
Success(UserProfile), Empty, Error(...) }) and similarly adjust getUserProfile()
to suspend fun getUserProfile(): Result<UserProfile?> or
Result<UserProfileState>; update implementations of observeUserProfile(),
getUserProfile(), and clearCache() so clearCache() emits the new
"empty/invalidated" state to existing collectors (e.g., emit
Result.Success(null) or the sealed Empty state) and update callers to handle the
nullable/sealed state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 89739430-f266-4f30-b584-2899f7f01abf

📥 Commits

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

📒 Files selected for processing (4)
  • data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/GetUserProfileUseCase.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt

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 및 반응형 패턴 도입)

2 participants