클린아키텍처를 사용해 로그인 정보 저장 로직 구성 및 데이터 전달

챗봇 앱을 구현하면서 TCA를 활용해 아키텍처를 구성할까, 클린아키텍처를 활용해 구성할까 고민하다가 외부라이브러리를 사용하지 않고 클린아키텍처를 통해 제대로 한번 구현해보고 싶은 마음에 클린아키텍처를 활용해 프로젝트를 구성하기로 했다.

 

이전 블로그 글을 통해 클린아키텍처에 대해서 정리했지만 간단하게 다시 훑고 지나가 보자 클린아키텍처는 크기 Presentation, Domain, Data 로 계층을 나눈다.

 

Presenstation은 UI와 사용자의 상호작용을 처리하는 역할을 담당하고 View와 ViewModel로 나뉜다. View는 사용자의 행동을 받아드려 ViewModel로 전달하게 되고 ViewModel은 사용자 이벤트를 처리하며 UseCase를 호출해 비즈니스 로직을 실행한다.

 

Domain은 핵심 비즈니스 로직과 규칙을 가지고 있는 계층으로 UseCase, Entity로 나누어 구현한다. UseCase는 특정 기능의 유스케이스를 정의하고 구현하며 Presentation에서 호출하는 방식으로 구현된다. Entity는 데이터 모델을 가지게 된다.

 

Data는 데이터 소스와 통신을 처리하는 계층으로 Repository와 DataSource로 나뉜다. 레포지토리는 데이터 접근 인터페이스와 실제 데이터 저장소 간의 중간 계층이며 데이터소스는 로컬 또는 원격 데이터 소스를 직접 처리한다.

 

실제로 이 아키텍처를 통해 구현한 내용을 살펴보면 사용자에게 웹뷰를 활용한 페이지와 네비게이션 인터페이스를 제공하도록 구성했는데 이 챗봇뷰에서 로그인 정보를 갖기 위해 Repository와 UseCase를 구성해보자

protocol SetNicknameUseCase {
    func setNickname(_ nickname: String)
    func fetchNickname() -> String
}
 
final class DefaultSetNicknameUseCase: SetNicknameUseCase {
    private let repository: AppStorageRepository
    
    init(repository: AppStorageRepository) {
        self.repository = repository
    }
    
    func setNickname(_ nickname: String) {
        repository.setNickname(nickname)
    }
    
    func fetchNickname() -> String {
        repository.fetchNickname()
    }
}

닉네임을 저장하는 메서드와 닉네임을 가져오는 메서드를 프로토콜화 해 구성한 뒤 DefaultSetNicknameUseCase를 통해 구현체를 만들어 닉네임 저장 및 가져오기 로직을 수행하도록 구현한다.

 

해당 작업을 위임할 저장소를 가지고 있고 UseCase는 이를 통해 Repository에 의존하지만 레포지토리의 구체적인 구현은 몰라도 되는 상태이다.

 

레포지토리를 호출해 닉네임 저장 및 닉네임 데이터를 가져오는 로직을 만들어 두면 끝

final class AppStorageRepository: SetNicknameUseCase {
    @AppStorage("nickname") private var nickname: String = ""
    @AppStorage("isloggedin") private var isloggedin: Bool = false

    func setNickname(_ nickname: String) {
        self.nickname = nickname
        self.isloggedin = true
    }
    
    func fetchNickname() -> String {
        return nickname
    }
    
    func fetchIsLoggedIn() -> Bool {
        return isloggedin
    }

    func isLoggedIn() -> Bool {
        return isloggedin
    }
    
    func clearData() {
        self.nickname = ""
        self.isloggedin = false
    }
}

nickname과 isloggedin을 통해 해당 값을 AppStorage에 저장하는 로직이다. SetNicknameUseCase의 프로토콜을 받아 해당 내용을 구현하게 되는데

 

닉네임을 저장하게 되면 isloggedin을 true로 설정하고 nickname을 저장해 가지고 있도록 레포지토리 로직을 구성한다.

그 다음 각 저장한 데이터를 리턴 및 데이터를 지울 수 있는 로직을 추가했다.

import Foundation

final class LoginViewModel: ObservableObject {
    @Published var nickname: String = ""
    @Published var isLoginedIn: Bool = false
    
    private let userSessionUseCase: UserSessionUseCase
    private let setNicknameUseCase: SetNicknameUseCase
    
    init(userSessionUseCase: UserSessionUseCase, setNicknameUseCase: SetNicknameUseCase) {
        self.userSessionUseCase = userSessionUseCase
        self.setNicknameUseCase = setNicknameUseCase
        loadStoredData()
    }
    
    func login() {
        guard !nickname.isEmpty else { return }
        setNicknameUseCase.setNickname(nickname)
        isLoginedIn = true
    }
    
    func loadStoredData() {
        nickname = setNicknameUseCase.fetchNickname()
        isLoginedIn = userSessionUseCase.isLoggedIn()
    }
    
    func clearLoginData() {
        userSessionUseCase.clearSession()
        nickname = ""
        isLoginedIn = false
    }
}

그 다음 로그인을 담당하는 LoginViewModel 내 가져와서 구성해야하는데 먼저 의존성 주입을 통해 비즈니스 로직을 전달할 수 있도록 만들자

init(userSessionUseCase: UserSessionUseCase, setNicknameUseCase: SetNicknameUseCase) {
    self.userSessionUseCase = userSessionUseCase
    self.setNicknameUseCase = setNicknameUseCase
    loadStoredData()
}

레포지토리에 의존하지 않고 UseCase에서만 의존선을 전달받아 재사용성을 높이도록 한다.

func login() {
    guard !nickname.isEmpty else { return }
    setNicknameUseCase.setNickname(nickname)
    isLoginedIn = true
}

로그인이 진행되면 해당 로직을 실행할 수 있도록 메서드를 구현하였는데 닉네임 텍스트필드가 비어있지 않다면 setNicknameUseCase 내 setNickname을 호출해 닉네임을 앱스토리지에 저장하고 isLoginedIn 값도 true로 설정해 저장한다.

func loadStoredData() {
    nickname = setNicknameUseCase.fetchNickname()
    isLoginedIn = userSessionUseCase.isLoggedIn()
}

저장된 로그인 및 닉네임 값을 가져오는 로직은 UseCase에 접근해 해당 메서드를 실행하는 방향으로 구현되는데 위에서 말했던 UseCase에서 비즈니스 로직을 전달받아 ViewModel은 View에 전달하는 역할을 담당하게 되는 것이다.

이렇게 클린아키텍처 로직을 바탕으로 데이터를 저장하고 전달하는 과정을 살펴봤다! 새로운 아키텍처 구조를 통해 프로젝트를 구현하는게 구현 방식을 다른 방향과 관점으로 살펴볼 수 있다는게 참 재밌는 것 같다.

 

앞으로도 클린아키텍처 구조를 통해 프로젝트 구성을 해 나갈 예정이다!

오늘은 여기까지!