SessionManager와 AuthManager 역할 분리하기 ‐ 최진원 - Team-HGD/SniffMEET GitHub Wiki

작업 내역

백로그

SessionManager와 AuthManager

두 매니저는 서로 매우 밀접하게 연관된 작업을 합니다. AuthManager는 사용자의 계정 인증을 담당하고 세션을 받아오며, SessionManager는 그 받아온 세션을 관리하는 작업을 해야합니다.

하지만 기존 코드에서는 대부분의 작업을 AuthManager가 처리하고 SessionManager는 세션을 소유하는 작업 이외의 역할을 맡지 않고 있던 문제가 있었습니다.

AuthManager가 하고 있던 세션 작업을 SessionManager로 옮기기

기존에는 AuthManager에서 세션 갱신, 복원, 저장, 로드 작업을 처리하고 있고, SessionManager는 세션의 만료 여부 확인과 세션 객체를 소유하는 역할만 합니다.

// AuthManager 인터페이스 프로토콜
protocol AuthManager {
    static var shared: AuthManager { get }
    var authStateSubject: PassthroughSubject<AuthState, Never> { get set }
    func signInAnonymously() async throws
    func restoreSession() async throws
    func refreshSession() async throws
    func loadTokens() throws
}

// SessionManager 클래스
final class SessionManager {
    static let shared = SessionManager()
    var session: SupabaseSession?
    var isExpired: Bool {
        guard let session else { return true }
        // 세션 만료를 파악할 때는 30초의 여유시간을 줍니다.
        return Date(timeIntervalSince1970: TimeInterval(session.expiresAt + 30)) < Date()
    }

    private init() {}
}

SessionManager는 인터페이스로 사용할 프로토콜이 존재하지 않아 콘크리트 타입으로 직접 싱글톤 객체에 접근하는 상황입니다.

또한 SessionManager가 세션을 관리해야하는데, 이 부분을 AuthManager가 처리하고 있습니다. 그래서 다음과 같이 서로의 역할을 구분했습니다:

// AuthManager 인터페이스 프로토콜
protocol AuthManageable {
    func signInAnonymously() async throws
}

// SessionManager 인터페이스 프로토콜
protocol SessionManageable {
    var userID: Result<UUID, SupabaseSessionError> { get }
    var accessToken: Result<String, SupabaseSessionError> { get }
    func restoreSession() async throws
    func saveSession(for session: SupabaseSession?) throws
    func checkSession() async throws
}

기존에 AuthManager가 담당하던 세션 복원, 갱신, 로드, 저장 역할을 SessionManager로 옮겼습니다.

싱글톤 구조 관리하기

AuthManager는 세션을 받아오는 것 말고는 세션에 관여하지 않기 때문에, 더 이상 앱 전반적으로 상태가 공유될 필요가 없습니다. 따라서 싱글톤 객체를 해제하고, 유즈케이스를 만들어 AuthManager를 주입하는 방식으로 로그인 하도록 했습니다.

protocol SignInUseCase {
    func execute() async throws
}

struct SignInUseCaseImpl: SignInUseCase {
    private let authManager: any AuthManageable
    
    init(authManager: any AuthManageable) {
        self.authManager = authManager
    }
    
    //TODO: 파라미터에 따라서 로그인 방식을 구분할 수 있도록 확장 가능할 것 같습니다.
    func execute() async throws {
        try await authManager.signInAnonymously()
    }
}

대조적으로 여전히 SessionManager는 앱 전반적으로 세션의 상태를 공유할 필요성이 있습니다. 세션 매니저의 싱글톤 구조를 해제하는 것 보다, 유지하는 것이 더 이점이 크다 생각하여 싱글톤 구조를 유지했습니다.

세션 저장하기 - SRP 위반 vs. 캡슐화 위반 vs. 재사용성 포기하기

하지만 문제가 하나 있습니다. 세션을 받아오는 것은 AuthManager가 하는데, 세션을 저장하는 것은 SessionManager가 하게 됩니다.

위 코드에서 보면 알 수 있듯이, 기존 구조에서 AuthManager에 있던 saveSession(for:)메소드는 공개 메소드가 아니었습니다. 하지만 역할이 옮겨간 이후 AuthManager에서 받아온 세션을 SessionManager로 전달해서 저장해야 되기 때문에 프로토콜에 공개해야 했습니다.

saveSession(for:)메소드가 외부에서 사용될 상황은 AuthManager가 응답으로 세션을 받아온 상황 한 번뿐인데, 불필요하게 공개한 느낌이 있습니다.

이를 해결하기 위해 몇 가지 방법을 생각할 수 있습니다.

  • saveSession(for:) 메소드를 노출한다. (현재 상황, 나쁜 캡슐화)

  • saveSession(for:) 메소드를 AuthManager로 원상복구한다. (SRP 위반)

    • 이 방법은 아래 있는 방법의 하위호환이므로 논외 (SessionManager에서도 쓰는 메소드 이므로)
  • saveSession(for:) 메소드의 로직을 AuthManager와 SessionManager 각각 작성한다 (재사용성 포기)

  • 노티피케이션 센터로 세션 받아온 상황 알리고, 데이터 전달하기

    • 컴바인은 SessionManager에서 AuthManager를 모르기 때문에 사용하기 힘듦 (역방향은 가능)
    • 이 방법을 도입하기 전에 팀 원들 의견을 들어봐야 할 것 같음
  • 노티피케이션 센터는 너무 앱 전역적이니까, SessionManager와 AuthManager가 통신할 수 있는 자체적인 레이어 만들어서 해결

    • NotificationCenter.default에 독립적인 커스텀 노티피케이션 센터 객체 만들기?
    • Supabase에 진짜 fit하게 내부 커스텀 이벤트 버스 만들기 (그냥 수도코드)
    // 세션 이벤트 버스
    final class SessionEventBus {
        private var sessionHandler: ((SupabaseSession) -> Void)?
        func subscribe(to handler: @escaping (SupabaseSession) -> Void) {
            self.sessionHandler = handler
        }
        func publish(session: SupabaseSession) {
            sessionHandler?(session)
        }
    }
    
    // 굳이 싱글톤 말고 SupabaseConfig에서 관리해도 좋을듯?
    // 지금은 싱글톤이랑 별차이 없지만 진짜 나중에 모듈화 하면 다르니까
    
    enum SupabaseConfig {
    		...
    		static let sessionEventBus = SessionEventBus()
    }
    
    // AuthManager 퍼블리시
    final class AuthManager {
        func signInAnonymously() async throws {
    		    ...
            SupabaseConfig.sessionEventBus.publish(session: session)
        }
    }
    
    // SessionManager 수신
    final class SessionManager {
        private init() {
            SupabaseConfig.sessionEventBus.subscribe { [weak self] session in
                do {
                    try self?.saveSession(session)
                } catch {
                    throw SupabaseSessionError.saveSessionFailed
                }
            }
        }
    }
    
    • 이야기하자!

인사이트

  • 노티피케이션 센터 객체를 생성하면 독립적으로 사용 가능하다.
    • default라는 싱글톤 객체명 때문에 될 것 같았는데 확실하게 확인 함

레퍼런스