Skip to content

Conversation

@easyhooon
Copy link
Contributor

@easyhooon easyhooon commented Jan 27, 2026

🔗 관련 이슈

📙 작업 설명

  • synchronized 블럭을 도입하여 동시 다발적인 토큰 재발급 요청을 순차 처리
  • check-then-refresh 패턴 적용(토큰 비교로 이미 갱신된 경우 refresh 스킵)
    • 실패한 요청의 토큰(failedToken)과 현재 저장된 토큰(currentToken)을 비교해서:
      • 다르면 -> 다른 요청이 이미 갱신함 → refresh 스킵, 새 토큰으로 재시도만
      • 같으면 → 아직 갱신 안됨 → 실제 refresh 수행
  • Provider -> Lazy 변경으로 기존에 생성된 ReedService를 재사용

🧪 테스트 내역 (선택)

  • 주요 기능 정상 동작 확인
  • 브라우저/기기에서 동작 확인
  • 엣지 케이스 테스트 완료
  • 기존 기능 영향 없음

💬 추가 설명 or 리뷰 포인트 (선택)

  • feat: AccessToken 갱신을 통한 API 자동 재시도 로직 구현 #27 (comment) 예전 PR에서 언급해주셨던 의견을 드디어 반영했습니다. 실제로 앱에서는 3개 이상의 API가 병렬로 호출되는 도서 상세화면 같은 화면이 존재하였고 이미 AuthToken이 갱신되었음에도 반복적으로 AuthToken이 갱신되는 상황이 발생하고 있었기에 이를 방지해봤습니다.

Summary by CodeRabbit

릴리스 노트

  • 버그 수정

    • 동시 요청 상황에서 토큰 갱신 중복 발생을 방지하도록 동기화 개선
    • 토큰 갱신 실패 시 토큰 정리 및 일관된 오류 처리·로깅 강화
  • 문서

    • 프로젝트 작업 지침 파일 추가(빌드·커밋·설정 관련 안내)
    • 코딩 가이드 문서 추가(코틀린/KCompose 관례 및 불변 컬렉션 사용 권장)

✏️ Tip: You can customize this high-level summary in your review settings.

@github-actions github-actions bot requested a review from seoyoon513 January 27, 2026 07:47
@easyhooon easyhooon changed the title [BOOK-497] refactor: TokenAuthenticator 동시성 처리 개선 및 Provider -> Lazy 적용 refactor: TokenAuthenticator 동시성 처리 개선 및 Provider -> Lazy 적용 Jan 27, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 27, 2026

Walkthrough

프로젝트 작업 지침 문서 두 건을 추가하고, TokenAuthenticator에서 Provider→Lazy 전환 및 synchronized 기반의 check‑then‑refresh 패턴을 도입해 동시 토큰 갱신을 방지하고 에러 처리 및 로깅을 개선했습니다.

Changes

Cohort / File(s) Summary
프로젝트 지침 문서
\.claude/CLAUDE.md, \.claude/CODING.md
작업·빌드·커밋 가이드 문서 및 코딩 가이드(특히 Composable 인자에 대한 불변 컬렉션 사용 권장) 추가.
토큰 인증자 리팩토링
core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenAuthenticator.kt
Provider<ReedService>Lazy<ReedService>로 변경. 동시성 방지용 private lock 및 synchronized 블록 추가, check‑then‑refresh 패턴 적용, 빈 리프레시 토큰 처리(로그·토큰 초기화) 및 실패 시 클리어/중단 로직 추가. 요청 재시도 로직과 로깅 개선.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Authenticator as TokenAuthenticator
    participant DataSource as TokenDataSource
    participant ReedService

    Client->>Authenticator: HTTP Request
    Authenticator->>Authenticator: 응답 401 확인
    Authenticator->>Authenticator: synchronized 블록 진입 (lock)

    alt 다른 요청이 이미 토큰 갱신함
        Authenticator->>DataSource: 최신 액세스 토큰 조회
        Authenticator->>Client: 새 토큰으로 요청 재시도
    else 갱신 필요
        Authenticator->>DataSource: 리프레시 토큰 조회
        alt 리프레시 토큰 빈값 아님
            Authenticator->>ReedService: reedService.value.refreshToken(refreshRequest)
            ReedService-->>Authenticator: 새 액세스/리프레시 토큰 반환
            Authenticator->>DataSource: 새 토큰 저장
            Authenticator->>Client: 새 토큰으로 요청 재시도
        else 리프레시 토큰 없음
            Authenticator->>DataSource: 토큰 초기화(클리어)
            Authenticator->>Client: 인증 실패 반환
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 락을 꽉 쥐고 서서히 밀어보니,
한 번의 갱신으로 모두가 웃네.
중복은 사라지고 흐름은 정리되어,
토큰은 평온히, 서버는 안심 😊
나는 당근 들고 축하 파티를 열겠네.

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 추가된 .claude/CLAUDE.md와 .claude/CODING.md 문서 파일들은 코드 리뷰 가이드라인이며, PR의 핵심 목표인 TokenAuthenticator 리팩토링과는 무관한 범위 외 변경으로 보입니다. 문서 파일들(.claude/CLAUDE.md, .claude/CODING.md)을 별도의 PR로 분리하여 코드 변경사항에만 집중하는 것을 권장합니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 TokenAuthenticator의 동시성 처리 개선과 Provider에서 Lazy로의 변경을 명확히 설명하며 주요 변경사항을 정확히 반영합니다.
Linked Issues check ✅ Passed PR이 #259의 모든 요구사항을 충족합니다: synchronized 블럭 도입, check-then-refresh 패턴 적용, Provider에서 Lazy로 변경이 TokenAuthenticator.kt에 구현되어 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch BOOK-497-refactor/#259

Tip

🧪 Unit Test Generation v2 is now available!

We have significantly improved our unit test generation capabilities.

To enable: Add this to your .coderabbit.yaml configuration:

reviews:
  finishing_touches:
    unit_tests:
      enabled: true

Try it out by using the @coderabbitai generate unit tests command on your code files or under ✨ Finishing Touches on the walkthrough!

Have feedback? Share your thoughts on our Discord thread!


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenAuthenticator.kt`:
- Around line 24-38: In TokenAuthenticator.authenticate, prevent retries with an
empty access token by adding a guard after obtaining currentToken: if
currentToken is blank/empty, return null to avoid building a retry request with
an empty Authorization header; update the synchronized block around
failedToken/currentToken comparison (variables failedToken and currentToken) so
the existing early-return for when failedToken != currentToken remains but only
proceeds if currentToken is non-empty.

Comment on lines 24 to +38
override fun authenticate(route: Route?, response: Response): Request? {
return runBlocking {
try {
val refreshToken = tokenDataSource.getRefreshToken()
// 동시 401 응답 시 중복 refresh 방지 (refresh token rotation 대응)
synchronized(lock) {
val failedToken = response.request.header("Authorization")
?.removePrefix("Bearer ")
.orEmpty()

if (refreshToken.isBlank()) {
Logger.d("TokenAuthenticator", "No refresh token available")
tokenDataSource.clearTokens()
return@runBlocking null
}
val currentToken = runBlocking { tokenDataSource.getAccessToken() }

val refreshTokenRequest = RefreshTokenRequest(refreshToken)
val refreshResponse = serviceProvider().refreshToken(refreshTokenRequest)
// 다른 요청이 이미 토큰을 갱신한 경우, 새 토큰으로 재시도만 수행
if (failedToken != currentToken) {
return response.request.newBuilder()
.header("Authorization", "Bearer $currentToken")
.build()
}
Copy link

@coderabbitai coderabbitai bot Jan 27, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

빈 토큰으로 재시도하는 케이스를 막는 게 안전합니다.
currentToken이 비어있는데도 재시도하면 불필요한 401 재요청이 발생할 수 있습니다. 빈 토큰이면 바로 종료하도록 가드 추가를 고려해주세요.

💡 제안 수정
 if (failedToken != currentToken) {
+    if (currentToken.isBlank()) {
+        return null
+    }
     return response.request.newBuilder()
         .header("Authorization", "Bearer $currentToken")
         .build()
 }
🤖 Prompt for AI Agents
In
`@core/network/src/main/kotlin/com/ninecraft/booket/core/network/TokenAuthenticator.kt`
around lines 24 - 38, In TokenAuthenticator.authenticate, prevent retries with
an empty access token by adding a guard after obtaining currentToken: if
currentToken is blank/empty, return null to avoid building a retry request with
an empty Authorization header; update the synchronized block around
failedToken/currentToken comparison (variables failedToken and currentToken) so
the existing early-return for when failedToken != currentToken remains but only
proceeds if currentToken is non-empty.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

currentToken이 빈 토큰일 경우의 수가 있을지 고민 좀 해봄

Copy link

Choose a reason for hiding this comment

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

@easyhooon 좋은 질문입니다. currentToken이 빈 토큰일 수 있는 시나리오를 정리해보면:

  1. 동시 요청 중 다른 요청의 refresh 실패: 다른 스레드가 토큰 갱신을 시도했지만 실패해서 clearTokens()를 호출한 경우
  2. 사용자 로그아웃: 401 응답과 이 체크 사이에 로그아웃이 발생한 경우
  3. 다른 메커니즘으로 토큰 초기화: 예를 들어, 만료된 토큰 감지 등으로 토큰이 클리어된 경우

이런 상황에서 가드 없이 빈 토큰으로 재시도하면:

  • Authorization: Bearer (빈 값)으로 요청 → 불필요한 401 재발생
  • 가드가 있으면 바로 null 반환 → 불필요한 네트워크 요청 방지

실무에서는 드문 케이스지만, 동시성 이슈나 엣지 케이스를 방어하는 차원에서 가드를 추가하는 것이 안전합니다. 특히 이 PR이 동시성 처리 개선을 목표로 하는 만큼, 이런 엣지 케이스도 함께 처리하면 더 견고한 코드가 될 것 같습니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

그나저나 synchronized block vs mutex block

이 상황에선 둘 중에 어떤 블록이 더 적합하다고 생각해?

Copy link

Choose a reason for hiding this comment

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

@easyhooon 좋은 질문입니다! 이 상황에서는 synchronized block이 더 적합합니다. 이유는:

synchronized가 더 적합한 이유

  1. 인터페이스 제약: Authenticator.authenticate()는 OkHttp의 동기(synchronous) 메서드이고, suspend 함수로 만들 수 없습니다
  2. 블로킹 컨텍스트: OkHttp의 인터셉터 체인은 스레드 블로킹 방식으로 동작합니다
  3. 이미 runBlocking 사용 중: 어차피 suspend 함수를 호출하기 위해 runBlocking을 사용하고 있어서, Mutex를 써도 결국 runBlocking { mutex.withLock { ... } } 형태가 되어 오히려 오버헤드만 증가합니다
  4. 단순한 락 요구사항: 복잡한 코루틴 스케줄링이 필요 없고, 단순히 동시 접근을 막는 것이 목적이므로 JVM 네이티브 synchronized가 더 효율적입니다

Mutex가 유리한 경우

반대로 Mutex는 다음 상황에서 더 좋습니다:

  • 전체 로직이 코루틴 기반일 때 (suspend 함수 체인)
  • 스레드를 블로킹하지 않고 suspend하고 싶을 때
  • 구조화된 동시성(structured concurrency)을 활용하고 싶을 때

결론

현재 코드는 OkHttp의 동기 API 제약 때문에 synchronized가 정답입니다. 만약 Retrofit의 Call Adapter나 다른 방식으로 완전히 코루틴 기반으로 리팩토링한다면 그때 Mutex로 전환을 고려할 수 있습니다.

참고로, 현재 구현은 동시성 처리를 위한 올바른 접근입니다! 👍

Copy link
Contributor Author

@easyhooon easyhooon Jan 30, 2026

Choose a reason for hiding this comment

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

Mutex가 아닌 synchronized 블럭을 선택한 이유가 있을까요?? synchronized 관련 블로그 글은 잘 봤습니다 ☺️

@seoyoon513 위 의견에 공감하고 있습니다.

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@@ -0,0 +1,21 @@
# Hime 프로젝트 작업 지침
Copy link
Contributor

Choose a reason for hiding this comment

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

굿

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

try {
val refreshToken = tokenDataSource.getRefreshToken()
// 동시 401 응답 시 중복 refresh 방지 (refresh token rotation 대응)
synchronized(lock) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Mutex가 아닌 synchronized 블럭을 선택한 이유가 있을까요?? synchronized 관련 블로그 글은 잘 봤습니다 ☺️

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @.claude/CLAUDE.md:
- Around line 13-15: 해당 Markdown 파일의 목록 들여쓰기 규칙이 일관되지 않습니다: 중첩된 리스트 항목은 2칸(스페이스
두 개) 들여쓰기를 사용하도록 `.claude/CLAUDE.md`의 목록 줄(`🤖 Generated with [Claude Code]...`,
`Co-Authored-By: Claude Sonnet...`, 그리고 그 아래의 한국어 문장)을 수정하세요; 각 하위 항목이 최상위 항목보다
정확히 2칸 들여쓰기되도록 정렬하고 탭 대신 스페이스를 사용하며 다른 목록 항목들과 스타일(백틱, 하이픈 등)을 일관되게 유지하세요.

Comment on lines +13 to +15
- `🤖 Generated with [Claude Code](https://claude.com/claude-code)`
- `Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>`
- 커밋 작업은 사용자가 직접 수행하는 경우가 많으므로, 요청받지 않은 경우 커밋하지 마세요
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

마크다운 들여쓰기 수정 필요

정적 분석 도구에서 리스트 항목의 들여쓰기가 일관되지 않다고 지적했습니다. 중첩된 리스트 항목은 2칸 들여쓰기를 사용해야 합니다.

📝 들여쓰기 수정 제안
 - **커밋 메시지에서 Claude 관련 문구를 제거합니다**
 - 다음 문구들을 커밋 메시지에 포함하지 마세요:
-    - `🤖 Generated with [Claude Code](https://claude.com/claude-code)`
-    - `Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>`
+  - `🤖 Generated with [Claude Code](https://claude.com/claude-code)`
+  - `Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>`
 - 커밋 작업은 사용자가 직접 수행하는 경우가 많으므로, 요청받지 않은 경우 커밋하지 마세요
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- `🤖 Generated with [Claude Code](https://claude.com/claude-code)`
- `Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>`
- 커밋 작업은 사용자가 직접 수행하는 경우가 많으므로, 요청받지 않은 경우 커밋하지 마세요
- `🤖 Generated with [Claude Code](https://claude.com/claude-code)`
- `Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>`
- 커밋 작업은 사용자가 직접 수행하는 경우가 많으므로, 요청받지 않은 경우 커밋하지 마세요
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 13-13: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


[warning] 14-14: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


[warning] 15-15: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

🤖 Prompt for AI Agents
In @.claude/CLAUDE.md around lines 13 - 15, 해당 Markdown 파일의 목록 들여쓰기 규칙이 일관되지
않습니다: 중첩된 리스트 항목은 2칸(스페이스 두 개) 들여쓰기를 사용하도록 `.claude/CLAUDE.md`의 목록 줄(`🤖
Generated with [Claude Code]...`, `Co-Authored-By: Claude Sonnet...`, 그리고 그 아래의
한국어 문장)을 수정하세요; 각 하위 항목이 최상위 항목보다 정확히 2칸 들여쓰기되도록 정렬하고 탭 대신 스페이스를 사용하며 다른 목록 항목들과
스타일(백틱, 하이픈 등)을 일관되게 유지하세요.

@easyhooon easyhooon merged commit 18354ac into develop Jan 31, 2026
4 checks passed
@easyhooon easyhooon deleted the BOOK-497-refactor/#259 branch January 31, 2026 06:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BOOK-497/refactor] Retrofit Authenticator 로직 개선

3 participants