클린 아키텍처+MVVM+Coordinator 패턴을 이용한 다중 소셜 로그인 구현

요즘 앱 로그인 할때 대부분 소셜 로그인을 사용합니다. 토이 프로젝트를 진행 할 때 소셜로그인을 넣으려고 했는데, 각 소셜 로그인마다 ViewModel에 googleLogin, kakaoLogin, appleLogin..등등 각 로그인 함수를 넣어서 구현하는것은 SOLID 원칙에도 위배되고, 각 케이스 마다 비슷비슷한 코드를 써야하기에 비효율적이라 생각해 클린 아키텍처를 사용해서 구현해 보게되었습니다. 

우선은 전체적인 Coordinator 패턴 흐름도입니다. 

AppCoordinator의 start에서 로그인을 했는지 안했는지 판단하고 로그인을 했으면 오토로그인, 하지 않는 경우에는 로그인뷰를 보여주게 됩니다.

여기서 LoginRepository는 각각 구글, 카카오, 애플 로그인 오브젝트를 말합니다. 각 오브젝트들은 LoginRepository를 채택하고 있습니다. ViewModel에서 UseCase를 사용하고, UseCase에서 LoginRepository를 사용해서 로그인 과정을 진행하게 됩니다. 도식화 하면 아래와 같은 관계가 됩니다.

Domain 계층

Domain 계층에는 UseCase와 프로토콜로 정의를 해논 Repository, 로그인한 User의 정보를 담을 Entitiy가 포함되어있습니다.

protocol LoginRepository {
    typealias loginResult = (Result<User, Error>)->()
    func login(vc: UIViewController?, completion: @escaping loginResult)
    func logout(completion: @escaping (Bool)->())
    func autoLogin(completion: @escaping loginResult)
    func getUserInfo(vc: UIViewController?, completion: @escaping(User)->())
}
struct User {
    let email: String
}
protocol LoginUseCase {
    func excute(loginRepository: LoginRepository, vc: UIViewController?, completion: @escaping (Result<User, Error>)->())
}

protocol LogoutUseCase {
    func excute(completion: @escaping (Bool)->())
}

final class DefaultLoginUseCase: LoginUseCase {
    private var loginRepository: LoginRepository?

    func excute(loginRepository: LoginRepository, vc: UIViewController? = nil, completion: @escaping (Result<User, Error>)->()) {
        self.loginRepository = loginRepository
        self.loginRepository?.login(vc: vc, completion: { result in
            switch result {
            case .success(let user):
                completion(.success(user))
            case .failure(let error):
                completion(.failure(error))
            }
        })
    }
}

final class DefaultLogoutUseCase: LogoutUseCase {
    private var loginRepository: LoginRepository?
    
    init(loginRepository: LoginRepository? = nil) {
        self.loginRepository = loginRepository
    }
    
    func excute(completion: @escaping (Bool)->()) {
        loginRepository?.logout(completion: { isLogOutSuccess in
            if isLogOutSuccess {
                completion(true)
            } else {
                completion(false)
            }
        })
    }
    
}

Presentation

Presentation 계층은 각 ViewController와 Coordinator, ViewModel을 가지고 있습니다. 각 로그인 오브젝트들은 LoginViewController가 버튼을 누를때 해당 로그인 오브젝트를 로그인 함수에 넣어서 로그인을 진행하는 식으로 구현하였습니다. 그리고 ViewModel은 어떤 

final class LoginViewController: UIViewController, StoryboardInstatiable {
    
    var loginViewModel: LoginViewModel?
    private var kakaoLoginRepository: KakaoLoginRepository?
    private var googleLoginRepository: GoogleLoginRepository?
    
    @IBOutlet weak var googleLoginButton: GIDSignInButton!
    @IBOutlet weak var loginButtonStackView: UIStackView!
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }
    
    static func create(loginViewModel: LoginViewModel, kakaoLoginRepository: KakaoLoginRepository, googleLoginRepository: GoogleLoginRepository) -> LoginViewController {
        let view = LoginViewController.instantiateViewController()
        view.loginViewModel = loginViewModel
        view.kakaoLoginRepository = kakaoLoginRepository
        view.googleLoginRepository = googleLoginRepository
        return view
    }
    
    @IBAction func kakaoLoginButtonPressed(_ sender: UIButton) {
        guard let kakaoLoginRepository = kakaoLoginRepository else {return}
        loginViewModel?.didLogin(loginRepository: kakaoLoginRepository, vc: nil)
    }
    
    
    @IBAction func googleLoginButtonPressed(_ sender: UIButton) {
        guard let googleLoginRepository = googleLoginRepository else {return}
        loginViewModel?.didLogin(loginRepository: googleLoginRepository, vc: self)
    }
}
protocol LoginViewModelDelegate {
    func successLogin(loginRepository: LoginRepository)
}

protocol LoginViewModelInput {
    func didLogin(loginRepository: LoginRepository, vc: UIViewController?)
}

protocol LoginViewModelOutput {
    
}

protocol LoginViewModel: LoginViewModelInput, LoginViewModelOutput {}

final class DefaultLoginViewModel {
    
    private let loginUseCase: LoginUseCase
    private var loginRepository: LoginRepository?
    var delegate: LoginViewModelDelegate?
    
    init(loginUseCase: LoginUseCase, delegate: LoginViewModelDelegate) {
        self.loginUseCase = loginUseCase
        self.delegate = delegate
    }
}

extension DefaultLoginViewModel: LoginViewModel {
    
    func didLogin(loginRepository: LoginRepository, vc: UIViewController? = nil) {
        loginUseCase.excute(loginRepository: loginRepository, vc: vc) { [weak self] result in
            switch result {
            case .success(let user):
                UserDefaults.standard.set(true, forKey: "isLoggedIn")
                UserDefaults.standard.set("\(loginRepository.self)".split(separator: ".").last! ?? "", forKey: "lastLoginRepository")
                self?.delegate?.successLogin(loginRepository: loginRepository)
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }
    
}

이런식으로 각 버튼에 대해 LoginRepository를 채택하고 있는 로그인 오브젝트를 넣기만 한다면, 중복된 코드 없이 동일하게 로그인을 진행 할 수 있습니다.

실행영상

구글, 카카오 둘다 정상적으로 로그인, 로그아웃이 되는것을 볼 수 있습니다. 자세한 코드는 깃허브를 참고해 주시길 바랍니다.

 

 

GitHub - Kim-Junhwan/PicterestGallery

Contribute to Kim-Junhwan/PicterestGallery development by creating an account on GitHub.

github.com