Skip to content

Conversation

@youz2me
Copy link
Member

@youz2me youz2me commented Mar 1, 2025

👻 PULL REQUEST

📄 작업 내용

  • 유저 ID, 닉네임 등 앱에서 자주 사용하는 정보들을 UserDefaults로 저장하기 위해 UserSessionStorage, UserSessionWrapper, UserSession을 각각 구현했어요.

💻 주요 코드 설명

UserSession을 이용한 로컬 정보 저장 구조체 구현

  • 로컬에 저장되어야 하는 정보를 선언한 UserSession 구조체를 선언했습니다.
  • UserDefaults에 저장하기 위해 인코딩, 디코딩이 지원되어야 하므로 Codable 프로토콜을 채택했습니다.
  • id를 프로퍼티로 가지므로 Identifiable 프로토콜도 함께 채택했습니다.
  • 알림 뱃지 값의 경우 로그인 이후 다른 API를 통해 받아오는 것으로 알고 있어 옵셔널 처리했습니다.
  • 자동 로그인 여부는 따로 서버에서 내려주지 않는 것으로 알고 있는데 클라이언트에서 처리하는 값인지 이후 따로 받아와야 하는지 확인되지 않아 일단 옵셔널 처리했습니다. 의견 주시면 감사하겠습니다 !!
struct UserSession: Codable, Identifiable {
    let id: Int
    let nickname: String
    let profileURL: String
    let isPushAlarmAllowed: Bool
    let isAdmin: Bool
    let isAutoLoginEnabled: Bool?
    let notificationBadgeCount: Int?
}

UserSessionStorage를 사용한 UserSessionWrapper 인터페이스 정의

  • UserSessionWrapper에서 구현될 메서드들의 인터페이스를 정의했습니다.
  • 이후 계정 전환 등의 케이스를 고려해 여러 세션을 저장할 수 있도록 설계했습니다.
  • 자동 로그인 여부, 알림 뱃지 값의 경우 로그인 이후 따로 값이 들어올 경우를 고려해 메서드를 따로 만들어 두었습니다.
import Foundation

// MARK: - UserSessionStorage

protocol UserSessionStorage {
    func fetchAllUserSessions() -> [String: UserSession]
    func fetchUserSession(forUserID userID: String) -> UserSession?
    func fetchActiveUserSession() -> UserSession?
    func fetchActiveUserID() -> String?
    func updateUserSession(_ session: UserSession, forUserID userID: String)
    func updateAutoLogin(enabled: Bool, forUserID userID: String)
    func updateNotificationBadge(count: Int, forUserID userID: String)
    func updateActiveUserID(forUserID userID: String?)
    func removeUserSession(forUserID userID: String)
}

UserSessionWrapper를 사용한 UserSession 관리 로직 구현

  • UserSessionStorage에서 정의한 인터페이스를 구현했습니다.
  • 현재 활성화된 유저 아이디와 UserDefaults에 접근할 때 사용할 키 값을 Keys라는 enum으로 정의했습니다.
class UserSessionWrapper {
    private enum Keys {
        static let userSessions = "sessionDictionary"
        static let activeUserID = "activeID"
    }
    
    private let defaults = UserDefaults.standard
}
  • 세션들을 불러오고 저장하는 로직을 private helper로 정의해 구현했습니다.
  • 이후 메서드 구현 시 해당 helper들을 사용했습니다.
// MARK: - Private Helper

extension UserSessionWrapper {
    private func fetchSessions() -> [String: UserSession] {
        guard let data = defaults.data(forKey: Keys.userSessions),
              let sessions = try? JSONDecoder().decode([String: UserSession].self, from: data) else {
            return [:]
        }
        return sessions
    }
    
    private func updateSessions(_ sessions: [String: UserSession]) {
        if let data = try? JSONEncoder().encode(sessions) {
            defaults.set(data, forKey: Keys.userSessions)
        }
    }
}

👀 기타 더 이야기해볼 점

  • 진짜너무졸려요

🔗 연결된 이슈

