배경
글로벌 앱이 도입됨에 따라 소셜 로그인이 추가되었다.
앱스토어 심사 지침에 따라 소셜 로그인을 추가 할 경우 애플 로그인은 필수로 적용해야 한다.
구글 SDK도 적용 해 놨지만, 지원 종료된다는 얘기가 있어서 웹뷰로도 대응해놨다.
그래서 최근에 읽은 '객체지향의 사실과 오해'를 보며 이해한 수준에서 설계해봤다..!
고려사항
두 로그인 방식에 있어서 결과적으로 필요한 데이터는 무엇일까?
클라이언트에서는 로그인 후 돌아오는 token을 디바이스에 저장하지 않고 서버측에 바로 넘기기만 하면 된다.
여기서 변하는 것과 변하지 않는 것을 생각해봤다.
우선 변하지 않는 부분은
각각의 로그인 방식 모두 베이스가 될 ViewController를 갖고 있어야 한다.
또한 로그인을 수행 할 authenticate() 함수(행위)가 필요하다.
이 함수는 인터페이스 즉, 외부와 연결된 public 한 함수다.
또한 각 소셜 로그인마다 뱉어낼 Custom Error Message가 필요하다.
변하는 부분은 무엇일까?
당연히 각 소셜 로그인이 수행할 authenticate 함수의 내부 '구현'이다.
객체지향 관점으로 보면 다형성을 생각할 수 있겠다.
또한 RxSwift를 사용했기에 비동기를 동기처럼 보이는 코드가 필요했다.
왜? 스트림을 이용해서 이벤트들이 쭉 연결되기 때문!
이벤트들이 연결된다는 의미는
1. 유저가 특정 로그인 인터랙션을 전달
2. 특정 로그인의 authenticate 함수 실행
3. 이 때 Observable 스트림 생성
4-1. 성공했다면 얻은 토큰을 유즈케이스에 담음
4-2. 실패했다면 로그인 실패 메세지를 담아 실패했다는 UI를 보여줌
5. 유즈케이스는 토큰을 받아 레포지토리에 전달
6. 레포지토리에서 서버에 토큰값을 전달
이런식으로 흐름이 연결되는 걸 목표로 잡았다.
구현
우선 외부 공용 인터페이스로 사용 될 AuthService를 만들어보자.
import UIKit
import RxSwift
protocol AuthService: AnyObject {
var viewController: UIViewController? { get }
func authenticate() -> Observable<Result<String, AuthError>>
}
struct AuthError: Error {
let errorMessage: String
}
AuthService라는 인증을 담당하는 '역할'을 먼저 생각했다.
다음으로는 각각의 소셜 로그인을 수행할 '책임'이 있는 클래스를 구현해보자.
final class GoogleAuthService: AuthService {
public weak var viewController: UIViewController?
private let signInConfiguration = GIDConfiguration(clientID: GoogleServiceInfo.clientId, serverClientID: GoogleServiceInfo.serverId)
init() {}
public func authenticate() -> Observable<Result<String, AuthError>> {
return Observable.create { [weak self] observer -> Disposable in
guard let self = self, let viewController = self.viewController else { return Disposables.create() }
GIDSignIn.sharedInstance.signIn(with: self.signInConfiguration, presenting: viewController
) { user, error in
guard error == nil else { return observer.onNext(.failure(AuthError(errorMessage: "구글 로그인에 실패했습니다."))) }
return observer.onNext(.success(user?.serverAuthCode ?? ""))
}
return Disposables.create()
}
}
}
소셜 인증은 ViewModel 클래스에서 처리한다.
그 때 ViewModel 객체가 각각의 소셜 로그인 객체에게 원하는 행위는 무엇인지 다시 상기해보자.
'인증하고 토큰 받아오기'이다.
여기서 행위는 너무 구체적이어서도 안되고, 너무 추상적이어도 안된다.
마찬가지로 Apple Login의 경우도 methodInvoked를 사용해 Observable을 반환하도록 만들어보자.
final class AppleAuthService: NSObject, AuthService, ASAuthorizationControllerDelegate {
public weak var viewController: UIViewController?
private let disposeBag = DisposeBag()
override init() {}
public func authenticate() -> Observable<Result<String, AuthError>> {
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.rx.delegate.setForwardToDelegate(self, retainDelegate: false)
authorizationController.performRequests()
return authorizationController.rx.appleLoginResult
}
}
methodInvoked를 사용한 구체적인 구현은 제외
이제 실제 ViewModel 클래스가 특정 User Interaction을 받았을 때 수행 할 로직을 만들어보자.
AuthService 프로토콜을 사용하여 역할을 만들었고, ViewModel 클래스는 AuthService에게 어떤 클래스든 상관없이 token을 뱉어주면 만족할 것이다!
let authService: AuthService = currentSocialData.type == .APPLE_SDK ? appleAuthService : googleAuthService
authService
.authenticate()
.bind { [weak self] result in
switch result {
case .success(let token):
self?.addSocialInfo(type: currentSocialData.type, token: token)
case .failure(let errorMessage):
self?.onShowToast?(message: errorMessage)
}
}
.disposed(by: disposeBag)
이렇게 구성하면 어떤 인터랙션이 들어와도 ViewModel 클래스는 AuthService가 '어떻게' 토큰을 만들어오는지는 관심을 끌 수 있다.
단지, 토큰을 넘겨오는 AuthService라는 역할을 채택한 '어떤' 클래스가 '책임'을 완수했다는 사실만 알 뿐이다.
최근의 프로그래밍 패러다임은 객체지향이다.
절차형이니, 함수형이니 하는 패러다임들도 있지만
생각해보면 프로그래밍의 패러다임은 기존의 패러다임이 사라지는 게 아닌, 서로의 장점만을 뽑아 발전하는 형태라고 볼 수 있다.
- 오브젝트 인용문
댓글