You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
refactor(core): 수신자 입력 거절의 서버 원문을 고정 진단 문구로 바꾼다 #934
수신자 등록·수정 실패인 ReceiverRequestRejectedException이 서버 원문을 Exception.message에 복사하는 마지막 도메인 계약을 정리한다. val userMessage 접근자 제거는 #1855로 이미 완료했고, 생성 지점 분리는 #1595가 병합되어 더 이상 선행 대기가 아니다.
현재 확정 계약:
수신자 관리의 입력 거절은 인증·계정 오류인 CoreAuthFailure와 별개다. 소비처가 구분하는 사유는 하나이므로 추가 sealed 계층이나 새로운 실패 사유를 만들지 않는다.
ReceiverRequestRejectedException(cause)는 고정 진단 문구를 사용하고 원본 예외는 동일한 cause로 보존한다. 서버 원문을 별도 도메인 필드나 message에 복사하지 않는다.
UserReceiverRepositoryImpl의 현재 번역 조건(HTTP 400/409이면서 비어 있지 않은 서버 사유)은 유지한다. 다른 HTTP 오류·사유가 없는 응답·코루틴 취소는 원본 그대로 전파한다.
setting 화면은 기존처럼 오류 타입만 보고 로컬 리소스를 표시한다. 공개 범위는 실제 프로덕션 소비처를 따른다. 번역 helper는 실제 소비가 같은 파일 안에만 있으므로 private로 유지한다.
완료 조건:
도메인 생성자의 서버 문구 인자를 제거하고 고정 message 및 원본 cause 계약을 적용한다.
data 생성 지점과 setting의 기존 테스트를 갱신하고 화면 표시 계약이 유지됨을 검증한다.
400/409 거절·빈 사유·다른 HTTP 오류·취소 전파의 공개 Repository 경유 회귀를 통과한다.
core 및 setting 관련 테스트·컴파일·ktlint와 전체 PR diff의 담당 경계 검사를 통과한다.
이미 완료되거나 독립 분리된 범위
core 인증 sealed 루트·취소 예외 통합, receiver/afternote 루트 압축과 죽은 예외 제거는 완료됐다. 상세 근거와 과거 인벤토리는 2026-09-03 전수 재감사를 보존한다.
본문 «AfternoteAuthoringValidationKind 는 서버 코드 475 하나뿐이라 현재는 enum 값이 1개다. 압축 시 유지 여부 판단 필요» 로 보류돼 있던 판단이 제거로 확정됐습니다 — #1387 에서 근거(수신자 전 카테고리 선택 확정 #951 · BE main 에 475/애프터노트 수신자 검증 부재)와 함께 AuthoringValidation variant·kind enum 을 포함한 체인 전 구간을 걷습니다. 이 이슈의 남은 범위에서 해당 항목은 #1387 이 대체합니다.
본문은 104462fd(2026-08-24) 기준이고 그 뒤 200건 넘는 PR 이 머지됐다. e400b07a6(현재 origin/develop) 에서 전 항목을 다시 쟀다.
측정 방법 — Throwable 을 간접 상속까지 포함해 닫힘 계산한 전수 스캔(문자열 리터럴·주석 제거 후 파싱, 파일 내 우선 해소로 동명이인 오탐 제거). 같은 스캐너를 104462fd 에 돌리면 선언 17개 / 고유 이름 16종이 나와 본문 수치와 정확히 일치한다 — 방법론이 본문과 같음을 확인한 뒤 현재 리비전을 쟀다.
konsist/ProductionVisibilityKonsistTest.kt 의 허용목록 문자열은 선언이 아니라 제외했다. TokenReissuer.Outcome.ServerFailure · ReceiverAfternoteListError.NetworkUnavailable 은 예외 하위 타입과 이름만 같은 별개 sealed 갈래라 제외했다(실코드 확인).
「test 에서만 쓰이는 예외」 — 0건. 「던지기만 하고 아무도 안 가르는 예외」 — 3건, 전부 텔레메트리 error_type 마커다: VerificationStatusMappingFailure · ReceiverListDecodingFailure · ReceiverListMappingFailure. ErrorReporter.recordFailure 에 넘겨 타입 이름만 속성으로 남기는 게 목적이라 «가르는 곳 없음» 이 설계대로다. 테스트가 error_type 속성값으로 단언한다 — 지우면 안 된다. 「어디서도 안 쓰이는 예외」 — 0건. 본문 문제 4(죽은 예외)는 이미 해소됐고, 새로 생긴 죽은 예외도 없다.
3. sealed 이득 실측 — 「when 을 아무도 안 쓴다」는 더 이상 참이 아니다
sealed 루트를 subject 로 하고 else 없이 전 분기를 열거한 exhaustive when 이 main 에 6개 있다.
본문이 처방한 「소비처를 루트로 좁힌다」가 그대로 구현돼 있다 — (this as? CoreAuthFailure)?.… · when (exception as? CoreAuthFailure) · private fun ReceiverFailure.displayUiTextOrNull(). 본문이 지적한 두 소비처(OnboardingFailureMessage 3/5 · LoginViewModel 2/5)는 둘 다 6/6 으로 채워졌고, 미처리 케이스 문구 결정도 끝났다(주석에 근거까지 남아 있다).
4. 중복·복사 페이로드
UserCancelledAuthException 2벌 — 해소됐다. 두 파일 다 origin/develop 에 없고, CoreAuthFailure.UserCancelledAuth 한 벌로 통합됐다. onboarding 쪽 KDoc 의 존재 이유(코루틴 CancellationException 구분)도 함께 옮겨졌다.
상위 페이로드 복사 — 해소됐다.ReceiverServerRejectionException 계열이 status·serverCode·serverMessage 를 하위 3벌에 복사해 나르던 구조가 사라졌다. 지금은 ReceiverFailureTranslation 이 그 값을 data 계층에서만 해석하고 도메인 타입은 아무것도 운반하지 않는다.
남은 복사 페이로드 1건 — ReceiverRequestRejectedException 의 val userMessage: String. 아래 6 참고.
NetworkUnavailable 이 세 루트에 각각 있는 건 중복이 아니다 — presentation 이 core:network 에 의존하지 않게 하려는 모듈별 어휘이고, 세 KDoc 이 서로를 참조하며 같은 규약임을 명시한다.
DeliveryNotReadyException(mindrecord) 과 ReceiverFailure.DeliveryConditionNotMet(receiver) 은 의미가 겹치지만 합칠 수 없다 — 합치려면 mindrecord 가 feature/receiver/domain 에 의존해야 하고, 그건 본문 전제(모듈별 루트)가 금지하는 방향이다.
5. 변경 면적 재측정
본문이 4-A 용으로 잡은 10파일(androidTest 2 포함)은 전부 소멸했다. 그 파일들이 참조하던 예외가 없어졌기 때문이다. AppAndReceiverCompletionAndroidTest · ReceiverVerificationAndroidTest 에는 Receiver*Exception 생성이 남아 있지 않다.
모듈별 sealed 루트 압축 작업의 잔여 면적은 3파일이고 전부 ReceiverRequestRejectedException 건이다.
해소.feature/afternote/domain/…/repository/author/MediaKind.kt 존재
남은 범위 — 실질 2건
(A) ReceiverRequestRejectedException 을 규약에 맞춘다
이 예외만 두 규약을 어긴다.
val userMessage 를 main 에서 읽는 곳이 0건이다. 전체 리비전에서 .userMessage 읽기는 테스트 1곳(ReceiverRequestFailureTest.kt:70)뿐이고, main 의 유일한 등장은 ReceiverFailureMessage.kt:9 의 KDoc 링크다. 유일한 소비처는 타입만 보고 로컬 리소스를 고른다.
이름이 규약의 반대를 광고한다.CoreAuthFailure·ReceiverFailure 두 루트의 KDoc 은 «서버 문구는 화면에 싣지 않는다(BE#92)» 를 명시하고, ReceiverFailureMessage.kt 는 그 경고에 KDoc 3줄을 쓴다. 그런데 userMessage 라는 public 접근자가 core:domain 에 열려 있어 어느 feature 모듈이든 그걸 그대로 화면에 실을 수 있다.
KDoc 이 댄 존치 근거가 사실과 다르다. 「리포팅·로그 용도로만 예외에 남긴다」고 적혀 있지만, ErrorReporter.recordFailure 는 redact(throwable) 로 message 를 통째로 버리고 타입 이름만 남긴 RedactedFailure 를 올린다(cause 도 잇지 않는다). 서버 문구는 리포팅 백엔드에 닿지 않는다.
(B) ApiException → 도메인 번역 지점
수신자는 ReceiverFailureTranslation.kt 로 전용 파일이 서 있고 규약도 문서화됐다. 반면 core 인증은 AuthRepositoryImpl.kt:197 · AccountRepositoryImpl.kt:116 에, mindrecord 는 MindRecordReceiverRepositoryImpl.kt:51 에 인라인이다. 셋 다 동작은 맞지만 「매퍼로 명시 정리」는 아직이다. 면적이 넓고 열린 PR 과 겹쳐 지금 착수 대상이 아니다.
반면 public 접근자 val 제거는 생성자 호출부를 건드리지 않는다(두 호출부 모두 위치 인자). 관련 3파일 — ReceiverRequestRejectedException.kt · ReceiverRequestFailureTest.kt · ReceiverFailureMessage.kt — 은 열린 PR 66건 중 어느 것도 건드리지 않는다.
그래서 (A) 를 둘로 갈라 접근자 제거분만 분리 이슈로 세워 지금 닫는다. 서버 문구를 진단 문구로 바꾸는 나머지(생성자 시그니처)는 #1595 머지 뒤 이 이슈에 남긴다.
Activity
Merge pull request #1051 from Afternote/refactor/934-1-core-auth
Merge pull request #1056 from Afternote/refactor/934-3-afternote
Merge pull request #1054 from Afternote/refactor/934-2-receiver
merge: develop 병합 — 예외 계층 정리(#934) 반영
Merge develop into refactor/1085-core-model-relocation
1hyok commentedon Aug 28, 2026
본문 «
AfternoteAuthoringValidationKind는 서버 코드 475 하나뿐이라 현재는 enum 값이 1개다. 압축 시 유지 여부 판단 필요» 로 보류돼 있던 판단이 제거로 확정됐습니다 — #1387 에서 근거(수신자 전 카테고리 선택 확정 #951 · BE main 에 475/애프터노트 수신자 검증 부재)와 함께AuthoringValidationvariant·kind enum 을 포함한 체인 전 구간을 걷습니다. 이 이슈의 남은 범위에서 해당 항목은 #1387 이 대체합니다.1hyok commentedon Sep 3, 2026
2026-09-03 전수 재감사 — 본문 핵심 주장 4개가 이미 해소됐다
본문은
104462fd(2026-08-24) 기준이고 그 뒤 200건 넘는 PR 이 머지됐다.e400b07a6(현재origin/develop) 에서 전 항목을 다시 쟀다.측정 방법 —
Throwable을 간접 상속까지 포함해 닫힘 계산한 전수 스캔(문자열 리터럴·주석 제거 후 파싱, 파일 내 우선 해소로 동명이인 오탐 제거). 같은 스캐너를104462fd에 돌리면 선언 17개 / 고유 이름 16종이 나와 본문 수치와 정확히 일치한다 — 방법론이 본문과 같음을 확인한 뒤 현재 리비전을 쟀다.1. 선언 전수 — 17 → 22 (고유 이름 16종 → 20종)
CoreAuthFailure.kt:17CoreAuthFailureCoreAuthFailure.kt:25.InvalidLoginCredentialsCoreAuthFailure.kt:33.SocialLoginRejectedCoreAuthFailure.kt:38.EmailAlreadyRegisteredCoreAuthFailure.kt:43.EmailVerificationCoreAuthFailure.kt:54.NetworkUnavailableCoreAuthFailure.kt:67.UserCancelledAuthReceiverRequestRejectedException.kt:4ReceiverRequestRejectedExceptionval userMessage: String· causeTokenAuthenticator.kt:81TokenReissueFailureExceptionBaseResponse.kt:67ApiExceptionAfternoteFailure.kt:12AfternoteFailureAfternoteFailure.kt:25.MediaSaveval media: MediaKind· causeAfternoteFailure.kt:38.NetworkUnavailableDeliveryNotReadyException.kt:9DeliveryNotReadyExceptionReceiverAuthDto.kt:155VerificationStatusMappingFailureReceiverAfternoteListItemDtoToDomain.kt:17ReceiverListDecodingFailureReceiverAfternoteListItemDtoToDomain.kt:25ReceiverListMappingFailureReceiverFailure.kt:20ReceiverFailureReceiverFailure.kt:34.UserRejectionval reason: ReceiverRejectionReason?· causeReceiverFailure.kt:50.UnexpectedServerFailureReceiverFailure.kt:61.NetworkUnavailableReceiverFailure.kt:77.DeliveryConditionNotMetcore:common의RedactedFailure(private, 리포팅 redact 사본) · 테스트 전용ErrorReporterPolicyTest.ServerFailure는 본문 17 과 같은 기준으로 제외했다. 포함하면 19 → 24 다.본문의 예외 16종 중 이름이 남은 건
ApiException·TokenReissueFailureException둘뿐이다. 나머지 14종은 전부 사라졌다 —git show origin/develop:<경로>로 재확인한 부재 목록:InvalidLoginCredentialsException·EmailAlreadyRegisteredException·EmailVerificationException·SocialLoginRejectedException·NetworkUnavailableException·UserCancelledAuthException(2벌) ·ReceiverServerRejectionException·ReceiverMasterKeyException·ReceiverEmailAuthException·ReceiverDeliveryVerificationException·MemorialVideoSaveException·MemorialPhotoSaveException·AfternoteValidationException·AfternoteAuthoringValidationException·TimeLetterServerRejectionException2. 사용처 분류 (main / test·androidTest 분리)
throw·생성·is/as분기를 소스셋별로 셌다. 선언 파일 자체는 제외했다.is/as)CoreAuthFailure(루트).InvalidLoginCredentials.SocialLoginRejected.EmailAlreadyRegistered.EmailVerification.NetworkUnavailable.UserCancelledAuthReceiverRequestRejectedExceptionTokenReissueFailureExceptionApiExceptionAfternoteFailure(루트).MediaSave.NetworkUnavailableDeliveryNotReadyExceptionVerificationStatusMappingFailureReceiverListDecodingFailureReceiverListMappingFailureReceiverFailure(루트).UserRejection.UnexpectedServerFailure.NetworkUnavailable.DeliveryConditionNotMet「test 에서만 쓰이는 예외」 — 0건.
「던지기만 하고 아무도 안 가르는 예외」 — 3건, 전부 텔레메트리
error_type마커다:VerificationStatusMappingFailure·ReceiverListDecodingFailure·ReceiverListMappingFailure.ErrorReporter.recordFailure에 넘겨 타입 이름만 속성으로 남기는 게 목적이라 «가르는 곳 없음» 이 설계대로다. 테스트가error_type속성값으로 단언한다 — 지우면 안 된다.「어디서도 안 쓰이는 예외」 — 0건. 본문 문제 4(죽은 예외)는 이미 해소됐고, 새로 생긴 죽은 예외도 없다.
3. sealed 이득 실측 — 「
when을 아무도 안 쓴다」는 더 이상 참이 아니다sealed 루트를 subject 로 하고
else없이 전 분기를 열거한 exhaustivewhen이 main 에 6개 있다.elseOnboardingFailureMessage.kt:38CoreAuthFailureLoginViewModel.kt:137CoreAuthFailurenullAfternoteEditorViewModel.kt:754AfternoteFailureAfternoteFailureReporting.kt:152ReceiverFailureReceiverAfternoteListError.kt:37ReceiverFailureReceiverErrorDisplay.kt:16ReceiverFailure본문이 처방한 「소비처를 루트로 좁힌다」가 그대로 구현돼 있다 —
(this as? CoreAuthFailure)?.…·when (exception as? CoreAuthFailure)·private fun ReceiverFailure.displayUiTextOrNull(). 본문이 지적한 두 소비처(OnboardingFailureMessage3/5 ·LoginViewModel2/5)는 둘 다 6/6 으로 채워졌고, 미처리 케이스 문구 결정도 끝났다(주석에 근거까지 남아 있다).4. 중복·복사 페이로드
UserCancelledAuthException2벌 — 해소됐다. 두 파일 다origin/develop에 없고,CoreAuthFailure.UserCancelledAuth한 벌로 통합됐다. onboarding 쪽 KDoc 의 존재 이유(코루틴CancellationException구분)도 함께 옮겨졌다.ReceiverServerRejectionException계열이status·serverCode·serverMessage를 하위 3벌에 복사해 나르던 구조가 사라졌다. 지금은ReceiverFailureTranslation이 그 값을 data 계층에서만 해석하고 도메인 타입은 아무것도 운반하지 않는다.ReceiverRequestRejectedException의val userMessage: String. 아래 6 참고.NetworkUnavailable이 세 루트에 각각 있는 건 중복이 아니다 — presentation 이core:network에 의존하지 않게 하려는 모듈별 어휘이고, 세 KDoc 이 서로를 참조하며 같은 규약임을 명시한다.DeliveryNotReadyException(mindrecord) 과ReceiverFailure.DeliveryConditionNotMet(receiver) 은 의미가 겹치지만 합칠 수 없다 — 합치려면 mindrecord 가feature/receiver/domain에 의존해야 하고, 그건 본문 전제(모듈별 루트)가 금지하는 방향이다.5. 변경 면적 재측정
본문이 4-A 용으로 잡은 10파일(androidTest 2 포함)은 전부 소멸했다. 그 파일들이 참조하던 예외가 없어졌기 때문이다.
AppAndReceiverCompletionAndroidTest·ReceiverVerificationAndroidTest에는Receiver*Exception생성이 남아 있지 않다.모듈별 sealed 루트 압축 작업의 잔여 면적은 3파일이고 전부
ReceiverRequestRejectedException건이다.core/domain/…/error/ReceiverRequestRejectedException.ktcore/data/…/repoimpl/UserRepositoryImpl.kt:293throw1core/data/src/test/…/ReceiverRequestFailureTest.kt:70.userMessage단언 1(
feature/setting/presentation/…/ReceiverFailureMessage.kt는 KDoc 링크 1줄,ReceiverRegisterViewModelTest.kt:83은 위치 인자 생성 1회 — 생성자 시그니처를 안 바꾸면 무변경.)6. 낡은 주장 — 본문에서 정정이 필요한 항목
when을 아무도 안 쓴다」when6개(위 3항)ReceiverServerRejectionException계열 소멸UserCancelledAuthException2벌」CoreAuthFailure.UserCancelledAuth한 벌85033b0a2, 2026-08-24). 소비처 좁히기·미처리 문구 결정까지 끝UserCancelledAuthException통합」ReceiverFailure로」a83909e78외, #1053 계열)AfternoteFailure로 압축」d19ddc42d외).MediaSave(media, cause)도입,cause보존·data class미사용 지침 지켜짐TimeLetterServerRejectionException을 같은 규약에 태울지 판단」origin/develop에 없다AfternoteAuthoringValidationKind유지 여부 판단」(2026-08-28 코멘트)AuthoringValidationvariant·kind enum 을 걷은 게83aa0a68c(2026-08-29) 로 develop 에 반영됨MediaKind는 아직 레포에 없다 — #938 을 먼저 태운다」feature/afternote/domain/…/repository/author/MediaKind.kt존재남은 범위 — 실질 2건
(A)
ReceiverRequestRejectedException을 규약에 맞춘다이 예외만 두 규약을 어긴다.
val userMessage를 main 에서 읽는 곳이 0건이다. 전체 리비전에서.userMessage읽기는 테스트 1곳(ReceiverRequestFailureTest.kt:70)뿐이고, main 의 유일한 등장은ReceiverFailureMessage.kt:9의 KDoc 링크다. 유일한 소비처는 타입만 보고 로컬 리소스를 고른다.CoreAuthFailure·ReceiverFailure두 루트의 KDoc 은 «서버 문구는 화면에 싣지 않는다(BE#92)» 를 명시하고,ReceiverFailureMessage.kt는 그 경고에 KDoc 3줄을 쓴다. 그런데userMessage라는 public 접근자가 core:domain 에 열려 있어 어느 feature 모듈이든 그걸 그대로 화면에 실을 수 있다.ErrorReporter.recordFailure는redact(throwable)로 message 를 통째로 버리고 타입 이름만 남긴RedactedFailure를 올린다(cause 도 잇지 않는다). 서버 문구는 리포팅 백엔드에 닿지 않는다.(B)
ApiException→ 도메인 번역 지점수신자는
ReceiverFailureTranslation.kt로 전용 파일이 서 있고 규약도 문서화됐다. 반면 core 인증은AuthRepositoryImpl.kt:197·AccountRepositoryImpl.kt:116에, mindrecord 는MindRecordReceiverRepositoryImpl.kt:51에 인라인이다. 셋 다 동작은 맞지만 「매퍼로 명시 정리」는 아직이다. 면적이 넓고 열린 PR 과 겹쳐 지금 착수 대상이 아니다.지금 닫을 수 있는 몫
열린 PR 66건(본문 작성 시점보다 늘었다)의 변경 파일을 전수 대조했다.
UserRepositoryImpl.kt:293의mapReceiverRequestFailure를 refactor(core): UserRepository 구현·fixture 를 수신자·프로필 책임으로 분리 (#1738) #1595 가 통째로UserReceiverRepositoryImpl.kt로 옮긴다(APPROVED·MERGEABLE). 그 줄을 지금 건드리면 정면 충돌한다.val제거는 생성자 호출부를 건드리지 않는다(두 호출부 모두 위치 인자). 관련 3파일 —ReceiverRequestRejectedException.kt·ReceiverRequestFailureTest.kt·ReceiverFailureMessage.kt— 은 열린 PR 66건 중 어느 것도 건드리지 않는다.그래서 (A) 를 둘로 갈라 접근자 제거분만 분리 이슈로 세워 지금 닫는다. 서버 문구를 진단 문구로 바꾸는 나머지(생성자 시그니처)는 #1595 머지 뒤 이 이슈에 남긴다.
본 이슈는 총괄로 열어 둔다.
[-]예외 계층 정리 — 모듈별 sealed 루트로 압축, 미사용 하위 타입·중복·죽은 예외 제거[/-][+]refactor(core): 수신자 입력 거절의 서버 원문을 고정 진단 문구로 바꾼다[/+]