Adapter Pattern (swift)
Adapter Pattern
아이폰을 생각해보자. 지금의 아이폰은 오디오 단자가 따로 없으며, 만약 유선 헤드폰을 연결하려고 할 경우 유선 헤드폰이 라이트닝 단자를 가지고 있어야 한다. 그러나 상당 수의 유선 헤드폰은 라이트닝 단자를 가지고 있지않다. 이런 상황을 해결하기 위해 오디오 단자를 입력으로 받아 라이트닝 단자로 출력해주는 어댑터란 제품이 존재하며, 서로 호환되지 않는 두 개의 단자를 연결해주는 역할을 한다.
소프트웨어에서의 Adpater Pattern 도 마찬가지로 위와 같은 상황을 해결하기 위해서 고안된 디자인 패턴이다. 예시와 함께 이해해보자.
Authenticate Service
현재 어떤 애플리케이션을 개발 중이고, 이 애플리케이션은 유저 인증을 위해 Firebase Authenticate 를 사용 중이다. 그러나 추후 사용할 인증 서비스가 추가될 수도 있고 (예를 들면 카카오, 구글과 같은), 아니면 사용하는 인증 서비스가 아예 변경될 수도 있다. 즉 애플리케이션은 직접적으로 Firebase 와 같은 인증 서비스에 의존하면 안된다. 물론 변경될 가능성이 전혀 없는 상황에서는 굳이 추상화하지 않고 직접적으로 인증 서비스를 의존해도 될 것이다.
변경에 유연성있게 대응하기 위해 아래와 같이 인증 과정을 추상화시킨 protocol 을 선언하였다.
struct User {
let email: String
let password: String
}
protocol AuthenticatorAdapter {
func login(_ email: String, password: String, completion: @escaping (User?, Error?) -> Void)
}
우리의 애플리케이션은 Firebase Authenticate 를 직접적으로 사용하기 보다는, 위의 protocol 을 사용함으로써 인증 서비스에 직접적인 의존을 피할 것이다.
아래가 Firebase Authenticate 의 현재 알고리즘이라고 생각해보자.
struct FirebaseUser {
let email: String
let nickname: String
let password: String
let token: String
}
class FirebaseAuthenticator {
func login(email: String, password: String, completion: @escaping (FirebaseUser?, Error?) -> Void) {
let user: FirebaseUser = FirebaseUser(email: email, nickname: "temp nickname", password: password, token: "temp token")
completion(user, nil)
}
}
보통의 상황에서 Firebase Authenticate 와 같은 써드파티 라이브러리는 절대 내부 로직을 개발자 임의로 변경할 수 없다. 따라서 우리는 Firebase Authenticator 의 인증 과정이 애플리케이션에서 선언한 Authenticator Adapter 프로토콜과 연결되도록 Concrete Adapter 를 작성해야 한다. Firebase Authenticate 를 사용해 Authenticator Adapter 프로토콜을 준수하는 FirebaseAuthenticatorAdapter 클래스를 작성해보자.
final class FirebaseAuthenticatorAdapter: AuthenticatorAdapter {
// MARK: Firebase Authenticator
private let firebaseAuthenticator: FirebaseAuthenticator = FirebaseAuthenticator()
// MARK: AuthenticatorAdapter function
func login(_ email: String, password: String, completion: @escaping (User?, Error?) -> Void) {
firebaseAuthenticator.login(email: email, password: password) { firebaseUser, error in
// firebase login 이 성공한 경우, FirebaseUser 를 User 로 변환시켜 completion handler 실행
if let firebaseUser = firebaseUser {
completion(User(email: firebaseUser.email, password: firebaseUser.password), nil)
} else if let error = error {
completion(nil ,error)
}
}
}
}
Firebase Authenticator 를 사용해 AuthenticatorAdapter 프로토콜을 준수하는 어댑터 클래스를 선언했다. 지금까지의 구조를 클래스 다이어그램으로 표현하면 아래와 같다.
이제 Authenticator Adapter 를 사용해서 인증 서비스를 직접 사용해보자.
class AuthenticateViewController: UIViewController {
// Authenticator protocol
private var authenticateService: AuthenticatorAdapter!
private var emailTextField: UITextField = UITextField()
private var passwordTextField: UITextField = UITextField()
private var loginButton: UIButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
loginButton.addTarget(self, action: #selector(method), for: .touchUpInside)
}
@objc private func login() {
// 사용자가 아이디와 비밀번호를 입력했는지?
guard let email = emailTextField.text, let password = passwordTextField.text else {
return
}
// Authenticator protocol 을 사용한 로그인
authenticateService.login(email, password: password) { user, error in
if let user = user {
print("로그인 성공!")
} else if let error = error {
print("로그인 실패 \(error.localizedDescription)")
}
}
}
}
지금까지의 클래스 다이어그램은 아래와 같다.
위와 같이 Adapter Pattern 을 사용해서 써드파티 라이브러리인 Firebase 인증 서비스에 직접적으로 의존하지 않고, 추상화된 인증 서비스를 사용함으로써 추후 인증 서비스를 변경하거나 인증 서비스가 추가되더라도 기존 코드를 수정하지 않고 새로운 protocol concrete type 을 추가해서 문제를 해결할 수 있다.
정리
- Adapter pattern 은 변경이 불가능한 Third party library 와 같은 legacy object 를 새로운 인터페이스에 연결해야 될 때 유용하다.