Skip to content

fix(receiver): 재진입 갱신 결선의 잔재를 지우고 결선 소실을 가드로 막는다 (#1725) - #1638

Merged
OnlyReadOneBook merged 4 commits into
developfrom
fix/701-detail-refresh-on-return
Sep 2, 2026
Merged

OnlyReadOneBook merged 4 commits into
developfrom
fix/701-detail-refresh-on-return

Conversation

@OnlyReadOneBook

@OnlyReadOneBook OnlyReadOneBook commented Aug 30, 2026 •

Copy link
Copy Markdown
Collaborator

📌𝘐𝘴𝘴𝘶𝘦𝘴

#701 의 «결선 소실 회귀 가드» 몫을 #1725 로 분리했다. 부모 #701 에는 받은 기록함 결선(#607 선행)과 목록성 화면의 당겨서 새로고침 판단이 남는다 (아래 「남는 범위」).

📎𝘞𝘰𝘳𝘬 𝘋𝘦𝘴𝘤𝘳𝘪𝘱𝘵𝘪𝘰𝘯

착수 전 실측 — 이슈 본문(2026-08-03)은 낡았다

develop 124547cbb 기준으로 6화면을 다시 쟀다. 결선이 필요한 4화면은 이미 결선돼 있다 — PR #1374 가 2026-08-29 에 머지됐다.

화면 ViewModel 결선 자리 상태
애프터노트 상세 AfternoteDetailViewModel AfternoteDetailRoute.kt:159 ✅
수신 애프터노트 상세 ReceivedAfternoteDetailViewModel ReceivedAfternoteDetailRoute.kt:42 ✅
수신 추억 플레이리스트 ReceiverMemorialPlaylistViewModel ReceivedAfternoteNavGraph.kt:51 ✅
발신자 상세 SenderDetailViewModel SenderDetailScreen.kt:69 ✅
수신자 홈 ReceiverHomeViewModel ReceiverHomeEntry.kt ✅ PR #1666 (#1452, 09-01)
받은 기록함 ReceivedRecordsViewModel 없음 (init 로드도 없음) ⛔ #607

네 화면은 프로덕션 진입점이 각각 하나뿐이고 그 하나가 전부 결선돼 있다. 두 모듈(feature:afternote:presentation · feature:receiver:presentation)의 ViewModel 을 전수로 훑어도, init 로드를 하면서 재로드 진입점이 없는 것은 ReceiverHomeViewModel 하나다.

그래서 이 PR 에 새로 걸 결선이 없다. 대신 실측에서 나온 두 가지를 닫는다.

1. 이동 뒤 남은 import 잔재

ReceiverNavGraph.kt 에 Lifecycle · LifecycleEventEffect import 두 줄이 쓰이지 않은 채 남아 있었다. 수신 애프터노트 화면 15파일을 afternote 모듈로 옮기며(2c6da444b · #1461) 이 파일의 LifecycleEventEffect(ON_RESUME) 결선 두 벌이 함께 빠져나갔는데 import 만 남은 것이다.

결선 자체는 옮겨간 자리에서 정상 동작하므로 동작 변화는 없다. 다만 :feature:receiver:presentation:ktlintCheck 는 이 잔재를 둔 채 초록이었다 — 실제로 develop 에서 돌려 확인했다. 이 저장소의 ktlint 설정에서 unused-import 는 잡히지 않는다.

2. 결선 소실 회귀 가드 (RefreshOnReturnWiringKonsistTest)

refreshOnReturn() 은 ViewModel 의 함수 하나와 화면의 한 줄로 갈라져 있다.

LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { viewModel.refreshOnReturn() }

화면 쪽 한 줄이 사라져도 컴파일이 통과하고, ViewModel 단위 테스트도 계속 초록이다. 함수는 그대로 남아 있으니 어떤 CI 신호도 뜨지 않고, 사용자에게만 「수정하고 돌아왔는데 옛 값이 그대로」로 나타난다 — 이 이슈가 애초에 고친 상태 그 자체다.

그리고 그 한 줄은 실제로 리팩터링에 두 번 휩쓸렸다.

  1. refactor(afternote): 수신 애프터노트 화면 15파일을 afternote 모듈 receiver 패키지로 (#275 초과 실행분 복원) #1461 의 모듈 이동이 ReceiverNavGraph 의 결선을 들어냈다. 옮겨간 자리에 다시 결선돼 동작은 살았지만, 그 사실을 알려 준 CI 신호는 없었다 (위 1번 잔재가 그 흔적이다).
  2. 수신자 홈 담당 이관(f01529dc1)은 결선과 refreshOnReturn() 을 함께 되돌려 짝이 맞았다. 함수만 남기고 결선만 지웠다면 역시 아무 신호도 없었을 것이다.

규칙 — main 소스셋의 클래스가 refreshOnReturn 을 선언하면, 자기 파일이 아닌 main 파일 중 Lifecycle.Event.ON_RESUME 과 refreshOnReturn( 을 함께 갖고 그 클래스 이름을 식별자로 참조하는 파일이 최소 하나 있어야 한다.

  • 검사는 프로젝트 전역이다. 홈 탭은 feature:home:presentation 의 ViewModel 을 app 모듈 AppNavigation 에서 걸기 때문에, 모듈 안으로 좁히면 그 결선이 위반으로 잡힌다.
  • 화면 하나가 여러 ViewModel 을 거는 자리(마인드레코드 홈의 카테고리 분기)도 같은 파일이 세 이름을 모두 참조하므로 그대로 통과한다.
  • 「ON_RESUME 안에서 정말 그 ViewModel 의 것을 부르는가」까지는 보지 않는다. 텍스트로 그 이상을 단언하면 오탐이 늘고, 이 가드가 막으려는 것은 「결선이 통째로 사라진」 경우다. 계약 자체(로딩 미방출 · 실패 시 화면 유지 · 첫 resume 스킵)는 각 ViewModel 의 단위 테스트가 이미 지킨다.

현행 refreshOnReturn 선언 11건이 전부 통과한다 (afternote 3 · mindrecord 6 · home 1 · receiver 1).

남는 범위 — 이 PR 이 닫지 않는 것

두 화면이 남아 있으므로 이슈는 Refs 로만 걸고 닫지 않는다.

해야 할 일 2 — 「목록성 화면에 당겨서 새로고침을 둘지」 판단

이 PR 에서는 두지 않는다. 대상으로 지목된 두 화면이 정확히 위의 제외 대상 둘(받은 기록함 · 수신자 홈)이라, 이 PR 이 손댈 수 있는 파일이 하나도 없다.

판단 자체도 지금은 이르다고 본다.

ON_RESUME 결선만으로도 이슈가 지목한 피해 경로(수정 후 복귀 · 탭 왕복 · 프로세스 백그라운드 복귀)는 닫힌다 — ON_RESUME 은 앱이 포그라운드로 돌아올 때도 발화하므로, 첫 코멘트가 든 「서버가 죽었다 살아난 뒤」 축도 결선된 4화면에서는 함께 덮인다.

🧪 CI Test Plan

{
  "androidTest": {
    "mode": "selected",
    "reason": "ReceiverNavGraph.kt 는 feature/*/presentation 의 navigation 패키지라 정책상 계측 경계다. 실제 변경은 쓰이지 않는 import 2줄 삭제로 바이트코드가 동일하지만, 경계 판정은 파일 경로로 내려가므로 그 그래프를 런타임으로 통과하는 테스트를 고른다. 선택한 둘은 이 파일이 정의하는 것을 각각 짚는다 — 시작 목적지(Route.Receiver → ReceivedRecordsRoute)와 열람 신청 중첩 그래프의 단계 이동·완료 복귀다. api30 으로 고정한 것은 api34 lane 이 BAL 로 막혀 있기 때문이다. konsist 가드는 JVM 전용이라 계측 대상이 아니다.",
    "tests": [
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/AppAndReceiverCompletionAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.AppAndReceiverCompletionAndroidTest#welcomeCheckRecords_opensActualReceivedRecordsStartDestination",
        "device": "api30"
      },
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/AppAndReceiverCompletionAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.ReceiverRuntimeCompletionAndroidTest#receiverVerificationActions_removeConsumedStepsAndCompletionReturnsToRecords",
        "device": "api30"
      }
    ]
  }
}

💬𝘛𝘰 𝘙𝘦𝘷𝘪𝘦𝘸𝘦𝘳𝘴

1. 가드는 양방향으로 확인했다. 통과만 보고 넣으면 아무것도 안 보는 테스트가 될 수 있어, SenderDetailScreen 의 결선 한 줄을 지우고 실패부터 확인했다.

refreshOnReturn() 은 선언돼 있는데 ON_RESUME 결선이 없는 ViewModel 이 있다 (1건).
컴파일도 단위 테스트도 통과하지만, 화면은 재진입해도 옛 값을 그대로 보여준다 (#701).

  feature/receiver/.../senderdetail/SenderDetailViewModel.kt — SenderDetailViewModel

화면(Route·Screen·NavGraph)에서 다음 한 줄을 되살린다:
  LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { viewModel.refreshOnReturn() }
갱신이 더는 필요 없다면 ViewModel 의 refreshOnReturn() 도 함께 지운다 — 짝을 맞춘다.

2. 확인 과정에서 발견한 별건 — konsist 테스트는 다른 모듈이 바뀌어도 UP-TO-DATE 로 건너뛴다. 결선을 지운 채 :konsist:test 를 그냥 돌렸더니 UP-TO-DATE 로 스킵돼 초록이 나왔고, --rerun-tasks 를 줘야 실패했다. konsist 는 다른 모듈의 소스를 런타임에 읽는데 그 파일들이 Gradle 태스크 입력으로 선언돼 있지 않아서다.

이 PR 은 konsist/src/test 를 건드리므로 resolve-pr-impact 의 runKonsist 분기(#1570 의 52d8b3120)를 타 확실히 실행된다. 다만 프로덕션 소스만 바꾸는 PR 에서 6개 가드 전부가 같은 성질을 갖는다 — 이 PR 이 만든 문제가 아니라 konsist 모듈 전반의 성질이라 여기서 고치지 않았다. 별도로 다루는 편이 맞아 보이는데, 이슈로 떼는 게 좋을지 의견 부탁드립니다.

3. QA · 시안 대조: 해당 없음. 화면에 그려지는 것이 바뀌지 않는다 — 쓰이지 않는 import 삭제와 테스트 추가뿐이라 렌더 결과가 동일하고, 대조할 시안이 없다.

로컬 검증

:feature:receiver:presentation:compileDebugKotlin          BUILD SUCCESSFUL
:feature:receiver:presentation:compileDebugUnitTestKotlin  BUILD SUCCESSFUL
:feature:receiver:presentation:testDebugUnitTest           40 tests, 0 failures, 0 skipped
:feature:receiver:presentation:ktlintCheck                 BUILD SUCCESSFUL
:konsist:ktlintCheck                                       BUILD SUCCESSFUL
:konsist:test (--rerun-tasks)                              12 tests, 0 failures  (신규 +1)
:app:compileDebugKotlin                                    BUILD SUCCESSFUL

수신 애프터노트 화면 15파일을 afternote 모듈로 옮기며(2c6da44 · #1461)
ReceiverNavGraph 의 LifecycleEventEffect(ON_RESUME) 결선 두 벌이 함께 빠져나갔는데,
그때 쓰이던 Lifecycle · LifecycleEventEffect import 만 파일에 남았다.

ktlint 는 이 import 를 잡지 못한다 — :feature:receiver:presentation:ktlintCheck 가
잔재를 그대로 둔 채 초록이었다. 결선이 옮겨간 자리(ReceivedAfternoteNavGraph ·
ReceivedAfternoteDetailRoute)에서 정상 동작하므로 동작 변화는 없다.

Refs #701
refreshOnReturn() 은 ViewModel 의 함수 하나와 화면의 LifecycleEventEffect(ON_RESUME)
한 줄로 갈라져 있다. 화면 쪽 한 줄이 사라져도 컴파일이 통과하고 ViewModel 단위 테스트도
계속 초록이다 — 사용자에게만 「수정하고 돌아왔는데 옛 값이 그대로」로 나타난다.
이 이슈가 고친 상태 그 자체이고, 리팩터링이 실제로 그 한 줄을 들어냈던 전례가 있다.

main 소스셋의 클래스가 refreshOnReturn 을 선언하면, 자기 파일이 아닌 main 파일 중
Lifecycle.Event.ON_RESUME 과 refreshOnReturn( 을 함께 갖고 그 클래스 이름을 참조하는
파일이 최소 하나 있어야 한다. 홈 탭처럼 모듈을 가로지르는 결선(feature:home 의 VM 을
app 모듈 AppNavigation 이 건다)도 통과하도록 검사는 프로젝트 전역이다.

현행 선언 11건이 모두 통과하고, SenderDetailScreen 의 결선을 지우면 그 ViewModel 을
파일 경로와 함께 지목하며 실패하는 것을 확인했다.

Refs #701
@github-actions github-actions Bot added the android-test PR head에서 Android Managed Device 계측 테스트 실행 label Aug 30, 2026
@OnlyReadOneBook

Copy link
Copy Markdown
Collaborator Author

정정 — konsist UP-TO-DATE 는 로컬 함정이고, CI 에는 구멍이 없습니다

본문 「To Reviewers」 2번에서 「프로덕션 소스만 바꾸는 PR 에서 6개 가드 전부가 같은 성질을 갖는다」며 이슈로 뗄지 물었습니다. CI 를 실측하니 그 우려는 성립하지 않습니다.

로컬 관찰은 맞습니다 (재현했습니다)

1회차                              > Task :konsist:test            (실행)
afternote presentation 소스 한 줄 수정
2회차                              > Task :konsist:test UP-TO-DATE (스킵)

konsist 가 런타임에 읽는 타 모듈 소스가 Gradle 태스크 입력으로 선언돼 있지 않아서라는 진단도 그대로입니다.

그런데 CI 에서는 스킵되지 않습니다

konsist 를 건드리지 않은 PR(#1641, app·core 만 변경)의 Unit Test job 로그입니다.

> Task :konsist:testClasses UP-TO-DATE
> Task :konsist:test                    ← UP-TO-DATE 없이 실행

러너가 매번 새 워크스페이스라 프로젝트 build/ 출력이 없고, 캐시하는 것은 Gradle User Home 뿐입니다. 같은 job 에 FROM-CACHE 태스크가 266건 있는데도 :konsist:test 는 실제로 돌았습니다.

그래서

이슈로 뗄 것은 없다고 봅니다. 가드가 막으려던 것은 CI 에서 그대로 막히고, 남는 것은 로컬 검증 습관입니다 — 「로컬 :konsist:test 초록」을 「가드 통과」로 읽으면 안 되고, konsist 가 볼 만한 변경을 했으면 --rerun-tasks 를 붙여야 합니다. 이 PR 이 이미 --rerun-tasks 로 검증했고 새 가드를 옛 코드에 대고 실패까지 확인한 것이 그 관행의 실례입니다.

같은 이유로 오늘 #1652(#599) 검증에서도 --rerun-tasks 를 붙였습니다 — 안 붙인 첫 실행이 3초 만에 BUILD SUCCESSFUL 로 끝나 캐시 스킵이 바로 드러났습니다.

@OnlyReadOneBook
OnlyReadOneBook added this pull request to the merge queue Sep 2, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 2, 2026
@OnlyReadOneBook
OnlyReadOneBook added this pull request to the merge queue Sep 2, 2026
@OnlyReadOneBook OnlyReadOneBook changed the title fix(receiver): 재진입 갱신 결선의 잔재를 지우고 결선 소실을 가드로 막는다 (#701) fix(receiver): 재진입 갱신 결선의 잔재를 지우고 결선 소실을 가드로 막는다 (#) Sep 2, 2026
@OnlyReadOneBook OnlyReadOneBook changed the title fix(receiver): 재진입 갱신 결선의 잔재를 지우고 결선 소실을 가드로 막는다 (#) fix(receiver): 재진입 갱신 결선의 잔재를 지우고 결선 소실을 가드로 막는다 (#1725) Sep 2, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

android-test PR head에서 Android Managed Device 계측 테스트 실행

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test(receiver): 재진입 갱신 결선 소실을 konsist 가드로 막고 이동 잔재 import 를 지운다 (#701 분리)

2 participants