@youz2me youz2me added ✨ feat 기능 또는 객체 구현 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌 labels Mar 1, 2025
@youz2me youz2me requested a review from JinUng41 March 1, 2025 19:38
@youz2me youz2me self-assigned this Mar 1, 2025
Copy link
Collaborator

@JinUng41 JinUng41 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다!
코멘트에도 적어두었지만, 로그인한 유저 정보를 다루기 위한 UserSession 모델을 정의하신 점이 매우 인상적이었습니다.
특히 네이밍 부분에서 말이죠. 앞으로도 이런 네이밍을 지어주실 와블의 이유진님을 기대하고 있겠습니다.

유진님께서는 해당 객체를 Repository에서 활용할 Infra 수준의 객체로 인식하시어, 네이밍에 Storage 또는 Wrapper를 사용하신 것으로 보입니다. 그러나 저는 UserSession을 UserDefaults에 저장하는 객체로 정의하는 것이 Repository의 역할에 더 부합한다고 판단합니다. 즉, UserDefaults라는 저장 방식을 감추고 추상화한 객체는 단순한 Wrapper가 아니라 요구사항에 맞춘 Repository로 보는 것이 적절하다고 생각했어요.

  • 첫 번째 이유는, UserSession Storage/Wrapper는 ‘로그인한 유저 정보’라는 특정 정보를 저장하고 관리한다는 점에서, Infra 수준의 범용 객체보다는 도메인 요구사항에 맞춘 Repository 수준의 객체가 적절하다는 것입니다.

  • 두 번째 이유는, KeychainWrapper와 달리 UserSession Storage/Wrapper는 모든 데이터를 저장할 수 있는 형태가 아니기 때문입니다. KeychainWrapper는 모든 데이터 형식을 Data로 변환하여 저장할 수 있으며, save, load, delete와 같은 통합 인터페이스를 제공하는 반면, UserSession Storage/Wrapper는 그러한 범용성을 갖추지 못합니다.

  • 마지막으로, Storage/Wrapper를 Repository 내부에서 사용하면 Repository가 제공해야 하는 정보 외의 데이터를 다루게 될 가능성이 있습니다. 오히려 Storage/Wrapper를 Repository 수준으로 끌어올리고, UseCase에서 UserSessionRepository와 다른 Repository를 함께 활용함으로써 해당 문제를 효과적으로 해소할 수 있다고 봅니다. UseCase가 단일 Repository만 이용해야 한다는 제약은 없으므로, 이 경우 UserSession이 도메인 모델로 정의되어야 하는 점에 대해 보다 심도 있게 검토할 필요가 있습니다.

그래서 결론적으로는 빠른 시일 내에 이와 관련해서 직접 토의를 해보면 좋을 것 같아요~
읽어보시고 유진님의 의견도 남겨주세요~ 화이팅!

Comment on lines 11 to 19
struct UserSession: Codable, Identifiable {
let id: Int
let nickname: String
let profileURL: String
let isPushAlarmAllowed: Bool
let isAdmin: Bool
let isAutoLoginEnabled: Bool?
let notificationBadgeCount: Int?
}
Copy link
Collaborator

@JinUng41 JinUng41 Mar 3, 2025

Choose a reason for hiding this comment

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

로그인한 유저 정보를 다룬다는 점에서 UserSession 이라는 네이밍은 너무나도 탁월한 것 같습니다.
저라면 LoginUserInfo 이런 식으로 다룰 것 같았거든요. 대단하십니다. 👍


자동 로그인 여부는 따로 서버에서 내려주지 않는 것으로 알고 있는데 클라이언트에서 처리하는 값인지 이후 따로 받아와야 하는지 확인되지 않아 일단 옵셔널 처리했습니다. 의견 주시면 감사하겠습니다 !!

제가 알기로는 자동로그인 정보는 서버에서 따로 내려주지 않는 것으로 알고 있습니다.
안드로이드 파트에서 자체 판단하에 구현한 것으로 알고 있는데요.
왜 구현했는지, 어떤 근거를 가지고 판단하는지 등의 로직은 안드로이드 개발자분들께 여쭤봐야 할 것 같습니다.

