Skip to content

refactor(core): 전역 coerceInputValues 를 걷어낸다 (#1494) - #1495

Merged
OnlyReadOneBook merged 3 commits into
developfrom
refactor/1494-drop-coerce-input-values
Aug 30, 2026
Merged

OnlyReadOneBook merged 3 commits into
developfrom
refactor/1494-drop-coerce-input-values

Conversation

@OnlyReadOneBook

@OnlyReadOneBook OnlyReadOneBook commented Aug 29, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1494

무엇을 걷어냈나

Json {
    ignoreUnknownKeys = true
-   coerceInputValues = true
}

coerceInputValues 는 non-null 프로퍼티에 null 이 와도 기본값이 있으면 조용히 그 값으로 바꾼다. 전 앱의 모든 응답 디코딩에 걸리고, 치환됐다는 기록은 어디에도 남지 않는다. #1010 이 세운 기준(«제외하더라도 세어서 보고», ErrorReporter)과 방향이 반대다.

ignoreUnknownKeys 는 유지한다 — 서버의 필드 추가는 정상적인 진화이고, 선언하지 않은 키는 어차피 못 쓴다.

이슈 본문의 「6곳」은 실제로 1곳이었다

이슈를 쓸 때 non-null + 기본값 프로퍼티 6곳을 적었는데, 실코드를 열어 보니 5곳이 요청 DTO 였다. 요청 DTO 는 인코딩만 하므로 coerceInputValues 와 무관하다.

프로퍼티 실제
AfternoteCreateGalleryRequestDto.category 요청 — 무관
AfternoteCreatePlaylistRequestDto.category · .receivers 요청 — 무관
AfternoteCreateAccountRequestDto.receivers 요청 — 무관
AfternotePlaylistRequestDto.songs 요청 — 무관
ReceivedTimeLetterDto.blocks 응답 — null 치환 축의 유일한 실제 대상

:konsist:test 의 ResponseDtoContractKonsistTest 로 교차 확인했다. 허용목록 14건 중 13건이 이미 해소됐고 남은 실제 위반은 이 1건이다.

그 1건이 안전한 근거 — BE 실코드

ReceivedTimeLetterResponse.java 가 미공개 구간에 빈 목록을 채운다.

List<TimeLetterBlockResponse> blockResponses = isAvailable
        ? timeLetter.getBlocks().stream()...toList()
        : List.of();

서버는 blocks 를 null 로 보내지 않는다. 그래서 플래그를 걷어도 파싱이 깨지지 않는다. 이 기본값이 실제로 가리는 것은 «키 자체가 빠지는» 경우이고, 그건 #790 이 추적한다(ci-expected-failures.json 에 의도된 실패로 등록됨). 그래서 이 PR 은 ReceivedTimeLetterDto 에 손대지 않는다.

두 번째 축 — unknown enum (리뷰 지적 반영, 실측 정정)

coerceInputValues 는 「non-null + 기본값에 null」 말고 unknown enum 값도 접는다. 리뷰에서 이 축이 0건으로 보인다는 지적이 있어 전수 재조사했고, 0건이 아니라 1건이다.

응답 DTO 의 enum 타입 프로퍼티를 전수(@Serializable enum 9종 × 사용처)로 훑으면, 기본값을 가진 자리는 DeliveryConditionItemDto.inactivityPeriod: InactivityPeriodDto? = null 한 곳이다(응답 DTO). nullable + 기본값이라 coerce 대상이 된다.

동작 차이는 실측했다.

payload: {"inactivityPeriod":"NINETY_YEARS", ...}
coerceInputValues = true  → Success(null)      // 알 수 없는 값이 조용히 null 로 접힘
제거 후                    → SerializationException: InactivityPeriodDto does not contain
                             element with name 'NINETY_YEARS' at path $.inactivityPeriod

지금 당장의 위험은 0이다. FE InactivityPeriodDto 와 BE InactivityPeriod.java 의 값 집합이 THREE_MONTHS·SIX_MONTHS·ONE_YEAR 로 일치한다. 이 변경이 발화하는 것은 BE 가 값을 새로 추가했을 때이고, 그때 조용히 null 이 되는 대신 터지는 것이 이 PR 이 세우려는 실패 방향 그대로다. 다음 사람이 다시 안 찾도록 조사 축으로 남긴다.

함께 정리한 것

  • konsist 허용목록 13건 삭제. 이미 해소된 항목이 남아 있으면 다음 위반이 그 자리에 숨는다 — 테스트가 경고로 알려 주던 것이다. 클래스 KDoc 의 coerceInputValues 설명도 현재 상태로 고쳤다.
  • 계약 테스트 15곳의 coerceInputValues = true 제거 (리뷰 blocker). 아래 별도 절.
  • AuthDtoSocialLoginContractTest 의 테스트 1건 이름 정정. 「coerceInputValues 로 흡수되지 않고」 가 사라진 설정을 가리키게 됐다. 단언 내용은 그대로다.

리뷰 blocker 반영 — 테스트가 프로덕션보다 관대했다

프로덕션에서 걷어낸 설정을 계약 테스트가 그대로 켜 두고 있어, 「프로덕션과 같은 Json 설정에서 계약이 지켜지는가」를 보는 테스트가 더 관대한 설정에서 통과하고 있었다. 이 PR 이 만든 드리프트라 이 PR 에서 닫는다.

  • 15개 파일의 coerceInputValues = true 제거 — 리뷰 시점 14곳에, develop 병합으로 들어온 ReceiverAfternotePagingSourceTest 1곳이 더해졌다.
  • 주석 7곳 정정. 「provideJson 과 동일 (ignoreUnknownKeys + coerceInputValues)」 → (ignoreUnknownKeys). 걷어낸 지금 테스트 설정은 provideJson 과 실제로 동일해졌다.
  • ReceivedAfternoteDetailTitleContractTest 의 KDoc·테스트명이 사라진 기제를 가리키고 있어 함께 정정했다.

걷어낸 뒤 새로 깨지는 테스트는 없다. :feature:timeletter:data 의 4건은 제거 전후가 동일한 #790·#873 의도된 실패이며, 제거 전 트리에서 같은 태스크를 돌려 대조했다(4 failed → 4 failed, 동일 4건).

검증

항목 결과
:core:network:testDebugUnitTest BUILD SUCCESSFUL
:konsist:test BUILD SUCCESSFUL (허용목록 경고 0건)
:feature:{afternote,mindrecord,receiver}:data:testDebugUnitTest BUILD SUCCESSFUL
ktlintCheck (전 모듈) BUILD SUCCESSFUL — :konsist:ktlintCheck 부재로 죽던 Ktlint job 은 #1447(develop) 병합으로 해소
:feature:timeletter:data:testDebugUnitTest 4건 실패 — 제거 전후 동일. ci-expected-failures.json 의 #790·#873 의도된 실패
CI 기대실패 init script 동반 전량 실행 BUILD SUCCESSFUL (CI 기대 실패 제외 :feature:timeletter:data:testDebugUnitTest: 4건)
unknown enum 축 실측 InactivityPeriodDto 로 coerce 유/무 동작 차이 확인 후 프로브 제거

CI Test Plan

{
  "androidTest": {
    "mode": "none",
    "reason": "변경 경계가 DI 로 제공되는 Json 설정과 JVM 계약 테스트·konsist 규칙이며, 계기 테스트가 닿는 UI·기기 경계가 없습니다.",
    "tests": []
  }
}

non-null 프로퍼티에 null 이 와도 기본값이 있으면 조용히 치환해, 계약 위반이
텔레메트리 없이 삼켜졌다. 이 팀 기준은 «제외하더라도 세어서 보고» 다(#1010).

응답 DTO 중 실제 coercion 대상은 ReceivedTimeLetterDto.blocks 하나뿐이고,
BE ReceivedTimeLetterResponse 가 미공개 구간에 List.of() 를 채우므로 서버는
이 키를 null 로 보내지 않는다. 나머지 5곳은 요청 DTO 라 디코딩과 무관하다.

konsist 허용목록에서 이미 해소된 13건을 지우고, 프로덕션 설정을 복제하던
core/network 테스트 픽스처 5곳도 같이 맞춘다.

@Sadturtleman Sadturtleman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

방향과 조사에 동의합니다. 이슈가 든 6곳 중 5곳이 요청 DTO 라 무관하다는 것을 실코드로 가려낸 것, 남은 1건(ReceivedTimeLetterDto.blocks)을 BE 소스로 「미공개 구간에도 빈 목록을 채운다」까지 확인한 것, ignoreUnknownKeys 는 남긴 경계까지 정확합니다.

[blocker] 계약 테스트 14개가 아직 coerceInputValues = true 로 돕니다

이 PR 이 프로덕션에서 걷어낸 설정을 테스트가 그대로 켜 두고 있습니다.

$ git grep -l "coerceInputValues = true" -- '*/src/test/*'   →  14 파일
feature/mindrecord/data/.../DiaryListContractTest.kt
feature/mindrecord/data/.../MindRecordDtoContractTest.kt
feature/receiver/data/.../ReceiverAfternoteResponseDtoContractTest.kt
feature/timeletter/data/.../TimeLetterDtoContractTest.kt
… 외 10

두 파일은 주석으로 사실이 아닌 것을 단언하게 됩니다.

/** Json 설정은 `NetworkModule.provideJson` 과 동일 (ignoreUnknownKeys + coerceInputValues). */

이 테스트들의 존재 이유가 「프로덕션과 같은 Json 설정에서 계약이 지켜지는가」인데, 이 PR 이후로는 더 관대한 설정에서 통과합니다. 서버가 non-null 자리에 null 을 보내면 프로덕션은 이제 던지는데 테스트는 여전히 초록입니다 — 이 PR 이 만든 드리프트라 이 PR 이 닫는 것이 맞습니다.

  • 14개 파일의 coerceInputValues = true 를 걷고, 위 두 주석도 사실에 맞게 고쳐 주세요.
  • 걷은 뒤 깨지는 테스트가 있으면 그게 곧 이 PR 의 실제 영향 범위입니다 — 지금은 그게 가려져 있습니다.

곁들여(막지 않음)

coerceInputValues 는 「non-null + 기본값에 null」 말고 unknown enum 값도 기본값으로 접습니다. 응답 DTO 중 enum 타입에 기본값을 둔 자리를 찾아봤는데 0건이라 실제 영향은 없어 보입니다. 조사 표에 그 축도 한 줄 남겨 두시면 다음 사람이 다시 안 찾습니다.

프로덕션에서 `coerceInputValues` 를 걷어내고도 계약 테스트 15곳이 그대로 켜 두고 있었다.
「프로덕션과 같은 설정에서 계약이 지켜지는가」를 보는 테스트가 **더 관대한 설정에서** 통과해,
서버가 non-null 자리에 null 을 보내면 프로덕션은 던지는데 테스트는 초록인 드리프트였다.

- 테스트 Json 15곳의 `coerceInputValues = true` 제거 — 리뷰 시점 14곳에 develop 병합으로
  들어온 `ReceiverAfternotePagingSourceTest` 1곳을 더했다.
- 「provideJson 과 동일 (ignoreUnknownKeys + coerceInputValues)」 주석 7곳을 사실에 맞게 정정.
  걷어낸 지금 테스트 설정은 `provideJson` 과 실제로 동일해졌다.
- `ReceivedAfternoteDetailTitleContractTest` 의 사라진 기제를 가리키던 KDoc·테스트명 정정.

걷어낸 뒤 깨지는 테스트는 없다. `:feature:timeletter:data` 의 4건은 제거 전후가 동일한
#790·#873 의도된 실패(`ci-expected-failures.json`)로, 같은 태스크를 제거 전 트리에서 돌려 대조했다.
@github-actions
github-actions Bot requested a review from Sadturtleman August 29, 2026 17:51
@OnlyReadOneBook

Copy link
Copy Markdown
Collaborator Author

리뷰 반영했습니다. blocker 와 곁들임 둘 다 처리했고, 곁들임 쪽은 실측 결과가 지적과 달라 정정해 남깁니다.

[blocker] 계약 테스트의 coerceInputValues — 걷었습니다

  • 15개 파일 제거. 지적하신 14곳에, 그 사이 develop 병합으로 들어온 ReceiverAfternotePagingSourceTest 1곳이 더해졌습니다(병합 후 재조회해서 잡았습니다).
  • 주석은 7곳이 같은 부류였습니다 — 「provideJson 과 동일 (ignoreUnknownKeys + coerceInputValues)」 → (ignoreUnknownKeys). 인용해 주신 2곳 외에 TodayDailyQuestion·WeeklyReport·DailyQuestionImage·DeliveryVerification·ReceiverEmailAuth 가 같은 문장을 쓰고 있었습니다. 걷어낸 지금은 이 문장이 사실이 됐습니다.
  • ReceivedAfternoteDetailTitleContractTest 는 KDoc 과 테스트명이 사라진 기제(coerceInputValues 가 구해 주지 않는다)를 가리키고 있어 함께 고쳤습니다.

「깨지는 테스트가 곧 실제 영향 범위」 — 새로 깨지는 건 없었습니다

:feature:timeletter:data 4건이 실패하는데, 제거 전 트리에서 같은 태스크를 돌려 대조했더니 동일한 4건이었습니다. ci-expected-failures.json 의 #790·#873 의도된 실패이고 이 변경과 무관합니다. CI 기대실패 init script 를 물려 전량 돌리면 BUILD SUCCESSFUL 입니다.

[곁들임] unknown enum 축 — 0건이 아니라 1건입니다

전수로 다시 훑었습니다(@Serializable enum 9종 × 사용처). 응답 DTO 중 enum 타입에 기본값을 둔 자리가 하나 있습니다 —

DeliveryConditionItemDto.inactivityPeriod: InactivityPeriodDto? = null (응답 DTO). nullable + 기본값이라 coerce 대상입니다. 실측했습니다.

payload: {"inactivityPeriod":"NINETY_YEARS", ...}
coerceInputValues = true  → Success(null)        // 조용히 null 로 접힘
제거 후                    → SerializationException:
                             InactivityPeriodDto does not contain element with name
                             'NINETY_YEARS' at path $.inactivityPeriod

다만 지금 당장의 위험은 0 입니다. FE InactivityPeriodDto 와 BE InactivityPeriod.java(43cf479) 의 값 집합이 THREE_MONTHS·SIX_MONTHS·ONE_YEAR 로 일치합니다. 발화 조건은 «BE 가 값을 새로 추가했을 때» 이고, 그때 조용히 null 이 되는 대신 터지는 게 이 PR 이 세우려는 실패 방향 그대로라 별도 조치는 두지 않았습니다.

말씀대로 조사 축으로 본문에 남겼습니다 — 다음 사람이 다시 안 찾도록.

곁들여 — Ktlint job

:konsist:ktlintCheck 태스크 부재로 죽던 건 제 변경이 아니라 브랜치가 낡아서였고, #1447 이 든 develop 을 병합해 해소했습니다.

@OnlyReadOneBook
OnlyReadOneBook added this pull request to the merge queue Aug 30, 2026
Merged via the queue into develop with commit 1dae727 Aug 30, 2026
29 of 33 checks passed
@OnlyReadOneBook
OnlyReadOneBook deleted the refactor/1494-drop-coerce-input-values branch August 30, 2026 05:51
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.

refactor(core): 전역 coerceInputValues 를 걷는다 — 계약 위반을 텔레메트리 없이 정상값으로 치환

2 participants