SniffMEET. 세션 저장 메소드를 공개해도 괜찮을까?

세션 저장하기 AuthManager와 SessionManager의 역할을 분리하고 나니, 세션을 받아오는 쪽과 저장하는 쪽이 서로 달라지는 문제가 발생했다. (AuthManager가 세션도 관리하던)기존 구조에서는 saveSession(for:)가 외부에 노출되어 있지 않았는데, 역할이 옮겨진 뒤에는 AuthManager에서 받아온 세션을 SessionManager로 전달해야 했다. 고민한 점 saveSession(for:)는 사실상 AuthManager가 세션을 받아온 상황에서만 필요한 메소드다. 그런데 이 메소드를 공개하면 캡슐화가 약해진다는 생각을 했고, 다시 AuthManager로 돌리면 SessionManager가 맡아야 할 책임을 되돌리는 것이 되었다. 따라서 몇 가지 방법을 떠올려 봤다. (최종 선택) saveSession(for:)를 공개한다 saveSession(for:)를 AuthManager로 다시 옮긴다 같은 로직을 AuthManager와 SessionManager에 각각 작성한다 세션이 갱신됐다는 이벤트를 따로 전달할 수 있는 통신 레이어를 만든다 처음에 가장 괜찮아 보였던 건 세션 갱신 이벤트를 따로 전달하는 방식이었다. ...

February 10, 2025

SniffMEET. 세션 갱신과 정보 접근을 SessionManager에 캡슐화하기

SessionManager 사용성 개선 세션을 갱신하는 방법 Supabase 서버와의 통신이 필요할 때, 우선 세션의 유효성을 검증해야 한다. 기존 방식에선 직접 AuthManager의 싱글톤 객체에서 세션을 갱신하는 작업을 했지만 AuthManager와 SessionManager의 역할 명확하게 분리 AuthManager의 싱글톤 해제 와 같은 변화점이 있었고, 기존 AuthManager가 담당하던 세션 갱신, 복원 작업이 SessionManager로 옮겨졌다. SessionManager의 싱글톤 객체에서 세션의 유효성을 체크하는 컴퓨티드 프로퍼티의 부울리언 값을 체크하여 세션을 갱신하는 메소드를 호출하는 방식을 사용했다: if try SessionManager.shared.isExpired { try await SessionManager.shared.refreshSession() } 이 방식은 SessionManager의 싱글톤 객체 호출이 두 번 일어나고, refreshSession이라는 메소드를 직접 노출해서, SessionManager가 세션을 갱신하는 방법을 직접 노출하는 문제가 있다. ...

February 10, 2025

SniffMEET. AuthManager와 SessionManager의 책임 분리하기

작업 배경 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, 인증은 AuthManager가 맡도록 기능을 다시 정의했다. ...

February 10, 2025

SniffMEET. DTO와 Entity의 경계 점검하기

들어가며 SniffMeet에 기능이 붙으면서 DTO와 Entity가 빠르게 늘어났다. 처음에는 필요한 타입을 그때그때 만들면 됐지만, 어느 순간부터 어떤 타입이 어느 레이어에서 쓰이는지 헷갈리기 시작했다. 특히 UserInfoDTO, UserInfo, DogInfo, Mate처럼 비슷한 정보를 담는 타입들이 여러 곳에서 서로 다른 이름으로 사용되고 있었다. 그래서 바로 코드를 고치기보다, 먼저 현재 타입들이 어디에서 쓰이고 있는지 정리해보기로 했다. 먼저 한 일: 사용처 조사 가장 먼저 DTO, Entity, Supabase 관련 Request/Response 타입을 나눠서 사용처를 훑어봤다. 전체 목록을 보면 타입 수가 많은 것도 문제였지만, 더 큰 문제는 몇몇 타입의 사용처가 너무 넓다는 점이었다. ...

January 11, 2025

SniffMEET. 비동기 태스크와 액터 점검하기 (2)

메이트 요청 didTabAcceptButton과 saveMateInfo를 따라가 보니 유스케이스 실행을 위해 Task가 존재했고, Combine의 sink 클로저 안에서도 Task를 생성하고 있었다. Interactor에서 태스크를 묶으면 저장 완료를 알리는 메서드가 따로 필요해 보였다. Presenter 메서드는 async로 바꾸고, View의 sink에서는 Task를 통해 호출하도록 두었다. acceptButton.publisher(event: .touchUpInside) .sink { [weak self] in Task { await self?.presenter?.didTapAcceptButton(id: self?.profile.id ?? DogProfileDTO.example.id) self?.presenter?.closeTheView() } } .store(in: &cancellables) // RequestMatePresenter func closeTheView() { if let view { router?.dismissView(view: view) } } func didTapAcceptButton(id: UUID) async { SNMLogger.info("id: \(id)") await interactor?.saveMateInfo(id: id) } 산책 요청 및 응답 RespondWalkPresenter 산책 요청은 Task로 Interactor에 존재했고, 위치 변환은 Presenter에서 await로 처리했다. ...

January 8, 2025

SniffMEET. 비동기 태스크와 액터 점검하기 (1)

작업 내역 이번 작업에서는 Task, async/await, @MainActor가 섞여 있던 부분을 화면별로 점검했다. 앱 시작 흐름부터 프로필 등록, 메이트 목록, 산책 응답까지 비동기 경계를 다시 살펴보고, 각 화면에서 책임이 어디에 있어야 하는지 정리했다. 앱 시작 플로우 UIWindow의 루트 뷰 컨트롤러를 결정하는 흐름은 @MainActor로 두고, 그 안에서 세션 복원이 끝나기를 기다렸다. await로 대기하는 동안에는 메인 액터를 점유하지 않는다. displayInitialScreen이 MainActor 안에서 실행되는지 breakpoint로 확인했고, displayOnBoardingView는 그대로 MainActor에 남겨 두었다. // AppRouter.swift func displayInitialScreen() { Task { @MainActor in do { try await SupabaseAuthManager.shared.restoreSession() displayTabBar() } catch { displayOnBoardingView() } } } 프로필 입력 및 등록 프로필 입력 부분은 전달만 하는 역할이라 비동기 처리가 필요하지 않았다. ...

January 8, 2025