Skip to content

refactor(core): UserRepository 구현·fixture 를 수신자·프로필 책임으로 분리 (#1738) - #1595

Merged
OnlyReadOneBook merged 4 commits into
developfrom
refactor/1282-user-repository-split-stage2
Sep 4, 2026
Merged

OnlyReadOneBook merged 4 commits into
developfrom
refactor/1282-user-repository-split-stage2

Conversation

@OnlyReadOneBook

@OnlyReadOneBook OnlyReadOneBook commented Aug 30, 2026 •

Copy link
Copy Markdown
Collaborator

📌𝘐𝘴𝘴𝘶𝘦𝘴

#1282 의 구현·fixture 분리 몫을 #1738 로 분리했다. 소비자 이관은 모듈별 담당자 이슈(#1739 setting · #1740 timeletter · #1741 mindrecord · #1742 home · #1743 afternote)로 세웠고, 합본 제거는 #1282 에 남는다.

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

1단계 #1409 는 계약만 갈라 뒀다(UserReceiverRepository·MyProfileRepository 신설, UserRepository 는 둘을 상속). 구현(UserRepositoryImpl)과 공용 fake(FakeUserRepository)는 여전히 17멤버 합본이라 이슈의 «각 Repository 구현은 필요한 API/DataStore와 세션·캐시 상태만 소유한다»·«공용 FakeUserRepository 를 책임별 test fixture 로 분리한다» 두 기준이 비어 있었다. 이 PR 이 그 둘을 채운다.

1단계와 같은 원칙으로 프로덕션은 코어 안에서 닫는다 — core:domain·core:data 밖의 프로덕션 코드는 0줄이고, 소비자·DI 계약·fake 공개 API 는 한 글자도 안 바뀐다. 테스트 2곳(app androidTest·feature:setting 테스트)만 새 조립 모양을 따라왔다(아래 «테스트가 따라온 것»).

core:data — 구현 분리

신설 소유
UserReceiverRepositoryImpl 수신자 8멤버 + receiverRefreshRevision + «로그인 구간 + collector» 귀속 목록 캐시
MyProfileRepositoryImpl 서버 정본 프로필 2멤버 (상태 없음, UserApiService 위임 + 매퍼)

UserRepositoryImpl 은 그 둘에 by 위임하고, 아직 core 밖으로 못 내려간 계정·푸시 6멤버만 직접 갖는다 — 283줄 → 110줄.

두 구현은 Hilt 가 주입한다. 상태(revision·목록 캐시)의 실제 소유자인 UserReceiverRepositoryImpl 클래스에 @Singleton 을 두고, UserRepositoryImpl 은 UserReceiverRepository·MyProfileRepository 계약 2종을 생성자로 받아 by 위임한다. CoreUserRepositoryModule 의 좁은 계약 2종 바인딩은 그 구현 클래스로 직접 간다. 스코프가 상태 소유자에 있으므로 합본을 거치든 좁은 계약을 직접 주입받든 같은 인스턴스다 — 좁은 계약으로 만든 수신자가 합본 구독자의 목록도 갱신한다. MyProfileRepositoryImpl 은 상태가 없어 unscoped 이고, UserRepositoryImpl 은 형제 구현들과 같이 internal 로 닫았다.

이 «같은 인스턴스» 계약이 조용히 깨지지 않도록 회귀 테스트를 하나 깔았다 — 좁은 계약으로 createReceiver 한 결과가 합본 계약의 receiverListFlow 구독자에게 도달하는지 본다.

core:domain testFixtures — fake 분리

FakeUserReceiverRepository·FakeMyProfileRepository 를 신설하고, FakeUserRepository 는 그 둘에 위임하는 합본으로 남겼다. 보존한 것: 생성자 파라미터 22개의 이름·순서·기본값, receiverState·receiverDetails·호출 기록 리스트·카운터 등 공개 프로퍼티 전부(생성 후 onX 재대입과 상태 쓰기가 위임 대상에 그대로 닿는다), 중첩 호출 기록 타입 7종, strict() 의 "UserRepository.*" 문구.

호출 기록 타입(ReceiverCreateCall 등)은 아직 FakeUserRepository 안에 남긴다. feature:setting 테스트가 import ...FakeUserRepository.ReceiverCreateCall as ... 로 참조하고 있어(중첩 classifier 는 typealias 로 못 받는다) 지금 옮기면 남의 모듈이 깨진다. 소비자 이관 때 함께 옮긴다.

테스트가 따라온 것 — 프로덕션은 테스트에 맞추지 않는다

첫 리비전은 app androidTest 와 feature:setting 테스트가 UserRepositoryImpl(userApi, authRepository, errorReporter) 를 직접 조립한다는 이유로 생성자 시그니처와 수동 조립을 남겼다. 테스트가 프로덕션 모양을 정한 것이라 걷어냈고, 테스트는 각자 자기 층으로 내려갔다.

  • :core:data 테스트 2건 — 같은 모듈이라 구현 3종을 프로덕션과 같은 모양으로 조립한다(UserRepositoryImplTest 의 repositoryOf). ReceiverRequestFailureTest 는 helper 가 사는 UserReceiverRepositoryImpl 을 직접 조립한다.
  • SettingCompletionTest.withdrawal_* — 실구현 대신 FakeUserRepository 로 화면 상태 전이(실패 → Error → 재시도 → Success)만 본다. «탈퇴 성공 뒤 세션 정리는 정확히 한 번, 서버 호출 뒤» 는 저장소 계약이라 UserRepositoryImplTest 의 deleteAccount 4건이 이미 고정한다. 실구현 조립용 픽스처 3종(gated UserApiService·ledger·상태형 auth fake)은 삭제.
  • ReceiverSessionIsolationAndroidTest — 실구현 대신 appTestUserRepository 로 «목록이 비면 이전 계정 식별자가 화면에 남지 않고 화면은 살아 있다» 만 본다. «로그아웃·새 세션 401 을 예외로 흘리지 않고 빈 목록으로 낮춘다» 는 UserRepositoryImplTest 의 receiverListFlow 계열이 고정한다. 가짜 UserApiService 삭제.

🧭 범위에서 뺀 것

🧪 검증 (전부 로컬 실행, 성공)

계약을 깨면 남의 모듈 테스트가 먼저 깨지므로 소비 모듈까지 전부 돌렸다.

명령 결과
:core:domain:test :core:data:testDebugUnitTest BUILD SUCCESSFUL
:feature:setting:presentation:testDebugUnitTest BUILD SUCCESSFUL
:feature:timeletter:presentation:testDebugUnitTest BUILD SUCCESSFUL
:feature:mindrecord:presentation:testDebugUnitTest BUILD SUCCESSFUL
:feature:receiver:presentation:testDebugUnitTest BUILD SUCCESSFUL
:feature:home:presentation:testDebugUnitTest :feature:afternote:presentation:testDebugUnitTest BUILD SUCCESSFUL
:app:compileDebugUnitTestKotlin :app:compileDebugAndroidTestKotlin BUILD SUCCESSFUL
:app:assembleDebug (Hilt 그래프 실측 — 바인딩 누락은 여기서 «could not be resolved») BUILD SUCCESSFUL
:konsist:test BUILD SUCCESSFUL
:core:domain:ktlintCheck :core:data:ktlintCheck :feature:setting:presentation:ktlintCheck :app:ktlintCheck BUILD SUCCESSFUL

동작 변경 0 — 옮긴 코드는 본문이 동일하다. DI 는 수신자 구현이 @Singleton 하나로 고정된다는 점만 다르고(종전엔 합본 싱글턴 안의 하나 — 관찰되는 결과는 같다), UserRepositoryImpl 을 직접 요청하는 곳은 없다(internal).

🧪 CI Test Plan

{
  "androidTest": {
    "mode": "selected",
    "reason": "프로덕션 변경은 core/data·core/domain 뿐이고 Hilt 그래프는 :app:assembleDebug 로 실측했다. app androidTest 는 ReceiverSessionIsolationAndroidTest 한 파일을 실구현 조립에서 fake 기반으로 다시 써서 그 클래스의 @Test 전량(1건)을 api30 에서 돌린다. 옮긴 수신자 세션 캐시 로직은 :core:data:testDebugUnitTest 가 로그아웃·재로그인·401 폐기까지 덮는다.",
    "tests": [
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/ReceiverSessionIsolationAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.ReceiverSessionIsolationAndroidTest#recipientList_receiverListEmptiesOnSessionChange_keepsScreenAliveWithoutPreviousReceiverIdentity",
        "device": "api30"
      }
    ]
  }
}

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

  • 봐 주셨으면 하는 곳은 스코프의 자리입니다 — 상태를 가진 UserReceiverRepositoryImpl 클래스에 @Singleton, 합본 UserRepositoryImpl 은 계약 2종을 생성자 주입으로 받아 위임합니다. 첫 리비전의 «생성자에서 직접 조립» 은 테스트 편의로 남긴 것이라 걷어냈습니다(승인 뒤 커밋 — 두 번째 리비전 diff 만 보시면 됩니다).
  • fake 합본의 위임 프로퍼티가 길어졌는데, 이건 «남의 모듈 0줄» 을 지키기 위한 전환기 비용입니다. 소비자가 좁은 fake 로 옮겨갈수록 지워집니다.

