Skip to content

GET /app/version 이 배선 0건 — 강제 업데이트를 붙이거나 죽은 서비스를 걷는다 #1539

Closed

Description

@OnlyReadOneBook

작업 유형

bug — 버그·오동작

주 담당 모듈

platform — Android 앱·CI·빌드·릴리스·저장소 운영

개요

#423 이 스웨거 대조로 GET /app/version(강제 업데이트용)을 network 계층에 추가했지만, 그 뒤 아무도 부르지 않는다. DI 에만 등록돼 있고 repository·usecase·UI 어디에도 소비처가 없다.

실측 (origin/develop)

심볼 소비처
AppVersionApiService service/AppVersionApiService.kt(선언) · di/ServiceModule.kt(등록) · ApiWireContractSmokeTest.kt(계약 테스트)
getAppVersion 위와 동일 — presentation·domain 참조 0건

즉 앱은 서버가 「이 버전은 더 못 쓴다」고 말해도 그 사실을 물어보지 않는다. 구버전 사용자가 계속 쓰다 다른 곳에서 깨진다.

완료 조건

둘 중 하나로 끝낸다. 지금처럼 «있는데 안 쓰는» 상태로 두지 않는다.

  • 배선한다 — 앱 시작 시 버전을 조회해 강제 업데이트가 필요하면 스토어로 보내는 관문을 붙인다. 서버 무응답·조회 실패에 앱이 잠기지 않아야 한다(fail-open)
  • 또는 걷는다 — 제품 요구가 아니면 AppVersionApiService·DI 등록·계약 테스트를 함께 지운다

참고

#423 의 「후속(범위 밖)」에 적혀 있던 «신규 엔드포인트들의 usecase/UI 배선» 중 아직 주인이 없는 갈래다. 같은 후속의 나머지는 이미 추적되고 있다 — 비밀번호 찾기 배선은 #457, 아이디 찾기 결과 화면은 #474, 구 users/delivery-condition 화면 마이그레이션은 이미 완료(구 엔드포인트 참조 0건, 설정 화면이 수신자별 getReceiverDeliveryConditions 를 쓴다).

POST /users/me/activity 는 의도적으로 배선하지 않는다 — 서버가 로그인·토큰 재발급에서 마지막 활동 시각을 갱신하므로 중복 왕복이다(Afternote-BE#137, #1413).

Activity

  1. added
    bugSomething isn't working
    area:platformCI·빌드·릴리스·저장소 운영 영역
    on Aug 29, 2026
  2. github-actions commented on Aug 29, 2026

    @github-actions
    Contributor

    bug 이슈는 사이드바 Fields > Priority 로 심각도를 지정해야 합니다. 판정 기준:

    • Urgent: 치명 — 크래시·데이터 소실·보안 무력화·핵심 경로 차단·가짜 성공
    • High: 심각 — 실패·틀린 값의 정상 위장, 핵심 기능 무동작
    • Medium: 불편 — 우회로가 있는 오동작·틀린 표시, 핵심 흐름은 유지
    • Low: 경미 — 문구·정렬·마감 품질 흠, 기능 영향 없음

    값이 비어 있어도 이슈를 닫지는 않습니다. 이 안내는 이슈당 한 번만 답니다.

  3. OnlyReadOneBook commented on Aug 30, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    재판정 — 두 갈래 다 지금은 실행할 수 없다. 실측이 이지선다의 전제를 둘 다 깼다

    origin/develop fc8f32cd5 · Afternote-BE origin/main 0a4b870 · 실서버 실측 2026-08-30 20:31 KST.

    1. 「죽은 서비스」가 아니다 — BE 는 살아 있고 적극 유지 중이다

    BE 실코드로 판정했다(OpenAPI 문서는 전 필드 optional 로 보여 근거가 못 되므로 제외).

    src/main/java/com/afternote/domain/appversion/controller/AppVersionController.java

    @RequestMapping("/api/v1/app/version")
    public class AppVersionController {
        @SecurityRequirements // 전역 bearer 상속 제거 → OpenAPI security: []
        @GetMapping
        public ApiResponse<AppVersionCheckResponse> checkVersion(
                @RequestParam AppPlatform platform,
                @RequestParam @Min(1) int versionCode) { ... }
    }

    선언만 있는 게 아니라 도메인 한 벌이 통째로 살아 있다 — AppVersionAdminController(/api/v1/admin/app-versions 릴리스 등록·조회) · AppVersionDataInitializer(시드) · AppVersionReleaseRepository · 서비스·컨트롤러 테스트 3종. WhiteListUrl.java:31 에 APP_VERSION("/api/v1/app/version") 로 무인증 화이트리스트 등록까지 돼 있다.

    응답 계약(AppVersionCheckResponse record)은 FE AppVersionDto 와 필드·nullability 가 정확히 일치한다 — updateRequired: boolean · latestVersionCode: int · storeUrl: String?(nullable=true).

    → 「제품 요구가 아니면 걷는다」의 전제인 «죽었다» 가 반증됐다. 걷는 쪽은 오판이다. 지우면 Play 출시 시점에 그대로 다시 짜야 하고, 누락 키를 fail 시키는 AppVersionContractTest 회귀 가드까지 같이 사라진다.

    덧붙여 계약 문구가 이 이슈 본문과 충돌한다. BE @Operation description:

    스플래시 시작 시 호출합니다. … API 호출 실패 또는 updateRequired=true이면 클라이언트에서 스플래시 진입을 막습니다.

    BE 는 조회 실패에도 fail-closed(스플래시 진입 차단)를 기술하는데, 이 이슈 완료 조건은 fail-open("서버 무응답·조회 실패에 앱이 잠기지 않아야 한다")을 요구한다. 배선 전에 어느 쪽인지 확정이 선행이다.

    2. 배선 0건은 사실이다 (재확인)

    src/test·src/androidTest 포함 심볼명 전수 집계:

    심볼 사용처
    AppVersionApiService 선언 1 · di/ServiceModule.kt 2 · ApiWireContractSmokeTest 2
    AppVersionDto 선언 1 · 서비스 2 · AppVersionContractTest 4
    checkVersion 선언 1 · ApiWireContractSmokeTest 1

    presentation·domain 참조 0건, */src/androidTest/* 참조 0건. 본문 그대로다.

    3. 그런데 지금 배선하면 모든 사용자가 100% 앱에서 잠긴다 — 실서버 실측

    $ curl "https://afternote.kro.kr/api/v1/app/version?platform=ANDROID&versionCode=1"
    {"status":200,"code":200,"message":"성공",
     "data":{"updateRequired":true,"latestVersionCode":10001,
             "storeUrl":"http://your-playstore-domain"}}
    

    그리고 이 앱의 실제 versionCode 는 모든 빌드에서 1 이다.

    • build-logic/src/main/kotlin/VersionCode.kt:4 — DEFAULT_AFTERNOTE_VERSION_CODE = 1
    • AFTERNOTE_VERSION_CODE 를 주입하는 워크플로 0건. docs/release/distribution.md:181 이 명시한다 — "일반 로컬·Firebase 빌드는 기본 versionCode 1을 유지한다. … 현재 값을 자동 산출·주입하거나 Play에 배포하는 workflow는 없다."

    즉 1 < 10001 이 항상 참 → 존재하는 모든 빌드가 updateRequired=true 를 받는다. 관문을 붙이는 순간 개발·기획·QA 테스터 전원이 스플래시에서 잠기고, 보내지는 목적지는 http://your-playstore-domain — 존재하지 않는 도메인이다.

    4. 보낼 곳 자체가 없다 — Play Store 등록이 아직 없다

    docs/play-release.md:18

    2026-08-05 기준, 현재 팀에서 확인 가능한 Google 계정은 Play Console 접속 시 /console/signup의 개발자 계정 생성 화면으로 이동한다. 따라서 아직 앱 등록과 Play App Signing 등록이 없으며…

    배포 채널은 Firebase App Distribution 하나뿐이다. 게다가 같은 런북의 기본안대로면 Firebase APK 와 Play APK 는 설치 인증서가 달라 서로 위에 업데이트할 수 없다 — 강제 업데이트 관문이 테스터를 스토어로 보내도 인플레이스 업데이트가 원리적으로 불가능하고 삭제·재설치가 필요하다.

    5. 시안 0건 (참고 — 이건 블로커가 아니다)

    Figma 정본 페이지 정리 Screen Design(4327:43064) 의 11개 구역 전수 walk 결과 강제 업데이트 화면·다이얼로그 0건. 업데이트 텍스트 히트는 전부 약관 문구("애프터노트는 서비스 품질 향상을 위해 기능을 변경하거나 업데이트할 수 있습니다")뿐이고, playstore 페이지(3309:16877)는 스토어 등록용 스크린샷이라 무관하다.

    다만 2026-08-23 디자이너 위임으로 시안 부재는 블로킹 근거가 아니다 — FE 재량으로 확정하면 된다. 진짜 블로커는 3·4번이다.

    결론과 조치

    • 걷지 않는다 — 서비스가 살아 있다. AppVersionApiService·DI·DTO·계약 테스트는 그대로 둔다.
    • 지금 배선하지 않는다 — 붙이는 즉시 전 사용자 잠금이고, 보낼 스토어가 없다. 이건 FE 안에서 닫을 수 없는 선행 조건이다.
    • 이 이슈를 Google Play 내부 테스트 트랙 배포 자동화 #852 에 blocked_by 로 건다. Google Play 내부 테스트 트랙 배포 자동화 #852 범위가 선행 조건을 정확히 덮는다 — Play Console 등록·Play App Signing, 그리고 "versionCode가 모든 배포에서 단조 증가하도록 repository 정책을 정하고". Google Play 내부 테스트 트랙 배포 자동화 #852 자신도 완료 조건에 "Play Console 또는 계정 준비가 안 된 동안에는 이슈를 blocked 상태로 두되 credential을 임시로 우회하지 않는다" 를 두고 있다.

    배선 재개 시 착수 조건 (#852 이후 이 이슈에서 확인한다)

    1. AFTERNOTE_VERSION_CODE 가 실제 릴리스 versionCode 로 주입돼 서버 latestVersionCode 와 같은 축에서 비교 가능할 것
    2. 서버가 storeUrl 에 실 Play Store URL 을 들고 있을 것 — 현재 dev 실측값은 http://your-playstore-domain 플레이스홀더다(BE application.yml 의 APP_ANDROID_STORE_URL 배포 환경변수가 미확정 값). 소비처가 0건인 지금은 무해하므로 별건으로 떼지 않고, 이 이슈 재개 시 재측정해 여전히 플레이스홀더면 그때 BE 와 확정한다.
    3. 실패 시 fail-open / fail-closed 를 BE 계약 문구와 이 이슈 사이에서 확정할 것 (현재 상반)
    4. Firebase 테스터가 Play 빌드로 넘어갈 때의 삭제·재설치 안내를 관문 문구에 포함할지 결정
  4. added
    pr-open이 이슈를 구현하는 PR 이 열려 있다 — base 가 기본 브랜치가 아니어도 드러낸다
    on Aug 30, 2026
  5. OnlyReadOneBook commented on Aug 30, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    배선했다 — PR #1642. blocked_by #852 를 걷을지 판단을 구한다

    이 이슈의 2026-08-30 재판정(«걷지 않는다 · 지금 배선하지 않는다»)을 다시 실측했다. 두 결론 중 앞의 것은 그대로 유효하고, 뒤의 것은 관문 안에서 닫을 수 있었다.

    유효한 것 — 걷지 않는다

    BE 실코드 재확인. origin/main 8e605f4 · origin/release 8bfe43d 양쪽에 domain/appversion 파일 14개가 있고, AppVersionController.java · AppVersionCheckResponse.java 는 두 브랜치에서 바이트 동일이다. main↔release 129 커밋 차에 이 도메인은 걸리지 않는다 — 실서버에도 살아 있다.

    응답 스키마도 실코드로 확정했다(updateRequired: boolean · latestVersionCode: int · storeUrl: String?). 기존 AppVersionDto 와 정확히 일치해 DTO 는 손대지 않았다.

    닫은 것 — 「지금 배선하면 전 사용자가 잠긴다」

    그 판정은 맞다. 다만 원인 두 가지가 전부 «관문이 사용자를 어디로도 보낼 수 없는 상태» 이고, 그건 관문 자신이 런타임에 판정할 수 있다. 그래서 updateRequired=true 를 받아도 아래 둘을 각각 확인한 뒤에만 막게 했다.

    이 이슈가 짚은 블로커 관문이 거는 조건 오늘의 결과
    서버 storeUrl 이 http://your-playstore-domain 플레이스홀더 market://details?id= · https://play.google.com/store/apps/details? 화이트리스트 걸린다 → 막지 않음
    모든 빌드 versionCode = 1, 서버 latest = 10001. Firebase 산출물은 서명이 달라 Play 가 못 덮는다 BuildConfig.STORE_DISTRIBUTED_BUILD — 릴리스 워크플로가 versionCode 를 주입한 산출물에서만 참 거짓 → 막지 않음

    오늘 존재하는 모든 빌드에서 아무도 잠기지 않는다. 오늘의 실서버 응답값(updateRequired=true + 플레이스홀더 URL)을 그대로 넣은 단위 테스트가 그것을 직접 단언한다. 그러면서 조회 자체는 매 기동마다 실제로 나가므로, 경계가 깨지면 Play 출시 때가 아니라 지금 텔레메트리에 드러난다.

    「배선 재개 시 착수 조건」 4개도 이 구조가 각각 흡수한다.

    1. versionCode 축 일치 — 안 맞는 동안은 관문이 스스로 켜지지 않는다. 주입이 서면 자동으로 켜진다.
    2. storeUrl 플레이스홀더 — 실 URL 이 아닌 동안은 막지 않고, stage=force_update_store_url non-fatal 로 남는다. 재측정을 사람이 기억할 필요가 없다.
    3. fail-open / fail-closed — 이슈 완료 조건대로 fail-open 으로 확정했다. BE @Operation 문구(«API 호출 실패 시에도 스플래시 진입 차단»)와 상반되는 점은 코드 KDoc·PR 본문에 명시했다. 덧붙여 관문이 시작을 지연시키지도 않게 했다 — 스플래시를 붙잡지 않고 앱은 평소대로 뜬 뒤 판정이 오면 그 위에 팝업이 얹힌다. 타임아웃 값에 기대는 실패 경로 자체가 없다.
    4. Firebase→Play 삭제·재설치 안내 — 2번 조건 때문에 Firebase 빌드는 관문을 아예 보지 않으므로 이 문구 결정이 필요 없어졌다.

    구할 판단 — blocked_by #852 를 걷을 것인가

    위 근거로 이 코드는 #852 를 기다릴 이유가 없다고 본다. 다만 그 관계를 건 판단이 이 이슈 본인의 것이라 임의로 걷지 않았고, PR 본문도 Closes 가 아니라 Refs #1539 로 두었다(Closes 를 쓰면 merge-order-guard 가 red 가 된다).

    동의하면 blocked_by 를 걷어 주면 PR 본문을 Closes #1539 로 올린다. 반대로 「관문 코드가 develop 에 있는 것 자체가 이르다」고 보면 PR 을 닫고 이 이슈는 지금 상태로 남긴다.

  6. OnlyReadOneBook commented on Sep 2, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    #852 와의 blocked_by 를 해제합니다. PR #1642 의 관문은 storeUrl 이 실 Play 주소이고 STORE_DISTRIBUTED_BUILD 가 참일 때만 막으므로, #852 가 세울 versionCode 주입·Play 등록은 이 코드의 선행 조건이 아니라 «켜지는 시점» 일 뿐입니다. 관계를 걸었던 전제(지금 배선하면 전원 잠금)가 코드로 사라졌습니다. #852 는 그대로 열려 있고 범위도 줄지 않았습니다. PR 은 Closes #1539 로 올립니다.

  7. removed
    pr-open이 이슈를 구현하는 PR 이 열려 있다 — base 가 기본 브랜치가 아니어도 드러낸다
    on Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:platformCI·빌드·릴리스·저장소 운영 영역bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions