안녕하세요 Niro 🚗 입니다!
🔗 [SOPT / 앱잼] Adapter Pattern 적용한 소셜 로그인 개발 회고록 (1)
위의 첫번째 글처럼 Adapter 패턴 적용에 이어서
'한단계 높힌 추상화 작업을 위한 UseCase 를 적용한 개발 회고' 통해 '어떻게 ViewController 에서 실행 시켰는지' 를 적어볼까합니다.
🔗 TOASTER 다운로드하기
아직 다운받지 않으신 분이 있다면 위의 링크를 통해 사용해보시고 많은 피드백 부탁드립니다!
먼저 첫번째 글을 요약해보자면 Adapter 패턴을 사용해서 각 소셜 로그인 Adapter 를 만들게 되었습니다.
결과적으로 보면
- 다른 방식의 소셜 로그인이라도 Protocol 을 통해 동일한 인터페이스를 제공
- 클라이언트 코드와 비즈니스 로직 간의 결합도를 감소
- 다른 Adapter 를 추가하거나 수정이 유용한 구조
와 같은 장점들을 얻을 수 있었습니다.
효율적인 패턴을 왜 써야하는지 고민을 하고 구현을 했을 때는 정말 뿌듯했고 더 이상 건드릴 필요가 없겠구나 생각했지만....
막상 사용을 해보니 가장 아쉬웠던 부분은
소셜 로그인의 동작을 알고 있어야 한다는 점입니다.
Protocol 의 다형성을 통해 동일한 인터페이스를 공유했지만 해당 Adapter 내부에서 어떠한 메서드를 사용해야하는지, 어떤 데이터를 반환하는지 구체적인 내용을 알아야 합니다.
다른 개발자들이 사용할 수 있게 한층 더 높은 추상화를 시켜 클라이언트가 구체적인 구현에 종속되지않게 구현을 하고자 많은 고민을 하였고
UseCase 를 통해 계층을 분리하고 한층 높은 추상화를 구현하고자 했습니다!
UseCase 의 특징 중 하나로 특정 기능이나 사용 사례에 대한 책임을 갖고 있어야 하는 단일 책임 원칙 준수 를 해야합니다.
소셜 로그인에 대한 UseCase 를 설정할 때 Login 과 Logout 에 대한 기능을 단일 책임을 갖기 위해서 따로 파일을 나누어 주었습니다.
struct LoginUseCase {
let adapter: AuthenticationAdapterProtocol
var adapterType: String {
return adapter.adapterType
}
init(adapter: AuthenticationAdapterProtocol) {
self.adapter = adapter
}
func login() async throws -> SocialLoginTokenModel {
return try await adapter.login()
}
}
struct LogoutUseCase {
let adapter: AuthenticationAdapterProtocol
var adapterType: String {
return adapter.adapterType
}
init(adapter: AuthenticationAdapterProtocol) {
self.adapter = adapter
}
func logout() async throws -> Bool {
return try await adapter.logout()
}
}
각 UseCase 를 보면 Adapter 에서 자주 보였던 AuthenticationAdapterProtocol
을 사용했습니다.
adapter
라는 프로퍼티를 통해 kakao, apple Adapter 를 주입 받고 Protocol 을 통해 상호작용 할 수 있게 됩니다.
UseCase 의 login
, logout
메서드를 실행 시키더라도 주입 받은 Adapter 를 통해 해당 Adapter 의 login
, logout
메서드를 실행시킬 수 있죠!
결과적으로 구체적인 Adapter 에 대한 의존성을 낮출 수 있고 의존성 역전 원칙을 따르는 것으로 볼 수 있습니다.
코드의 유연성과 확장성을 향상 시킬 수 있었고 Adapter 가 추가되거나 변경을 하더라도 각 UseCase 에는 영향을 주지 않는 장점도 얻을 수 있게 됩니다!
의존성 역전 원칙이란?
Login & Logout UseCase 는 구체적인 Adapter 클래스에 직접 의존하는 것이 아닌 Protocol 을 통해 추상화된 인터페이스에 의존하게 됩니다. 즉, 어떤 구체적인 Adapter 가 사용되는지 알 필요가 없게 되고 어떤 종류의 Adapter 라도 사용할 수 있게 됩니다.
자, 한층 더 높은 추상화을 위해 UseCase 를 만들었으니 이제 사용을 해봐야겠죠?
// Kakao
loginUseCase = LoginUseCase(adapter: KakaoAuthenticateAdapter())
// Apple
loginUseCase = LoginUseCase(adapter: AppleAuthenticateAdapter())
LoginVC 에서 각 소셜 로그인 button 의 action 메서드에 위와 같이 선언해주었습니다.
아주 간단하죠?
그 다음은 loginUseCase 프로퍼티를 통해 login 메서드를 실행해야 합니다!
func attemptLogin() async throws -> SocialLoginTokenModel {
guard let loginUseCase = self.loginUseCase else {
throw LoginError.notSettingUsecase
}
do {
let result = try await loginUseCase.login()
return result
} catch let error {
print("Login Error:", error.localizedDescription)
throw LoginError.failedReceiveToken
}
}
각 Button Action 에서 비동기 처리를 위한 코드가 공통적으로 사용되어 위와 같이 따로 attemptLogin
메서드를 만들어 주었습니다.
Adapter 를 잘 선언했는지 guard let
으로 확인을 하고 do catch
구문에서 결과 또는 에러를 반환 받을 수 있게 작성하였습니다.
loginUseCase.login()
에서 login
메서드는 AuthenticationAdapterProtocol
보았던 메서드이며 우리는 어떤 Adpater 를 할당하더라도 똑같은 코드인 login
메서드만 호출하면 해당 Adapter 구현부 내의 login 메서드까지 실행이 되죠!
사실 굉장히 많은 고민을 했던 부분입니다.
Adapter 를 주입 받아 로그인, 로그아웃을 구현하고
User 정보를 갖고 있는 Social Login 을 담당하는 싱글톤 객체를 만들어 관리를 할까?
라는 생각도 해보았습니다.
싱글톤 객체를 만들면 전역에서 호출하기도 편하고 User 정보를 공유할 수 있는 장점이 있기 때문입니다.
하지만!
프로젝트 내부에서 User 정보를 다양하게 필요하지 않으며 각 VC 마다 API 를 통해 User 정보를 전달 받고 있어 이중으로 User 정보를 갖고 있을 필요가 없다!
라고 생각하여 싱글톤 객체로 구현하지 않았습니다...
사실 Adapter Pattern 만을 사용해도 전혀 문제는 없습니다!
다른 개발자가 로그인 & 인증 관련 작업을 해야할 때 Adapter Pattern 으로 구현되어 있다는 것을 알아야 하기 떄문에 한번 더 추상화를 하게 되면 세부 사항을 알지 못하더라도 다른 개발자들이 편하게 사용할 수 있을거라 생각했습니다.
글의 시작부터 여러 근거를 통해 UseCase 의 특징인 하나의 기능을 독립적으로 갖고 있고 User 의 사용패턴에 맞춰 구현할 수 있어 더욱 직관적으로 사용할 수 있다는 장점으로 UseCase 까지 구현하게 되었습니다.
Adapter Pattern 과 UseCase 를 활용한 소셜 로그인을 구현했던 개발 경험을 회고록 2편에 걸쳐 작성했습니다.
글을 적으면서 다시 복기해 볼 수 있는 시간이었고 기억에 남을 수 있는 좋은 시간이였습니다.
마지막 3번째 편으로 로그인을 구현하면서 느꼈던 점을 적어볼까 합니다.
긴글 읽어주셔서 감사하고 많은 피드백과 질문 대환영입니다!
다음 편도 기대해주세요!