Skip to content

[Refactor/#192] DailyEmotion 데이터 흐름 개선#193

Open
wjdrjs00 wants to merge 3 commits intodevelopfrom
refactor/#192-daily-emotion-flow
Open

[Refactor/#192] DailyEmotion 데이터 흐름 개선#193
wjdrjs00 wants to merge 3 commits intodevelopfrom
refactor/#192-daily-emotion-flow

Conversation

@wjdrjs00
Copy link
Member

@wjdrjs00 wjdrjs00 commented Mar 8, 2026

[ PR Content ]

스크럼 시간에 언급했던 flow 기반의 데이터 스트림 방식을 DailyEmotion(오늘의 감정) 부분에 먼저 적용을 해봤습니다.
작업 문서는 노션에 띠로 정리했습니다.! -> 🔗 노션 링크

Related issue

Screenshot 📸

  • N/A

Work Description

  • EmotionRepositoryImpl.kt: StateFlow 도입, onSubscription 및 중복 호출 방지 로직 적용
  • DailyEmotion.kt: fetchedDate 필드 및 isStale 속성 추가를 통한 상태 판단 로직 내재화
  • HomeViewModel.kt / RecommendRoutineViewModel.kt: 명령형 로직 제거 및 Flow 관찰 방식으로 리팩터링
  • 삭제: EmotionChangeEvent.kt, FetchDailyEmotionUseCase.kt, GetEmotionChangeEventFlowUseCase.kt (레거시 제거)

To Reviewers 📢

  • 궁금한 사항이 있다면 리뷰 남겨주세요!

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 일일 감정 데이터를 구독 방식으로 실시간 관찰할 수 있는 기능이 도입되었습니다.
  • 개선 사항

    • 구독 시 자동으로 최신 데이터를 가져와 UI에 즉시 반영됩니다.
    • 중복 동시 조회를 차단해 감정 데이터 갱신이 더 안정적이고 일관되게 처리됩니다.
  • 제거

    • 기존의 이벤트 기반 감정 변경 알림 경로가 단순화되어 제거되었습니다.

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

coderabbitai bot commented Mar 8, 2026

Walkthrough

이벤트 기반 감정 갱신을 제거하고 StateFlow 기반의 반응형 dailyEmotionFlow를 도입했습니다. DailyEmotion에 fetchedDate/isStale가 추가되고, 저장소에서 onSubscription 자동 로드 및 AtomicBoolean 기반 중복 호출 방지가 구현되었습니다. ViewModel들은 ObserveDailyEmotionUseCase로 전환되었습니다.

Changes

Cohort / File(s) Summary
도메인 모델 변경
domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/DailyEmotion.kt
fetchedDate: LocalDate 필드 추가, isStale(today: LocalDate) 메서드 및 companion object { INIT } 추가.
응답 매핑 변경
data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt
toDomain() 시그니처가 toDomain(fetchedDate: LocalDate)로 변경되어 fetchedDate를 도메인에 전달하도록 수정.
저장소 구현 리팩터링
data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt
MutableStateFlow(DailyEmotion.INIT) 도입, onSubscription으로 자동 fetch 트리거, AtomicBoolean으로 동시성 제어, fetchDailyEmotion() 시그니처 및 반환형 변경, 기존 EmotionChangeEvent 흐름 제거.
저장소 인터페이스 변경
domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt
val dailyEmotionFlow: Flow<DailyEmotion> 추가, fetchDailyEmotion() 시그니처 변경(currentDate 제거, Result<Unit> 반환), EmotionChangeEvent 관련 API 제거.
이벤트 관련 파일 제거
domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionChangeEvent.kt, domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionChangeEventFlowUseCase.kt
EmotionChangeEvent 타입 및 관련 유스케이스 삭제.
유스케이스 리네임/변경
domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt
FetchDailyEmotionUseCaseObserveDailyEmotionUseCase, 반환 타입이 suspend Result<DailyEmotion>에서 Flow<DailyEmotion>으로 변경되어 repository.dailyEmotionFlow를 반환.
프레젠테이션 변경
presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt, presentation/src/main/java/com/threegap/bitnagil/presentation/screen/recommendroutine/RecommendRoutineViewModel.kt
기존 Fetch/GetEmotionChangeEvent 유스케이스 제거, ObserveDailyEmotionUseCase 주입으로 전환. container에 buildSettings 추가 및 구독 기반 일일 감정 관찰 로직 도입.

Sequence Diagram(s)

sequenceDiagram
    participant VM as ViewModel
    participant UC as ObserveDailyEmotionUseCase
    participant Repo as EmotionRepositoryImpl
    participant Remote as RemoteDataSource
    participant DB as LocalCache

    rect rgba(135,206,250,0.5)
    VM->>UC: 구독 시작 (ObserveDailyEmotionUseCase())
    end

    UC->>Repo: dailyEmotionFlow 구독
    Note right of Repo: MutableStateFlow onSubscription
    Repo->>Repo: isFetching 검사 (AtomicBoolean)
    alt stale or INIT
        Repo->>Remote: API 호출 (fetch)
        Remote-->>Repo: 응답
        Repo->>DB: 캐시 갱신 (선택적)
        Repo->>Repo: _dailyEmotionFlow 업데이트 (DailyEmotion with fetchedDate)
        Repo-->>UC: Flow 업데이트 전파
        UC-->>VM: DailyEmotion 방출
    else fresh
        Repo-->>UC: 현재 값 반환 (no network)
        UC-->>VM: DailyEmotion 방출
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

세환

🐰 깡총깡총 코드 숲을 달려,
StateFlow로 새벽을 맞이했네.
캐시도 챙기고, 동시성도 지켜,
모두의 화면에 감정이 반짝이네. ✨

🚥 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 제목은 DailyEmotion 데이터 흐름 개선이라는 주요 변경사항을 명확히 요약하고 있으며, 리팩토링 범위와 관련 이슈(#192)를 포함하고 있습니다.
Description check ✅ Passed PR 설명은 템플릿 구조(Related issue, Screenshot, Work Description, To Reviewers)를 따르고 있으며, 작업 내용과 변경 사항을 상세히 기술하고 있습니다.
Linked Issues check ✅ Passed 모든 #192의 요구사항이 충족됨: StateFlow 도입, fetchedDate/isStale 추가, onSubscription 구현, AtomicBoolean 중복 호출 방지, 레거시 삭제(EmotionChangeEvent/FetchDailyEmotionUseCase/GetEmotionChangeEventFlowUseCase), ObserveDailyEmotionUseCase 도입, ViewModel 리팩토링.
Out of Scope Changes check ✅ Passed 모든 변경사항이 #192의 StateFlow 기반 반응형 스트림 구현 및 관련 리팩토링 범위 내에 있으며, 추가적인 비관련 변경은 없습니다.

✏️ 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/#192-daily-emotion-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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt (1)

21-28: ⚠️ Potential issue | 🟠 Major

fetchedDate는 변환 시각이 아니라 요청 기준 일자를 사용해야 합니다.

27번 줄에서 LocalDate.now()를 호출하면, EmotionRepositoryImpl.kt 46번 줄에서 고정한 요청 일자와 어긋날 수 있습니다. 자정 전후에 응답이 돌아오면 전날 데이터를 받아놓고도 fetchedDate가 오늘 날짜로 기록되어 isStalefalse가 되고, 다음 구독에서 재조회가 차단됩니다. 요청에 사용한 LocalDatetoDomain(requestedDate: LocalDate) 매개변수로 전달하는 방식이 안전합니다.

수정 예시
-fun DailyEmotionResponse.toDomain(): DailyEmotion =
+fun DailyEmotionResponse.toDomain(fetchedDate: LocalDate): DailyEmotion =
     DailyEmotion(
         type = emotionMarbleType,
         name = emotionMarbleName,
         imageUrl = imageUrl,
         homeMessage = emotionMarbleHomeMessage,
-        fetchedDate = LocalDate.now(),
+        fetchedDate = fetchedDate,
     )
-            val currentDate = LocalDate.now().toString()
-            emotionDataSource.fetchDailyEmotion(currentDate).map {
-                _dailyEmotionFlow.value = it.toDomain()
+            val requestedDate = LocalDate.now()
+            emotionDataSource.fetchDailyEmotion(requestedDate.toString()).map {
+                _dailyEmotionFlow.value = it.toDomain(requestedDate)
             }
🤖 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/emotion/model/response/DailyEmotionResponse.kt`
around lines 21 - 28, DailyEmotionResponse.toDomain currently sets fetchedDate =
LocalDate.now(), which can diverge from the request date used in
EmotionRepositoryImpl and cause stale-check bugs; change the conversion to
accept a requestedDate parameter (e.g., fun
DailyEmotionResponse.toDomain(requestedDate: LocalDate)): construct DailyEmotion
with fetchedDate = requestedDate, update all callers (notably where
EmotionRepositoryImpl maps responses at the request site) to pass the same
requestedDate used for the request, and ensure the DailyEmotion data class usage
remains consistent.
🤖 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/emotion/repositoryImpl/EmotionRepositoryImpl.kt`:
- Around line 20-25: The current compareAndSet in isFetching causes a concurrent
fetch to short-circuit and return Result.success(Unit), which can drop a
subsequent refresh triggered by registerEmotion() or a subscription's initial
fetch; change the fetch control in fetchDailyEmotion() (the routine guarded by
isFetching.compareAndSet) so that when a fetch is already in progress it either
queues a single follow-up fetch or serializes requests: e.g., replace the
immediate success return with a flag/queue (e.g., pendingRefresh boolean) that
is set when a fetch is in progress and a new fetch is requested, and after the
in-flight fetch completes have the code check pendingRefresh and perform one
additional fetch if needed; update references to isFetching, compareAndSet,
fetchDailyEmotion(), and registerEmotion() to use this handshake so a trailing
refresh is not lost.

---

Outside diff comments:
In
`@data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt`:
- Around line 21-28: DailyEmotionResponse.toDomain currently sets fetchedDate =
LocalDate.now(), which can diverge from the request date used in
EmotionRepositoryImpl and cause stale-check bugs; change the conversion to
accept a requestedDate parameter (e.g., fun
DailyEmotionResponse.toDomain(requestedDate: LocalDate)): construct DailyEmotion
with fetchedDate = requestedDate, update all callers (notably where
EmotionRepositoryImpl maps responses at the request site) to pass the same
requestedDate used for the request, and ensure the DailyEmotion data class usage
remains consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 52f22984-cc82-4ed7-bda4-56d2b9b2db63

📥 Commits

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

📒 Files selected for processing (9)
  • data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt
  • data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/DailyEmotion.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionChangeEvent.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionChangeEventFlowUseCase.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/screen/recommendroutine/RecommendRoutineViewModel.kt
💤 Files with no reviewable changes (2)
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionChangeEventFlowUseCase.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionChangeEvent.kt


init {
initialize()
observeDailyEmotion()
Copy link
Contributor

Choose a reason for hiding this comment

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

[2] RecommendRoutineViewModel에서는 container의 onCreate 블록에 observeDailyEmotion을 배치했는데, 여기에서는 init 블록에 배치한 이유가 있으신가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

기술적인 이유보단 homeViewmodel에서는 아직 변경하지 않은 로직이 남아있어서 onCreateinit 각각 정의하면 혼동할 수 가능성 때문에 기존 init에 정의하고 추후 다른 변경사항과 함께 onCreate로 변경하고자 했숩니다~!

Copy link
Contributor

@l5x5l l5x5l left a comment

Choose a reason for hiding this comment

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

Event를 각 ViewModel에서 감지해 감정 데이터 조회를 호출하는 공통 로직을 제거한 것은 좋아 보입니다! 👍
다만 수정해주신 코드에서 DailyEmotionResponse의 toDomain이나 DailyEmotion의 isStale 같이 LocalDate를 사용하는 부분을 보면 해당 정보를 인자로 전달받아 사용하기 보다는 함수 내부에서 매번 LocalData.now()를 호출하고 있어 호출 시점마다 결과가 달라질 수 있는 비결정적 코드라는 느낌을 받았습니다...!
이 부분만 외부에서 LocalDate를 주입받아 사용할 수 있도록 변경해주시면 추후 테스트시 또는 기능 유지보수시에 더 좋을 것 같아요!

@wjdrjs00
Copy link
Member Author

wjdrjs00 commented Mar 9, 2026

Event를 각 ViewModel에서 감지해 감정 데이터 조회를 호출하는 공통 로직을 제거한 것은 좋아 보입니다! 👍 다만 수정해주신 코드에서 DailyEmotionResponse의 toDomain이나 DailyEmotion의 isStale 같이 LocalDate를 사용하는 부분을 보면 해당 정보를 인자로 전달받아 사용하기 보다는 함수 내부에서 매번 LocalData.now()를 호출하고 있어 호출 시점마다 결과가 달라질 수 있는 비결정적 코드라는 느낌을 받았습니다...! 이 부분만 외부에서 LocalDate를 주입받아 사용할 수 있도록 변경해주시면 추후 테스트시 또는 기능 유지보수시에 더 좋을 것 같아요!

수정완료! -> 3edbfd1

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 (1)
data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt (1)

20-25: ⚠️ Potential issue | 🟠 Major

진행 중 fetch를 성공으로 단락하면 최신 감정 갱신이 유실됩니다.

Line 44에서 이미 fetch 중이면 바로 Result.success(Unit)을 반환해서, Line 39의 후행 refresh가 Lines 23-25의 초기 구독 fetch와 겹칠 때 그대로 드롭됩니다. 첫 요청이 등록 이전 상태를 읽었다면 _dailyEmotionFlow는 같은 날짜 동안 stale로 간주되지 않아 오래된 값을 유지할 수 있습니다. 중복 호출만 막지 말고, 진행 중 요청 뒤에 refresh 1회를 보장하도록 직렬화하거나 pending 플래그를 두는 쪽이 안전합니다.

예시 수정안
-import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.sync.Mutex

-    private val isFetching = AtomicBoolean(false)
+    private val fetchMutex = Mutex()
+    `@Volatile`
+    private var pendingRefresh = false

     override suspend fun fetchDailyEmotion(): Result<Unit> {
-        if (!isFetching.compareAndSet(false, true)) return Result.success(Unit)
+        if (!fetchMutex.tryLock()) {
+            pendingRefresh = true
+            return Result.success(Unit)
+        }
         return try {
-            val today = LocalDate.now()
-            emotionDataSource.fetchDailyEmotion(today.toString()).map {
-                _dailyEmotionFlow.value = it.toDomain(today)
-            }
+            var result: Result<Unit>
+            do {
+                pendingRefresh = false
+                val today = LocalDate.now()
+                result = emotionDataSource.fetchDailyEmotion(today.toString()).map {
+                    _dailyEmotionFlow.value = it.toDomain(today)
+                }
+            } while (pendingRefresh)
+            result
         } finally {
-            isFetching.set(false)
+            fetchMutex.unlock()
         }
     }

Also applies to: 39-52

🤖 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/emotion/repositoryImpl/EmotionRepositoryImpl.kt`
around lines 20 - 25, The current concurrency handling in EmotionRepositoryImpl
uses isFetching to short-circuit fetchDailyEmotion and returns early, which can
drop a later refresh requested from onSubscription and leave _dailyEmotionFlow
stale; update fetchDailyEmotion (and related callers) so that when isFetching is
true you record a pending-refresh flag (or enqueue one) instead of returning
success immediately, and when the in-flight fetch completes the code checks that
flag to run exactly one additional fetch (or emit a refresh) and clear it;
reference isFetching, fetchDailyEmotion, _dailyEmotionFlow and dailyEmotionFlow
to locate where to add the pending flag/serialization and ensure
onSubscription-triggered fetches are not lost.
🤖 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/emotion/repositoryImpl/EmotionRepositoryImpl.kt`:
- Around line 20-25: The current concurrency handling in EmotionRepositoryImpl
uses isFetching to short-circuit fetchDailyEmotion and returns early, which can
drop a later refresh requested from onSubscription and leave _dailyEmotionFlow
stale; update fetchDailyEmotion (and related callers) so that when isFetching is
true you record a pending-refresh flag (or enqueue one) instead of returning
success immediately, and when the in-flight fetch completes the code checks that
flag to run exactly one additional fetch (or emit a refresh) and clear it;
reference isFetching, fetchDailyEmotion, _dailyEmotionFlow and dailyEmotionFlow
to locate where to add the pending flag/serialization and ensure
onSubscription-triggered fetches are not lost.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b5f7366c-1d2e-4fa6-aeff-8c044aa64d10

📥 Commits

Reviewing files that changed from the base of the PR and between ead9462 and 3edbfd1.

📒 Files selected for processing (3)
  • data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt
  • data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/DailyEmotion.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] DailyEmotion 데이터 흐름을 StateFlow 기반 반응형 스트림으로 개선

2 participants