1단계(#1409)에서 계약만 갈라 두고 구현과 공용 fake 는 합본으로 남아 있었다. 그 둘을
책임별로 나누되 코어 안에서 닫는다 — 소비자·DI 계약·fake 공개 API 는 그대로다.

- core:data: UserReceiverRepositoryImpl(수신자 8멤버 + receiverRefreshRevision·목록 캐시)·
  MyProfileRepositoryImpl(서버 정본 프로필 2멤버) 신설. UserRepositoryImpl 은 그 둘에
  위임하고 아직 못 내려간 계정·푸시 6멤버만 직접 갖는다(283줄 → 110줄).
- 두 구현을 Hilt 가 아니라 UserRepositoryImpl 생성자가 직접 조립한다. Hilt 가 따로 만들면
  UserRepository 로 등록한 수신자가 UserReceiverRepository 구독자의 목록을 갱신하지 못하고
  revision 이 갈린다.
- core:domain testFixtures: FakeUserReceiverRepository·FakeMyProfileRepository 신설.
  FakeUserRepository 는 그 둘에 위임하는 합본으로 남기고 생성자·프로퍼티·호출 기록 타입·
  strict 문구를 전부 보존한다.
- UserRepositoryImpl 생성자 시그니처 무변경 — app androidTest 와 feature:setting 테스트가
  Hilt 를 우회해 직접 조립한다.
@github-actions github-actions Bot added the conflict base 와 충돌 — CI 가 트리거되지 않는다. base 를 병합하면 자동으로 떨어진다 label Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

이 PR 은 develop 와 충돌 상태이고, 그래서 CI 가 돌지 않는다.

pull_request 워크플로는 base 와 head 를 합친 가상 머지 커밋(refs/pull/<N>/merge)에서
실행되는데, 충돌이면 GitHub 이 그 ref 를 만들지 못해 워크플로가 트리거조차 되지 않는다.
체크가 실패하는 것이 아니라 아예 생성되지 않으므로, 새 커밋을 올려도 no checks reported
인 채 최신 커밋이 한 번도 검증되지 않은 상태로 남는다 (#1028).

develop 를 병합해 충돌을 풀면 검증이 다시 돈다. 해소되면 이 라벨은 자동으로 떨어진다.

…-repository-split-stage2

# Conflicts:
#	core/data/src/main/java/com/afternote/core/data/repoimpl/UserRepositoryImpl.kt
… 닫는다 (#1738)

첫 리비전은 app androidTest·feature:setting 테스트가 UserRepositoryImpl(userApi, auth, errorReporter)
를 직접 조립한다는 이유로 생성자 시그니처를 지키고 두 구현을 생성자 안에서 직접 new 했다. 테스트가
프로덕션 모양을 정한 것이라 걷어낸다.

- 상태(revision·목록 캐시) 소유자인 UserReceiverRepositoryImpl 클래스에 @singleton + @Inject.
  합본을 거치든 좁은 계약을 직접 주입받든 같은 인스턴스다.
- MyProfileRepositoryImpl 은 상태가 없어 unscoped @Inject.
- UserRepositoryImpl 은 UserReceiverRepository·MyProfileRepository 를 생성자로 받아 by 위임하고
  형제 구현과 같이 internal 로 닫는다.
- CoreUserRepositoryModule 의 좁은 계약 2종 바인딩은 구현 클래스로 직접 간다.

테스트는 각자 자기 층으로 내려갔다.
- core:data 2건은 같은 모듈이라 구현 3종을 프로덕션과 같은 모양으로 조립한다.
- SettingCompletionTest.withdrawal_* 는 FakeUserRepository 로 화면 상태 전이만 본다 — 세션 정리
  «정확히 한 번·서버 호출 뒤» 는 UserRepositoryImplTest 의 deleteAccount 4건이 고정한다.
- ReceiverSessionIsolationAndroidTest 는 appTestUserRepository 로 화면 경계만 본다 — 401·로그아웃을
  빈 목록으로 낮추는 판정은 UserRepositoryImplTest 의 receiverListFlow 계열이 고정한다.
@github-actions github-actions Bot added the android-test PR head에서 Android Managed Device 계측 테스트 실행 label Sep 4, 2026
@OnlyReadOneBook
OnlyReadOneBook added this pull request to the merge queue Sep 4, 2026
Merged via the queue into develop with commit e658471 Sep 4, 2026
28 of 33 checks passed
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.

refactor(core): UserRepositoryImpl·FakeUserRepository 를 수신자·프로필 책임별 구현·fixture 로 분리 (#1282 분리)

2 participants