Skip to content

feat(core-network): expiresIn 기반 액세스 토큰 선제 reissue (closes 408)#411

Open
1hyok wants to merge 5 commits into
feat/407from
feat/408
Open

feat(core-network): expiresIn 기반 액세스 토큰 선제 reissue (closes 408)#411
1hyok wants to merge 5 commits into
feat/407from
feat/408

Conversation

@1hyok

@1hyok 1hyok commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

📌𝘐𝘴𝘴𝘶𝘦𝘴

Closes #408 — feat(core-network): expiresIn 활용 — 액세스 토큰 만료 전 선제 reissue
연계 #410(BE) — expiresIn 을 발급 응답으로 이동 (2026-06-20 서버 반영·실측 확인)

📎𝘞𝘰𝘳𝘬 𝘋𝘦𝘴𝘤𝘳𝘪𝘱𝘵𝘪𝘰𝘯

  • 발급 응답 dataexpiresIn 수신 — LoginData(Default/Social)·ReissueData 에 필드 추가. BE chore(backend): expiresIn 을 목록 응답에서 토큰 발급 응답(로그인·리이슈)으로 이동 요청 #410 으로 expiresIn 이 봉투 최상위에서 발급(로그인/리이슈) 응답 data 안(RFC 6749 §5.1 의 access_token 형제 자리)으로 이동했고, BaseResponse 봉투 필드·EXPIRES_IN_SERIAL_NAME 상수는 제거 (df2bf92)
  • 신규 token/AccessTokenExpiryTracker — 만료 deadline 을 in-memory 보관 (elapsedRealtime 기준, 임계 60초, 의도적 비영속 — 재시작 후엔 다음 발급/회전이 다시 기록)
  • 신규 token/TokenReissuer — 선제·401 두 reissue 경로의 단일 비행(single-flight) 락. "기대 토큰 vs 저장 토큰" 재확인으로 이중 rotateToken 경쟁 차단. 회전 성공 시 발급 응답의 expiresIn 으로 deadline 기록(미동봉이면 clear), 실패 시 clear
  • 발급 지점 기록 — 로그인 성공은 AuthRepositoryImpl(default/kakao/google 3개 진입점), 회전 성공은 TokenReissuer 가 각각 발급 응답 expiresIn 을 record. 과거 모든 성공 응답을 peek 하던 횡단 수집(AuthInterceptor.recordExpiresIn)은 제거
  • AuthInterceptor — 만료 임박 시 요청 전 선제 reissue(실패는 기존 401 안전망에 위임, clearSession 안 함). 응답 peek·json 의존 제거로 토큰 부착·선제 갱신 책임만 남김
  • TokenAuthenticator — 회전을 TokenReissuer 경유로 전환, withBearer/responseCount 를 OkHttp 공식 recipes 형태의 확장으로 정리
  • core:network 유닛 테스트 27개 — tracker 경계·reissuer 단일 비행/record 분기 계약·authenticator 401 계약 6종 + 발급 DTO data.expiresIn 수신 계약(AuthDtoExpiresInContractTest 3종). 인터셉터 봉투 기록 가드는 peek 제거로 함께 제거 (9fb8fb0, df2bf92)

📷𝘚𝘤𝘳𝘦𝘦𝘯𝘴𝘩𝘰𝘵

UI 변경 없음 — core:network 토큰 갱신 로직 전용.

💬𝘛𝘰 𝘙𝘦𝘷𝘪𝘦𝘸𝘦𝘳𝘴

  • stack base: feat(afternote): 수신자 본인확인 이메일 인증 stub → 실 API 전환 (closes 407) #409(feat/407) 위에 쌓음 — BaseResponse.kt 겹침. feat(afternote): 수신자 본인확인 이메일 인증 stub → 실 API 전환 (closes 407) #409 머지 후 develop 재타겟 예정 (base 필터 CI 는 재타겟+push 전까지 안 돎).
  • chore(backend): expiresIn 을 목록 응답에서 토큰 발급 응답(로그인·리이슈)으로 이동 요청 #410 서버 해결 반영 완료 (2026-06-20 실측, 동일 QA 계정·전부 status 200): POST /auth/login·POST /auth/reissue 응답 dataexpiresIn:3600 추가, GET /users/receivers 목록에선 제거(롤백) 확인. 이에 맞춰 수신 경로를 "모든 응답 봉투 peek" → "발급 응답 data 기록"으로 교체 — 회전 메커니즘(tracker·TokenReissuer·선제 분기·401 전환)은 채널과 무관하게 유지. 위치는 봉투 최상위가 아니라 data 안이라 FE 가 거기서 읽는다.
  • 로그인 경로 record 단위 테스트는 보류: TokenDataSource(final + DataStore 의존) fake 셋업이 convention plugin 의존성을 건드릴 위험(throwaway)이라, ① 데이터 수신은 AuthDtoExpiresInContractTest(발급 DTO data.expiresIn 디코드) ② 회전 record 는 TokenReissuerTest 로 가드. 로그인 record 호출 자체는 명시적 3줄 + 에뮬 실측으로 커버. repo 단위 테스트가 필요하면 후속 커밋으로 추가 가능.
  • 선제 reissue 실동작은 "만료 59분 경과" 시나리오라 단위 테스트로 검증.
  • BE 는 현재 refresh 재사용 허용(실측: 동일 refresh 로 reissue 2연속 200) — TokenReissuer 단일 비행은 현 시점 방어적 설계이고, BE 가 rotation(RFC 9700 권고) 도입 시 필수가 된다.
  • 로그아웃이 만료 임박 창(60초)에 걸리면 회전된 구 refresh 로 로그아웃이 나가는 한계는 의도 수용 — AuthInterceptor KDoc 명시.
  • isReturnDefaultValuesTokenAuthenticator 의 Log.e 경유 유닛 테스트용 — 공식 가이드의 last-resort 단서를 build.gradle.kts 주석에 반영.
  • 빌드 검증: :core:network:testDebugUnitTest(27) · :core:data:testDebugUnitTest · :app:assembleDebug · :core:network/:core:data/:core:model ktlintCheck 모두 BUILD SUCCESSFUL.

1hyok added 2 commits June 11, 2026 13:55
- `AuthInterceptorTest`: 선제적 토큰 재발급(reissue) 및 응답 봉투의 `expiresIn` 값을 통한 만료 기한 학습 로직 검증
- `TokenAuthenticatorTest`: 401 Unauthorized 응답 발생 시 사후 대응, 최대 재시도 제한 및 세션 정리 계약 검증
- `AccessTokenExpiryTrackerTest`: 토큰 만료 기한(deadline) 산식 및 60초 임계값 경계 조건 테스트
- `TokenReissueCoordinatorTest`: 다중 경로 요청 시 중복 재발급을 방지하는 단일 비행(single-flight) 조정 로직 검증
- `BaseResponseExpiresInContractTest`: 서버 응답의 `expiresIn` 필드 유무에 따른 JSON 디코딩 호환성 테스트
- 테스트 시나리오 구성을 위한 `FakeAuthRepository` 및 `RecordingChain` 구현 추가
- 액세스 토큰의 만료 시점(deadline)을 단조 시계(`SystemClock.elapsedRealtime`) 기반으로 추적하는 `AccessTokenExpiryTracker` 추가
- 선제 갱신(`AuthInterceptor`)과 401 사후 대응(`TokenAuthenticator`) 간의 중복 회전을 방지하기 위해 단일 락(Single-flight)을 보장하는 `TokenReissuer` 구현
- `AuthInterceptor`에서 성공 응답 봉투의 `expiresIn` 필드를 추출하여 토큰 수명을 기록하고, 만료 임박 시 선제적으로 reissue를 수행하는 로직 추가
- `BaseResponse` 공통 응답 모델에 `expiresIn` 필드 추가 및 관련 상수 정의
- `TokenAuthenticator`가 `TokenReissuer`를 경유하도록 리팩터링하고, `responseCount` 및 요청 재생성 로직(`withBearer`) 개선
- 단위 테스트 실행 시 `android.util.Log` 등 Android API 스텁의 기본값 반환을 위해 `isReturnDefaultValues` 설정 활성화
- 기존 `TokenReissueCoordinator`를 `TokenReissuer`로 대체 및 관련 테스트 코드 최신화
@1hyok 1hyok added the feature 기능추 label Jun 11, 2026
- `expiresIn` 수신이 특정 엔드포인트에 의존하며, 사용자 동선에 따라 기록되지 않을 수 있음을 명시
- 기록이 없을 경우 선제 갱신이 생략되고 401 안전망을 통해 만료 처리가 수행됨을 설명
- 향후 근본적인 해결 방향(#410)에 대한 참고 정보 추가
1hyok added 2 commits June 20, 2026 23:39
- `BaseResponse` 공통 응답 봉투 최상위에 정의되어 있던 `expiresIn` 필드 제거
- `LoginData` 및 `ReissueData` DTO 내부로 `expiresIn` 필드 이동 (#410 서버 계약 변경 대응)
- `AuthInterceptor`에서 모든 성공 응답 본문을 peek 하여 `expiresIn`을 추출하던 횡단 관심사 로직 제거 (성능 개선)
- `AuthRepositoryImpl`(로그인) 및 `TokenReissuer`(토큰 회전)에서 응답 데이터 수신 시 직접 `AccessTokenExpiryTracker`에 만료 시각을 기록하도록 변경
- `TokenBundle` 모델 및 `AuthMapper`에 `expiresIn` 필드 추가 및 매핑 로직 반영
- 공통 봉투 기반의 만료 테스트(`BaseResponseExpiresInContractTest`)를 삭제하고, DTO 기반의 신규 계약 테스트(`AuthDtoExpiresInContractTest`) 추가
- `AuthInterceptorTest` 및 `TokenReissuerTest`에서 변경된 만료 정보 기록 흐름에 맞춰 테스트 케이스 현행화
- `AuthInterceptor`가 매 요청 직전 호출하여 현재 시각 기준으로 만료 임박 여부를 매번 재계산함을 명시
- 만료 시각 정보가 없을 경우(null)의 동작 방식(선제 갱신 스킵 및 401 발생 시 사후 대응)에 대한 설명 구체화

@koongmai koongmai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

#411 로그아웃 요청도 선제 reissue 대상이라 새 refresh token이 서버에 남을 수 있음
core/network/.../AuthInterceptor.kt:59에서 만료 임박이면 모든 인증 요청에 대해 선제 reissue를 수행하고, core/data/.../AuthRepositoryImpl.kt:117의 logout은 그 전에 읽어둔 기존 refresh token으로 로그아웃 요청을 보냅니다.
토큰 만료 60초 이내에 logout하면 interceptor가 먼저 새 refresh token을 발급/저장한 뒤, 서버에는 old refresh token만 전달될 수 있어서 “로컬 로그아웃은 됐지만 서버 세션 폐기는 안 된” 상태가 됩니다. logout endpoint는 선제 reissue 제외하거나, logout 시 최신 refresh token 기준으로 요청하도록 고쳐야 합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 기능추

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants