Repository navigation
fix(auth): Keychain 토큰 저장 실패 전파 (조용한 로그아웃 방지) - #110
Conversation
기존엔 saveAccessToken이 Keychain 결과를 버려서, 저장 실패 시 이번 세션은 로그인 상태처럼 보이지만 다음 실행 때 토큰이 사라져 조용히 로그아웃됐다. - TokenStorageProtocol.saveAccessToken → @discardableResult -> Bool - TokenStore: 저장 성공 시에만 인메모리 반영(세션 상태=실제 저장 일치). 로그아웃은 인메모리 항상 클리어 - LoginError.tokenPersistenceFailed 추가 - AuthRepositoryImpl.login/signup: 저장 실패 시 throw (login은 저장을 do/catch 밖으로 이동해 에러가 삼켜지지 않게) - AuthTests 목 시그니처 동기화 검증: build-for-testing + AuthTests 통과. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request updates the token storage mechanism to return a boolean indicating success or failure when saving or deleting an access token. It also updates the authentication repository to handle token persistence failures by throwing a new tokenPersistenceFailed error. The review feedback suggests refactoring the saveAccessToken method in TokenStore.swift to use a guard let statement instead of if let, which reduces nesting and improves code readability in accordance with Swift style guidelines.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if let token { | ||
| KeychainStorage.save(token, key: Key.accessToken) | ||
| let saved = KeychainStorage.save(token, key: Key.accessToken) | ||
| if saved { | ||
| lock.withLock { state in | ||
| state.token = token | ||
| state.loaded = true | ||
| } | ||
| } | ||
| return saved | ||
| } else { | ||
| KeychainStorage.delete(key: Key.accessToken) | ||
| let deleted = KeychainStorage.delete(key: Key.accessToken) | ||
| // 로그아웃/삭제 시 보안상 인메모리는 항상 비운다 (Keychain 삭제 실패와 무관). | ||
| lock.withLock { state in | ||
| state.token = nil | ||
| state.loaded = true | ||
| } | ||
| return deleted | ||
| } |
There was a problem hiding this comment.
if let token과 else 블록을 사용하는 대신, guard let token else 패턴을 사용하면 중첩(nesting)을 줄이고 "Happy Path"(토큰 저장 성공 흐름)를 함수의 최상위 레벨에 유지할 수 있어 Swift 스타일 가이드에 더 부합하며 가독성이 향상됩니다.
| if let token { | |
| KeychainStorage.save(token, key: Key.accessToken) | |
| let saved = KeychainStorage.save(token, key: Key.accessToken) | |
| if saved { | |
| lock.withLock { state in | |
| state.token = token | |
| state.loaded = true | |
| } | |
| } | |
| return saved | |
| } else { | |
| KeychainStorage.delete(key: Key.accessToken) | |
| let deleted = KeychainStorage.delete(key: Key.accessToken) | |
| // 로그아웃/삭제 시 보안상 인메모리는 항상 비운다 (Keychain 삭제 실패와 무관). | |
| lock.withLock { state in | |
| state.token = nil | |
| state.loaded = true | |
| } | |
| return deleted | |
| } | |
| guard let token else { | |
| let deleted = KeychainStorage.delete(key: Key.accessToken) | |
| // 로그아웃/삭제 시 보안상 인메모리는 항상 비운다 (Keychain 삭제 실패와 무관). | |
| lock.withLock { state in | |
| state.token = nil | |
| state.loaded = true | |
| } | |
| return deleted | |
| } | |
| let saved = KeychainStorage.save(token, key: Key.accessToken) | |
| if saved { | |
| lock.withLock { state in | |
| state.token = token | |
| state.loaded = true | |
| } | |
| } | |
| return saved |
References
- Swift API Design Guidelines 및 일반적인 Swift 스타일 가이드에서는 조기 탈출(early exit)을 위해
guard문을 사용하고, 함수의 주요 흐름(happy path)을 최소한의 들여쓰기로 유지하는 것을 권장합니다. (link)
CI가 여태 테스트를 안 돌려(SwiftLint만) 숨어 있던 결함들: - ProjectHelper: 모든 Testing 타깃에 .shared 추가 (SearchTesting이 Shared 참조하나 미선언 → 빌드 실패) - LoginViewModel: handleLoginError switch에 .tokenPersistenceFailed 처리 추가 (PR #110에서 케이스 추가 후 non-exhaustive 회귀) - MypageTests: StubTokenStorage.saveAccessToken을 -> Bool로 동기화 (PR #110 프로토콜 변경 미반영) 로컬 tuist test --platform ios 전체 통과 확인. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
문제
TokenStore.saveAccessToken이KeychainStorage.save의 반환값(성공 여부)을 버렸다. Keychain 저장이 실패하면:fix/stability-hardening(#107)이save를-> Bool로 바꿔뒀지만, 콜러가 그 값을 안 썼다. 이 PR이 콜러까지 연결한다.변경
TokenStorageProtocol.saveAccessToken→@discardableResult -> BoolTokenStore: 저장 성공 시에만 인메모리 반영 → 세션 상태가 실제 저장과 어긋나지 않음. 로그아웃/삭제는 보안상 인메모리 항상 클리어LoginError.tokenPersistenceFailed추가AuthRepositoryImpl.login/signup: 저장 실패 시throw→ UI가 로그인 실패로 처리·재시도 가능. (login은 저장 로직을do/catch밖으로 빼 에러가networkError로 삼켜지지 않게)검증
tuist generate+xcodebuild build-for-testing성공AuthTests통과🤖 Generated with Claude Code