Comment on lines 13 to 23
protocol UserSessionStorage {
func fetchAllUserSessions() -> [String: UserSession]
func fetchUserSession(forUserID userID: String) -> UserSession?
func fetchActiveUserSession() -> UserSession?
func fetchActiveUserID() -> String?
func updateUserSession(_ session: UserSession, forUserID userID: String)
func updateAutoLogin(enabled: Bool, forUserID userID: String)
func updateNotificationBadge(count: Int, forUserID userID: String)
func updateActiveUserID(forUserID userID: String?)
func removeUserSession(forUserID userID: String)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

좋은 프로토콜 추상화인 것 같습니다.
Swift 언어의 특징인 POP를 잘 따르고 계시는군요!

Comment on lines 24 to 28
guard let data = defaults.data(forKey: Keys.userSessions),
let sessions = try? JSONDecoder().decode([String: UserSession].self, from: data) else {
return [:]
}
return sessions
Copy link
Collaborator

Choose a reason for hiding this comment

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

guard 구문에서 조건문이 2가지 이상일 경우, else는 무조건 리턴하기로 약속했던 것을 잠시 깜빡하신 것 같아요.


JSONDecoder는 메서드가 불릴 때마다 생성될 것 같은데요.
이러한 방법 보다는 프로퍼티로 소유하고, 이를 사용하는 형태면 메모리를 절약할 수 있을 것 같아요.

Comment on lines 32 to 34
if let data = try? JSONEncoder().encode(sessions) {
defaults.set(data, forKey: Keys.userSessions)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

JSONEncoder도 프로퍼티로 가진다면, 매번 생성될 필요가 없어지겠죠? ㅎㅎ

Comment on lines 12 to 15
private enum Keys {
static let userSessions = "sessionDictionary"
static let activeUserID = "activeID"
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

UserDefaults 저장을 위한 키를 String으로 사용하지 않은 점 최고에요! 👍

@JinUng41 JinUng41 self-requested a review March 5, 2025 13:52
Copy link
Collaborator

@JinUng41 JinUng41 left a comment

Choose a reason for hiding this comment

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

이즈 굳~
키체인과 유저 디폴츠를 하나로 묶어 추상화를 구현하시다니 대단하십니다.
몇 가지 코멘트만 확인 부탁드려요.
고생하셨습니다.

Comment on lines 16 to 17
init(userDefaults: UserDefaults, jsonEncoder: JSONEncoder, jsonDecoder: JSONDecoder) {
self.userDefaults = userDefaults
Copy link
Collaborator

Choose a reason for hiding this comment

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

init(userDefaults: UserDefaults = .standard, ...) 정도는 구현해도 좋을 것 같아요.

Copy link
Member Author

Choose a reason for hiding this comment

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

반영했습니다!

Comment on lines +40 to +43
func removeValue(for key: String) throws {
guard userDefaults.object(forKey: key) != nil else {
throw LocalError.dataNotFound
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

좋은 에러 추상화네요~

Comment on lines 11 to 15
protocol LocalStorage {
func setValue<T: Codable>(_ value: T, for key: String) throws
func getValue<T: Codable>(for key: String) throws -> T?
func removeValue(for key: String) throws
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

지금은 CoreData 등을 사용하고 있지 않지만, 저는 그럼에도 메서드의 기능이 Key-Value의 형식으로 동작하는 것처럼 보여서, LocalKeyValueStorage로 네이밍을 변경하는 것은 어떨까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

반영했습니다! 좋은 의견 감사합니다

@youz2me youz2me merged commit d273064 into develop Mar 5, 2025
@youz2me youz2me deleted the feat#108-local-repository branch March 5, 2025 14:14
@youz2me youz2me added this to Wable-iOS Mar 6, 2025
@youz2me youz2me moved this to Done in Wable-iOS Mar 6, 2025
@youz2me youz2me removed this from Wable-iOS Mar 6, 2025
youz2me added a commit that referenced this pull request Oct 26, 2025
[Feat] UserDefaults 정보 저장을 위한 UserSession 기능 구현
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 기능 또는 객체 구현 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 로컬 레포지토리 구현

3 participants