Skip to content

fix: 최근 검색 동시 저장 유실 — Repository를 actor로 - #366

Merged
YuGeonHui merged 1 commit into
env/devfrom
fix/v2-three-bugs
Sep 24, 2026
Merged

YuGeonHui merged 1 commit into
env/devfrom
fix/v2-three-bugs

Conversation

@YuGeonHui

Copy link
Copy Markdown
Collaborator

실제 버그: 최근 검색 유실 (재현 확인)

RecentSearchRepositoryImpl.save/remove가 read-modify-write인데 값 타입이라 단일 writer가 보장되지 않았습니다. AppDIContainer:234가 repository를 1회 생성해 홈·검색이 공유하므로 동시 쓰기 경로가 실재합니다.

단일 인스턴스에 4건 동시 저장
struct (수정 전) ["서울역", "홍대입구역"] — 2건 유실
actor (수정 후) 4건 전부 보존

actor로 전환해 단일 writer를 타입으로 강제합니다. RecentSearchRepository 프로토콜이 이미 async throws라 호출처 변경은 없습니다.

회귀 테스트 작성 시 주의점

반드시 단일 인스턴스로 검증해야 합니다. actor 격리는 인스턴스 단위라, 매번 새 인스턴스를 만드는 computed property sut를 쓰면 수정 후에도 실패합니다 (실제로 그렇게 헛짚었습니다). 실제 앱의 공유 조건과 일치시켜야 수정 전후가 갈립니다.

잠재 버그: refreshPulled의 defer 누락

Task.isCancelled 경로에서 early return하면 refreshTask가 영구히 non-nil로 남아 진입 가드에 영원히 걸리고(당김 새로고침 영구 잠김), onManualSyncFinished도 불리지 않아 스피너가 멈추지 않는 구조입니다.

다만 refreshTask?.cancel()이 deinit에서만 호출되므로 취소 시점엔 VM이 해제 중이라 [weak self]가 먼저 nil이 되어 현재 코드 경로에서는 발현되지 않습니다. 방어적 수정이자 같은 파일 chipTapped(:261)와의 일관성 확보입니다.

오진이었던 것 (변경 없음)

SceneDelegate:108-109의 UserDefaults 직접 읽기는 버그가 아니었습니다. 어댑터 :341-343이 devDismissedDefaultsKey를 nonisolated static으로 의도적으로 노출하며 "MainActor(UI)에서 동기로 읽어도 어긋나지 않는다" 고 명시합니다. 경고 주석(:346-347)은 쓰기에 대한 것이고 여기는 읽기이며, #if DEV 한정입니다.

검증

Domain 92 / AtchaData 53 / AtchaV2 20 / Home 66 / Search 26 / Settings 12 — 전부 통과


base 주의: 이 PR은 feat/v2-guest-auth 위에 쌓여 있습니다. #365를 먼저 머지한 뒤 이 PR의 base가 자동으로 env/dev로 바뀝니다.

🤖 Generated with Claude Code

## 실제 버그: 최근 검색 유실 (재현 확인)

`RecentSearchRepositoryImpl.save`/`remove`가 read-modify-write인데 값 타입이라
단일 writer가 보장되지 않았다. `AppDIContainer:234`가 repository를 1회 생성해
홈(`promoteChipDestination`)과 검색(`saveTask`)이 공유하므로 동시 쓰기 경로가
실재한다.

단일 인스턴스에 4건을 동시 저장하는 테스트로 재현:

    struct (수정 전): ["서울역", "홍대입구역"]      2건 유실
    actor  (수정 후): 4건 전부 보존

`actor`로 전환해 단일 writer를 타입으로 강제한다. `RecentSearchRepository`
프로토콜이 이미 `async throws`라 호출처 변경은 없다.

회귀 테스트는 **반드시 단일 인스턴스로** 검증한다 — actor 격리는 인스턴스
단위라, 매번 새 인스턴스를 만드는 computed property `sut`를 쓰면 수정 후에도
실패한다(실제로 그렇게 헛짚었다). 실제 앱의 공유 조건과 일치시켜야 한다.

## 잠재 버그: refreshPulled의 defer 누락

`Task.isCancelled` 경로에서 early return하면 `refreshTask`가 영구히 non-nil로
남아 진입 가드에 영원히 걸리고(당김 새로고침 영구 잠김) `onManualSyncFinished`도
불리지 않아 스피너가 멈추지 않는 구조다.

다만 `refreshTask?.cancel()`이 `deinit`에서만 호출되므로 취소 시점엔 VM이 해제
중이라 `[weak self]`가 먼저 nil이 되어 **현재 코드 경로에서는 발현되지 않는다.**
방어적 수정이자 같은 파일 `chipTapped`(:261)와의 일관성 확보다.

## 오진이었던 것

`SceneDelegate:108-109`의 UserDefaults 직접 읽기는 버그가 아니다. 어댑터
`:341-343`이 `devDismissedDefaultsKey`를 `nonisolated static`으로 의도적으로
노출하며 "MainActor(UI)에서 동기로 읽어도 어긋나지 않는다"고 명시한다.
경고 주석(`:346-347`)은 쓰기에 대한 것이고 여기는 읽기이며, `#if DEV` 한정이다.

검증: Domain 92 / AtchaData 53 / AtchaV2 20 / Home 66 / Search 26 / Settings 12

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@YuGeonHui
YuGeonHui changed the base branch from feat/v2-guest-auth to env/dev September 24, 2026 10:06
@YuGeonHui YuGeonHui closed this Sep 24, 2026
@YuGeonHui YuGeonHui reopened this Sep 24, 2026
@YuGeonHui
YuGeonHui merged commit facc6e3 into env/dev Sep 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant