Skip to content

refactor(core): 수신자 입력 거절의 서버 원문을 고정 진단 문구로 바꾼다 #934

Closed
1 of 1 issue completed

Description

@Sadturtleman

작업 유형

refactor — 동작 변경 없는 구조·책임·경계 개선

주 담당 모듈

core — 코어

개요

수신자 등록·수정 실패인 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 수신자 실패 계약만 독립적으로 검증하고 닫는다.

Activity

  1. added 4 commits that reference this issue on Aug 25, 2026
  2. added
    refactor동작 변경 없이 코드 구조·책임·경계를 개선하는 작업
    area:platformCI·빌드·릴리스·저장소 운영 영역
    and removed
    enhancementNew feature or request
    on Aug 28, 2026
  3. 1hyok commented on Aug 28, 2026

    @1hyok
    Collaborator

    본문 «AfternoteAuthoringValidationKind 는 서버 코드 475 하나뿐이라 현재는 enum 값이 1개다. 압축 시 유지 여부 판단 필요» 로 보류돼 있던 판단이 제거로 확정됐습니다 — #1387 에서 근거(수신자 전 카테고리 선택 확정 #951 · BE main 에 475/애프터노트 수신자 검증 부재)와 함께 AuthoringValidation variant·kind enum 을 포함한 체인 전 구간을 걷습니다. 이 이슈의 남은 범위에서 해당 항목은 #1387 이 대체합니다.

  4. added
    pr-open이 이슈를 구현하는 PR 이 열려 있다 — base 가 기본 브랜치가 아니어도 드러낸다
    and removed
    pr-open이 이슈를 구현하는 PR 이 열려 있다 — base 가 기본 브랜치가 아니어도 드러낸다
    on Aug 30, 2026
  5. 1hyok commented on Sep 3, 2026

    @1hyok
    Collaborator

    2026-09-03 전수 재감사 — 본문 핵심 주장 4개가 이미 해소됐다

    본문은 104462fd(2026-08-24) 기준이고 그 뒤 200건 넘는 PR 이 머지됐다. e400b07a6(현재 origin/develop) 에서 전 항목을 다시 쟀다.

    측정 방법 — Throwable 을 간접 상속까지 포함해 닫힘 계산한 전수 스캔(문자열 리터럴·주석 제거 후 파싱, 파일 내 우선 해소로 동명이인 오탐 제거). 같은 스캐너를 104462fd 에 돌리면 선언 17개 / 고유 이름 16종이 나와 본문 수치와 정확히 일치한다 — 방법론이 본문과 같음을 확인한 뒤 현재 리비전을 쟀다.

    konsist/ProductionVisibilityKonsistTest.kt 의 허용목록 문자열은 선언이 아니라 제외했다. TokenReissuer.Outcome.ServerFailure · ReceiverAfternoteListError.NetworkUnavailable 은 예외 하위 타입과 이름만 같은 별개 sealed 갈래라 제외했다(실코드 확인).


    1. 선언 전수 — 17 → 22 (고유 이름 16종 → 20종)

    # 파일:줄 이름 모듈 sealed 가시성 나르는 프로퍼티
    1 CoreAuthFailure.kt:17 CoreAuthFailure core:domain 루트 public (message, cause)
    2 CoreAuthFailure.kt:25 .InvalidLoginCredentials core:domain 하위 public cause
    3 CoreAuthFailure.kt:33 .SocialLoginRejected core:domain 하위 public cause
    4 CoreAuthFailure.kt:38 .EmailAlreadyRegistered core:domain 하위 public cause
    5 CoreAuthFailure.kt:43 .EmailVerification core:domain 하위 public cause
    6 CoreAuthFailure.kt:54 .NetworkUnavailable core:domain 하위 public cause
    7 CoreAuthFailure.kt:67 .UserCancelledAuth core:domain 하위 public —
    8 ReceiverRequestRejectedException.kt:4 ReceiverRequestRejectedException core:domain 아니오 public val userMessage: String · cause
    9 TokenAuthenticator.kt:81 TokenReissueFailureException core:network 아니오 internal cause
    10 BaseResponse.kt:67 ApiException core:network 아니오 public status · code · serverMessage
    11 AfternoteFailure.kt:12 AfternoteFailure feature/afternote/domain 루트 public (message, cause)
    12 AfternoteFailure.kt:25 .MediaSave feature/afternote/domain 하위 public val media: MediaKind · cause
    13 AfternoteFailure.kt:38 .NetworkUnavailable feature/afternote/domain 하위 public cause
    14 DeliveryNotReadyException.kt:9 DeliveryNotReadyException feature/mindrecord/domain 아니오 public cause
    15 ReceiverAuthDto.kt:155 VerificationStatusMappingFailure feature/receiver/data 아니오 private —
    16 ReceiverAfternoteListItemDtoToDomain.kt:17 ReceiverListDecodingFailure feature/receiver/data 아니오 internal —
    17 ReceiverAfternoteListItemDtoToDomain.kt:25 ReceiverListMappingFailure feature/receiver/data 아니오 internal —
    18 ReceiverFailure.kt:20 ReceiverFailure feature/receiver/domain 루트 public (message, cause)
    19 ReceiverFailure.kt:34 .UserRejection feature/receiver/domain 하위 public val reason: ReceiverRejectionReason? · cause
    20 ReceiverFailure.kt:50 .UnexpectedServerFailure feature/receiver/domain 하위 public cause
    21 ReceiverFailure.kt:61 .NetworkUnavailable feature/receiver/domain 하위 public cause
    22 ReceiverFailure.kt:77 .DeliveryConditionNotMet feature/receiver/domain 하위 public cause

    core: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 · TimeLetterServerRejectionException


    2. 사용처 분류 (main / test·androidTest 분리)

    throw·생성·is/as 분기를 소스셋별로 셌다. 선언 파일 자체는 제외했다.

    예외 main 생성·throw main 분기(is/as) test·androidTest
    CoreAuthFailure (루트) — 21 17
    .InvalidLoginCredentials 1 3 6
    .SocialLoginRejected 1 2 2
    .EmailAlreadyRegistered 1 2 —
    .EmailVerification 1 4 1
    .NetworkUnavailable 2 2 7
    .UserCancelledAuth 6 6 1
    ReceiverRequestRejectedException 1 (throw) 1 4
    TokenReissueFailureException 1 (throw, 같은 파일) — 2
    ApiException 1 11 (catch 5 포함) 44
    AfternoteFailure (루트) — 3 8
    .MediaSave 2 1 6
    .NetworkUnavailable 1 1 2
    DeliveryNotReadyException 1 2 5
    VerificationStatusMappingFailure 1 (throw, 같은 파일) — —
    ReceiverListDecodingFailure 1 (같은 파일) — 3
    ReceiverListMappingFailure 1 (같은 파일) — 3
    ReceiverFailure (루트) — 15 35
    .UserRejection 2 3 13
    .UnexpectedServerFailure 2 3 6
    .NetworkUnavailable 1 3 3
    .DeliveryConditionNotMet 1 3 7

    「test 에서만 쓰이는 예외」 — 0건.
    「던지기만 하고 아무도 안 가르는 예외」 — 3건, 전부 텔레메트리 error_type 마커다: VerificationStatusMappingFailure · ReceiverListDecodingFailure · ReceiverListMappingFailure. ErrorReporter.recordFailure 에 넘겨 타입 이름만 속성으로 남기는 게 목적이라 «가르는 곳 없음» 이 설계대로다. 테스트가 error_type 속성값으로 단언한다 — 지우면 안 된다.
    「어디서도 안 쓰이는 예외」 — 0건. 본문 문제 4(죽은 예외)는 이미 해소됐고, 새로 생긴 죽은 예외도 없다.


    3. sealed 이득 실측 — 「when 을 아무도 안 쓴다」는 더 이상 참이 아니다

    sealed 루트를 subject 로 하고 else 없이 전 분기를 열거한 exhaustive when 이 main 에 6개 있다.

    위치 루트 분기 else
    OnboardingFailureMessage.kt:38 CoreAuthFailure 6/6 없음
    LoginViewModel.kt:137 CoreAuthFailure 6/6 + null 없음
    AfternoteEditorViewModel.kt:754 AfternoteFailure 2/2 없음
    AfternoteFailureReporting.kt:152 ReceiverFailure 4/4 없음
    ReceiverAfternoteListError.kt:37 ReceiverFailure 4/4 없음
    ReceiverErrorDisplay.kt:16 ReceiverFailure 4/4 없음

    본문이 처방한 「소비처를 루트로 좁힌다」가 그대로 구현돼 있다 — (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 건이다.

    파일 소스셋 결합
    core/domain/…/error/ReceiverRequestRejectedException.kt main 선언
    core/data/…/repoimpl/UserRepositoryImpl.kt:293 main throw 1
    core/data/src/test/…/ReceiverRequestFailureTest.kt:70 test .userMessage 단언 1

    (feature/setting/presentation/…/ReceiverFailureMessage.kt 는 KDoc 링크 1줄, ReceiverRegisterViewModelTest.kt:83 은 위치 인자 생성 1회 — 생성자 시그니처를 안 바꾸면 무변경.)


    6. 낡은 주장 — 본문에서 정정이 필요한 항목

    본문 주장 현재 실측
    「예외 17개(고유 16종)」 22개(고유 20종). 단 본문의 16종 중 14종은 소멸했고 늘어난 건 sealed 하위 타입과 신규 data 마커다
    「문제 1 — sealed 인데 when 을 아무도 안 쓴다」 거짓. exhaustive when 6개(위 3항)
    「문제 2 — 계층이 상위 페이로드를 복사한다」 해소. ReceiverServerRejectionException 계열 소멸
    「문제 3 — UserCancelledAuthException 2벌」 해소. CoreAuthFailure.UserCancelledAuth 한 벌
    「문제 4 — 죽은 예외」(#880 이관) 해소. 죽은 예외 0건
    「1단계 — core:domain 5개를 sealed 루트로」 완료 (85033b0a2, 2026-08-24). 소비처 좁히기·미처리 문구 결정까지 끝
    「2단계 — UserCancelledAuthException 통합」 완료
    「4-A — 수신자 예외를 ReceiverFailure 로」 완료 (a83909e78 외, #1053 계열)
    「4-B — AfternoteFailure 로 압축」 완료 (d19ddc42d 외). MediaSave(media, cause) 도입, cause 보존·data class 미사용 지침 지켜짐
    「변경 면적 10파일(androidTest 2)」 0파일. 대상 파일이 전부 소멸. #949 컴파일 게이트 리스크도 소멸
    「#767 의 TimeLetterServerRejectionException 을 같은 규약에 태울지 판단」 대상 소멸. 그 클래스는 origin/develop 에 없다
    「AfternoteAuthoringValidationKind 유지 여부 판단」(2026-08-28 코멘트) 완료. #1387 이 AuthoringValidation variant·kind enum 을 걷은 게 83aa0a68c(2026-08-29) 로 develop 에 반영됨
    「MediaKind 는 아직 레포에 없다 — #938 을 먼저 태운다」 해소. feature/afternote/domain/…/repository/author/MediaKind.kt 존재

    남은 범위 — 실질 2건

    (A) ReceiverRequestRejectedException 을 규약에 맞춘다

    이 예외만 두 규약을 어긴다.

    1. val userMessage 를 main 에서 읽는 곳이 0건이다. 전체 리비전에서 .userMessage 읽기는 테스트 1곳(ReceiverRequestFailureTest.kt:70)뿐이고, main 의 유일한 등장은 ReceiverFailureMessage.kt:9 의 KDoc 링크다. 유일한 소비처는 타입만 보고 로컬 리소스를 고른다.
    2. 이름이 규약의 반대를 광고한다. CoreAuthFailure·ReceiverFailure 두 루트의 KDoc 은 «서버 문구는 화면에 싣지 않는다(BE#92)» 를 명시하고, ReceiverFailureMessage.kt 는 그 경고에 KDoc 3줄을 쓴다. 그런데 userMessage 라는 public 접근자가 core:domain 에 열려 있어 어느 feature 모듈이든 그걸 그대로 화면에 실을 수 있다.
    3. KDoc 이 댄 존치 근거가 사실과 다르다. 「리포팅·로그 용도로만 예외에 남긴다」고 적혀 있지만, 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건(본문 작성 시점보다 늘었다)의 변경 파일을 전수 대조했다.

    • (A) 중 생성자 시그니처 변경은 지금 못 한다 — 유일한 생산 지점 UserRepositoryImpl.kt:293 의 mapReceiverRequestFailure 를 refactor(core): UserRepository 구현·fixture 를 수신자·프로필 책임으로 분리 (#1738) #1595 가 통째로 UserReceiverRepositoryImpl.kt 로 옮긴다(APPROVED·MERGEABLE). 그 줄을 지금 건드리면 정면 충돌한다.
    • 반면 public 접근자 val 제거는 생성자 호출부를 건드리지 않는다(두 호출부 모두 위치 인자). 관련 3파일 — ReceiverRequestRejectedException.kt · ReceiverRequestFailureTest.kt · ReceiverFailureMessage.kt — 은 열린 PR 66건 중 어느 것도 건드리지 않는다.

    그래서 (A) 를 둘로 갈라 접근자 제거분만 분리 이슈로 세워 지금 닫는다. 서버 문구를 진단 문구로 바꾸는 나머지(생성자 시그니처)는 #1595 머지 뒤 이 이슈에 남긴다.

    본 이슈는 총괄로 열어 둔다.

  6. changed the title [-]예외 계층 정리 — 모듈별 sealed 루트로 압축, 미사용 하위 타입·중복·죽은 예외 제거[/-] [+]refactor(core): 수신자 입력 거절의 서버 원문을 고정 진단 문구로 바꾼다[/+] on Sep 13, 2026
  7. added
    area:core공용 코어·네트워크 기능 영역
    and removed
    area:platformCI·빌드·릴리스·저장소 운영 영역
    on Sep 13, 2026
  8. added
    pr-open이 이슈를 구현하는 PR 이 열려 있다 — base 가 기본 브랜치가 아니어도 드러낸다
    on Sep 13, 2026
  9. removed
    pr-open이 이슈를 구현하는 PR 이 열려 있다 — base 가 기본 브랜치가 아니어도 드러낸다
    on Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:core공용 코어·네트워크 기능 영역refactor동작 변경 없이 코드 구조·책임·경계를 개선하는 작업

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions