Repository navigation
fix(core): 재발급 클라이언트에 전용 Dispatcher 를 줘 콜드 스타트의 동시 401 교착을 없앤다 (#2160) - #2162
Merged
Merged
Conversation
재발급 클라이언트가 base 의 Dispatcher 를 공유해, 같은 호스트의 401 5건이 호스트당 실행 칸 5개를 쥔 채 재발급 락에서 기다리는 동안 재발급 호출은 대기열에서 시작하지 못했다. 대기열 호출에는 callTimeout 도 걸리지 않아 콜드 스타트의 인증 요청이 응답도 실패도 없이 멈췄다. 재발급 클라이언트에만 새 Dispatcher 를 주고 커넥션 풀은 계속 공유한다. NetworkModule KDoc 에 이 예외를 적고, NetworkModuleTest 에 풀 공유· 디스패처 분리 단언을 더했다. 재현 테스트의 정리 단계는 두 디스패처를 모두 닫는다.
Sadturtleman
approved these changes
Sep 25, 2026
21 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
먼저 볼 파일:
NetworkModule.kt(재발급 클라이언트의 전용 Dispatcher 와 KDoc),ConcurrentUnauthorizedReissueTest.kt(운영 조립 그대로의 동시 401 재현),NetworkModuleTest.kt(풀 공유·디스패처 분리 단언)입니다.📌𝘐𝘴𝘴𝘶𝘦𝘴
Closes #2160
📎𝘞𝘰𝘳𝘬 𝘋𝘦𝘴𝘤𝘳𝘪𝘱𝘵𝘪𝘰𝘯
액세스 토큰이 만료된 채 콜드 스타트하면 인증 요청 5건이 응답도 실패도 없이 멈추고
/auth/reissue는 서버로 나가지 않았습니다. 재발급 클라이언트가 base 의Dispatcher를 공유해서입니다. 같은 호스트의 401 5건이 호스트당 실행 칸 5개를 쥔 채TokenReissuer락에서 재발급을 기다리고, 재발급 호출은 칸이 비기를 기다리며 대기열에 머뭅니다. 대기열의 호출에는callTimeout도 걸리지 않습니다. 원인 분석과 스레드 덤프는 #2160 에 있습니다.provideRefreshOkHttpClient에.dispatcher(Dispatcher())한 줄을 더했습니다. 커넥션 풀은 계속 base 와 공유합니다.NetworkModuleTest에 재발급 클라이언트가 풀은 공유하고 디스패처는 따로 쓰는지 단언을 더했습니다.ConcurrentUnauthorizedReissueTest를 싣습니다.NetworkModule·ServiceModule조립과TokenReissuer·TokenAuthenticator·AuthInterceptor를 그대로 쓰고 기본 주소만 로컬 소켓 서버로 바꿉니다. 이 PR 에서 바뀐 곳은 정리 단계뿐으로, 따로 생긴 재발급 디스패처도 닫습니다.이 방식을 고른 이유
maxRequestsPerHost)을 올리면 교착이 나는 동시 요청 수만 뒤로 밀립니다.execute()로 바꾸면 디스패처 상한을 안 받지만TokenApiService.reissue의suspend계약과AuthRepository.rotateToken이 같이 바뀝니다. 전용 디스패처는 같은 효과를NetworkModule한 곳에서 냅니다.Dispatcher(baseClient.dispatcher.executorService)로 칸 계산만 가를 수도 있습니다. 그러면 두 클라이언트가 스레드를 나눠 쓰는데, 서로 막지 않는다는 성질이 실행기가 무제한 캐시 풀이라는 전제에 기댑니다. 새Dispatcher()는 그 전제가 필요 없습니다. 재발급은 락 안에서 한 번에 한 건이라 스레드가 더 드는 것은 재발급 뒤 60초 동안 남는 유휴 스레드 하나입니다.🧪 CI Test Plan
{ "androidTest": { "mode": "none", "reason": "core/network 의 OkHttp 클라이언트 조립(NetworkModule)과 JVM 테스트만 바뀐다. 화면·내비게이션·매니페스트·계측 테스트 파일 변경이 없고, 재발급 교착은 로컬 소켓 서버를 쓰는 JVM 재현 테스트(ConcurrentUnauthorizedReissueTest)와 에뮬레이터 콜드 스타트로 확인했다.", "tests": [] } }✅ 검증
로컬 (JVM)
c49167fba) 재현 테스트요청 5건 중 0건만 5초 안에 끝남. 재발급 서버 도달 0회, 디스패처 실행 5건·대기 1건, maxRequestsPerHost=5:core:network:testDebugUnitTest --rerun-tasksApiWireContractSmokeTest한 케이스), 실패 0. 재현 테스트 4건 0.40초·5건 0.005초.dispatcher(Dispatcher())한 줄만 빼고di패키지 테스트NetworkModuleTest의 디스패처 분리 단언:core:network:ktlintCheck --rerun-tasks:konsist:test --rerun-tasks:app:compileDebugUnitTestKotlin·:app:compileDebugAndroidTestKotlin·:app:assembleDebug에뮬레이터 콜드 스타트
2026-09-23 20:44~20:52 KST, QA 에뮬레이터(Android 16, 1080x2400), dev 서버, QA 계정 세션입니다. 첫 두 회차의 access 는 19:43 에 발급돼 20:43 에 만료된 실제 토큰이고 refresh 는 유효했습니다. 회차마다 앱 정지, logcat 비움, 콜드 스타트, 45초 관찰(
callTimeout30초가 지난 뒤) 순서로 돌렸고, OkHttp 로그는 요청·응답 첫 줄만 셌습니다. 토큰 값은 보지 않고 DataStore 의 access·refresh 가 바뀌었는지와 만료 시각만 확인했습니다..dispatcher(Dispatcher())한 줄만 뺀 빌드), 만료 accessusers/me,users/receivers,daily-questions/today,users/push-tokens,mind-record)이 45초 동안 응답 0건./auth/reissue요청 0회. 인증이 없는app/version만 200. 스레드 덤프에서 요청 스레드 5개 중 1개가TokenReissuer.kt:91의runBlocking에서 락을 쥐고, 4개가TokenReissuer.kt:82에서 그 락을 기다립니다. 홈은 캐시된 이름만 그리고 수신인 상태·오늘의 질문 자리는 45초 뒤에도 로딩 모양 그대로입니다. DataStore 토큰은 바뀌지 않았습니다./auth/reissue1회(26ms). 그 뒤users/me·users/receivers·users/push-tokens200,daily-questions/today·mind-record는 앱이 취소(Canceled). 홈은 "정보를 불러오지 못했습니다." 실패 화면. DataStore 의 access·refresh 가 둘 다 새 값으로 바뀌었습니다(refresh 만료 09-30 20:45).mind-record는 앱이 취소.ReceiverListDto·UserMapper변경을 커밋 없이 얹은 빌드, 유효 access/auth/reissue1회(25ms) 뒤 인증 요청 5건 모두 200, 홈이 다 그려집니다. DataStore 의 access·refresh 가 둘 다 새 값으로 바뀌었습니다(refresh 만료 09-30 20:51).GET /users/receivers응답에authCode가 없고relation이 null 인 행이 있어 develop 의ReceiverListDto(두 필드 필수) 디코드가 실패합니다.GetHomeSummaryUseCase는 수신자 조회를 필수로 묶어 두어, 그 실패가 같은coroutineScope의 나머지 조회를 취소하고 홈 전체를 실패로 그립니다. 유효 access 대조군에서도 똑같이 나고 fix(setting): 수신자 목록 응답 계약을 맞춰 행 소실을 막는다 (#2105) #2120 의 변경을 얹으면 사라집니다. 이 PR 에서는 고치지 않습니다.TokenAuthenticator는 401 의 오류 코드를 읽지 않으므로 만료 토큰과 같은 경로입니다.📷𝘚𝘤𝘳𝘦𝘦𝘯𝘴𝘩𝘰𝘵
💬𝘛𝘰 𝘙𝘦𝘷𝘪𝘦𝘸𝘦𝘳𝘴
TokenReissuer.reissue이름 변경)은TokenReissuer·TokenAuthenticator·AuthInterceptor를 고치고 이 PR 은NetworkModule만 고쳐 파일이 겹치지 않습니다.OkHttpClient.Builder()로 만들어져 디스패처가 따로였습니다. 나머지 파생(메인·S3·Coil)의 공유는 그대로입니다.