Skip to content

refactor: 노선명 파싱을 DesignSystem으로 모으고 복제 제거 - #373

Merged
YuGeonHui merged 6 commits into
env/devfrom
refactor/v2-transport-badge-dedup
Sep 24, 2026
Merged

YuGeonHui merged 6 commits into
env/devfrom
refactor/v2-transport-badge-dedup

Conversation

@YuGeonHui

Copy link
Copy Markdown
Collaborator

무엇이 문제였나

TransportBadgeMapper가 Home·Search에 바이트 단위로 동일하게 복제돼 있었다(diff 결과 0줄 차이, 각 91줄). 지하철 노선 하나를 추가하면 두 파일을 같이 고쳐야 하고, 한쪽만 고쳐도 빌드는 통과한다.

어떻게 나눴나

노선명 문자열 파싱(버스 종류 · 지하철 노선 테이블)을 DSTransportBadge.Kind의 팩토리로 옮겼다. 이 테이블은 팔레트와 짝이라 색이 추가되면 같이 바뀐다 — DesignSystem이 들고 있는 게 맞다.

DesignSystem이 Domain을 import하지는 않는다. DSTransportBadge.Kind는 의도적으로 Domain 타입이 아니고(기존 주석이 그렇게 말한다), 디자인 시스템이 TransportLeg을 알게 되면 비즈니스 엔티티에 묶인다. 경계를 문자열에 두면 둘 다 지킨다.

전 후
Home TransportBadgeMapper 91줄 26줄 (leg.mode switch만)
Search TransportBadgeMapper 91줄 (동일 복제) 26줄
노선 테이블 2벌 1벌 (DesignSystem)
테이블 테스트 0개 15개

테스트가 잡게 된 것

복제돼 있던 동안 이 테이블에는 테스트가 하나도 없었다. 특히 원래 코드의 주석이 말하던 규칙 —

"Ordered so distinctive names win before the bare N호선 patterns (인천 1호선 must not fall through to 1호선)"

— 이 이제 깨지면 잡힌다. 테이블에 줄을 잘못된 위치에 넣는 것이 이 코드의 유일한 실질적 실수 경로다.

검증

항목 결과
AtchaV2 Debug / Stage / Release 3구성 모두 통과
DesignSystem 73 통과 (+15 신규)
HomeFeature / SearchFeature 66 / 26 통과

🤖 Generated with Claude Code

YuGeonHui and others added 6 commits September 24, 2026 22:54
GetCurrentLocation / ReverseGeocode / SearchPlaces / RecentSearches 를
지우고 호출처가 Domain 포트(LocationService·PlaceRepository·
RecentSearchRepository)를 직접 주입받는다. 넷 다 단일 Repository 메서드를
한 줄 위임하던 계층이라, 같은 프로토콜에 이름만 하나 더 입히고 있었다.

Presentation → Domain 경계는 그대로다 — 피처가 받는 건 여전히 Domain
프로토콜이고 구체 Data 타입은 AppDIContainer만 본다.

부수 정리:
- 위임 여부만 검증하던 Domain 테스트 5개 삭제 (Observe/ObserveChange/
  RequestAlarmSync/GetLastRouteDetail/ReverseGeocode). 앞 커밋에서 UseCase를
  지울 때 테스트 파일이 남아 DomainTests가 컴파일되지 않고 있었다.
- Example/Tests 스텁이 좁은 UseCase 대신 PlaceRepository 전체를 채택하게 되어
  쓰지 않는 메서드에 기본 구현을 달았다(의도된 Tests/Example 중복 규약).
- RecentSearchesUseCase.fetch() → RecentSearchRepository.recentSearches()로
  호출명이 바뀐다.

검증: Debug/Stage/Release 3구성 빌드 + Domain 112 · AtchaData 53 · AtchaV2 39
· HomeFeature 66 · SearchFeature 26 · SettingsFeature 18 전부 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
검색 화면은 진입할 때마다 위치 획득 + GET /locations/rgeo를 다시 돌았고,
같은 키워드를 다시 쳐도 매번 서버로 갔다. 코드베이스 전체에 응답 캐시가
0건이었다.

CoreStorage에 ExpiringCache<Value>를 추가하고, Data 레이어에
CachingPlaceRepository 데코레이터로 감쌌다. 피처는 여전히 Domain
프로토콜만 보므로 호출처는 한 줄도 바뀌지 않는다.

캐시 대상은 "틀렸을 때의 피해"로 골랐다:
- 역지오코딩 7일 — 좌표→주소는 행정구역 개편 수준에서만 바뀐다
- 장소 검색 1시간 — POI는 생기고 없어진다
- 서비스 지역 판정 캐시 안 함 — 틀리면 되는 지역을 막는다. 호출도
  집 주소 저장 시 1회뿐이라 아낄 왕복이 없다

좌표는 키로 쓰기 전에 반올림한다(역지오코딩 4자리≈11m, 검색 편향
3자리≈110m). GPS는 가만히 있어도 미터 단위로 떨려서, 원시 좌표를 키로 쓰면
적중률이 사실상 0이 된다.

만료에는 ±10% 지터를 넣는다. 같은 TTL로 한꺼번에 채워진 항목은 한꺼번에
만료되는데, 이 앱의 실제 피크는 막차 시간대에 수천 기기가 동시에 앱을 여는
순간이라 만료가 몰리면 그 순간 요청이 서버에 몰린다.

실패는 캐시하지 않는다 — 지하철에서 한 번 실패한 키워드가 지상에 나와도
계속 실패로 답하면 재시도 버튼이 의미를 잃는다.

저장은 FileKeyValueStore(.cache) — 백업에서 제외된다. UserDefaults는 첫
접근에 plist 전체를 올리므로 이 크기(≈20KB)를 담을 자리가 아니다.

부수: RecentSearchRecordDTO → PlaceRecordDTO. 최근 검색과 캐시가 같은
표현을 쓰게 되어 이름이 용도를 좁게 말하고 있었다.

검증: 3구성 빌드 + CoreStorage 44(+11) · AtchaData 64(+11) 등 전 스킴 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
리팩토링으로 확정된 규칙이 커밋 메시지에만 남아 있었다. 다음 작업자가
같은 판단을 다시 내리지 않도록 규약으로 적는다.

- UseCase는 기본이 아니다(포트 2개 조합 또는 비즈니스 규칙일 때만)
- 화면 상태 채널은 정확히 2개, != oldValue 게이트 유지
- 영속·캐시는 CoreStorage의 actor 3종으로만, 매체는 값 크기로 가름
- 응답 캐시는 Data 데코레이터로, 대상은 "틀렸을 때의 피해"로 선정
- 막차 경로 캐시를 왜 아직 안 했는지와 도입 조건 2가지

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AlarmSessionLifecycleService.isAcknowledged는 쓰기 2곳, 읽기 0곳이었다.
"스냅샷 영속화의 인메모리 미러"였는데 정본(AlarmSessionStore)이 세션 수명을
들게 되면서 미러를 볼 이유가 사라졌다. 남겨 두면 두 곳이 어긋날 수 있다는
인상만 준다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TransportBadgeMapper가 Home·Search에 **바이트 단위로 동일하게** 복제돼
있었다. 지하철 노선 하나를 추가하면 두 파일을 같이 고쳐야 했고, 한쪽만
고쳐도 빌드는 통과한다.

노선명 문자열 파싱(버스 종류·지하철 노선 테이블)을 DSTransportBadge.Kind의
팩토리로 옮겼다. 이 테이블은 팔레트와 짝이라 색이 추가되면 같이 바뀌므로
DesignSystem이 들고 있는 게 맞다.

DesignSystem이 Domain을 import하지는 않는다. DSTransportBadge.Kind는
의도적으로 Domain 타입이 아니고(원래 주석), 디자인 시스템이 TransportLeg을
알게 되면 비즈니스 엔티티에 묶인다. 경계를 문자열에 두면 둘 다 지킨다 —
피처에는 leg.mode switch 약 10줄만 남는다(각 91줄 → 26줄).

테스트 15개 신설. 복제돼 있던 동안 이 테이블에는 테스트가 하나도 없었다.
특히 "인천 1호선이 1호선으로 떨어지면 안 된다"는 테이블 순서 규칙이 이제
깨지면 잡힌다.

검증: 3구성 빌드 + DesignSystem 73(+15) · HomeFeature 66 · SearchFeature 26.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gitignore에 개별 경로 규칙 하나만 있어서 다른 디렉터리의 .DS_Store는
그대로 커밋될 수 있었고, 실제로 #371에서 Projects/.DS_Store가 딸려
들어갔다. 전역 규칙으로 바꾸고 추적 중이던 3개를 인덱스에서 뺀다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@YuGeonHui
YuGeonHui merged commit 4949fc1 into env/dev Sep 24, 2026
1 check passed
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.

1 participant