[Swift 디자인 패턴] Adapter Pattern (어댑터) - 디자인 패턴 공부 7
안녕하세요 Pingu입니다.🐧
지난 글에서는 구조 패턴에 대해서 간단하게 알아봤는데요, 이번 글에서는 여러 가지 구조 패턴 중 하나인 Adapter Pattern(어댑터)에 대해 알아보도록 하겠습니다.
어댑터 패턴이란?
어댑터 패턴은 서로 다른 인터페이스를 가진 객체를 함께 사용할 수 있도록 해주는 디자인 패턴입니다.
이러한 어댑터 패턴에는 Object Adapter, Class Adapter가 있습니다.
Object Adapter
- Client는 프로그램의 기존 로직을 포함하는 클래스입니다.
- Target은 다른 클래스가 현재 로직에 포함 되려면 따라야 하는 프로토콜입니다. (Client가 사용 중인 인터페이스)
- Adaptee는 현재 존재하는 클래스가 아닌 외부에서 가지고온 유용한 클래스입니다. 따라서 인터페이스가 다르기 때문에 바로 사용할 수가 없습니다.
- Adapter가 이러한 Client, Adaptee를 모두 사용할 수 있도록 도와줍니다. Adapter는 Client, Adaptee 객체를 모두 다룰 수 있고 Adaptee 객체를 Client 인터페이스로 래핑 하거나 Client 객체를 Adaptee 클래스가 사용할 수 있는 형식으로 변환합니다.
- 이러한 Adapter를 사용하면 기존 Client 코드를 수정하지 않고도 새로운 클래스들을 사용할 수 있습니다.
Class Adapter
다중 상속을 지원하는 언어에서 어댑터는 위와 같이 여러 개의 인터페이스를 상속하여 여러 클래스에서 동작 가능한 어댑터를 만들 수 있습니다. 이렇게 되면 아까의 Object Adapter와는 다르게 객체를 래핑하여 변환할 필요 없이 어댑터 내부에서 처리하게 됩니다.
어댑터 패턴은 언제 쓰나요?
예를 들어 XML 파일을 사용하는 코드가 있다고 해볼게요. 해당 기능이 너무 좋아서 코드를 가지고 새로운 기능을 만드려고 하는데 새로운 기능은 JSON 파일로 작동하게 만들어야 한다면 기존 코드를 쓰지 못할거에요. 그렇다고 이걸 모조리 다시 만들자니 기존 코드가 너무 아까운 상황이죠..
이럴 때 어댑터 패턴을 사용할 수 있습니다! 어댑터를 만들고 XML파일을 처리하는 코드와 JSON을 처리하는 코드를 함께 사용할 수 있도록 하면 됩니다. 만약 어댑터가 JSON 파일을 처리해서 XML 파일로 변환해주는 작업을 해준다면 JSON 파일도 기존 코드로 처리할 수 있게 되겠죠?
이렇게 어댑터는 데이터를 다양한 형식으로 변환할 때도 사용하고, 서로 다른 인터페이스를 가진 객체가 함께 사용할 수 있도록 할 때도 사용할 수 있습니다. 서로 다른 인터페이스를 가진 클래스가 어댑터를 사용하여 함께 사용하는 방법은 아래와 같아요.
- 예를 들어 두 개의 객체 A, B가 존재할 때 어댑터가 A와 동일한 인터페이스를 갖는다고 가정합니다.
- 어댑터가 A객체와 동일한 인터페이스를 사용하므로 A 객체는 어댑터의 메서드들을 호출 할 수 있습니다.
- A 객체가 어댑터의 메서드를 호출하면 어댑터가 이를 B 객체가 사용할 수 있는 정보로 변환해줍니다.
- 이러한 과정은 단방향일 수도 있지만, 양방향일 수도 있어요.
어댑터 패턴의 결과
Object Adapter
- 하나의 어댑터에 여러개의 어댑터가 존재하여 다양한 하위 클래스와 함께 작동하게 할 수 있습니다.
- Adaptee의 메서드를 override 하기 힘들어집니다. Adaptee를 서브 클래싱 하고 Adapter가 Adaptee 자체가 아닌 서브 클래스를 참조하도록 해야 합니다.
Class Adapter
- Adaptee 클래스를 Target에 맞게 변환합니다. 하지만 하위 클래스가 존재하는 경우 클래스 어댑터는 작동하지 않을 수 있어요.
- Adapter가 Adaptee를 상속받은 하위 클래스 이므로 Adaptee의 메서드들을 쉽게 override 할 수 있습니다.
예제
그럼 이제 Swift로 간단하게 어댑터 패턴을 구현해보겠습니다.
요즘 로그인을 구현 할 때 하나의 서비스에서 여러 플랫폼 계정으로 로그인하는 기능이 많은데요, 분명 서로 다른 플랫폼이라 인터페이스가 다를 텐데 동일한 서비스를 위한 계정으로 사용할 수 있어요.
이러한 상황을 어댑터 패턴으로 간단하게 구현해보겠습니다.
// 기존 로그인 서비스 프로토콜
protocol LoginService {
func login(email: String,
password: String,
success: @escaping (User?, Token?) -> Void,
failure: @escaping (LoginError?) -> Void)
}
struct User {
let email: String
let password: String
}
struct Token {
let value: String
}
struct LoginError: Error {
var errorMessage: String
}
class NormalLogin: LoginService {
func login(email: String, password: String, success: @escaping (User?, Token?) -> Void, failure: @escaping (LoginError?) -> Void) {
if !email.hasSuffix("normal.com") {
failure(LoginError(errorMessage: "email 에러"))
return
}
let token = Token(value: "아무도 모르는 Normal 비밀 토큰")
let user = User(email: email, password: password)
success(user, token)
}
}
기존에는 위와 같이 로그인을 하고 있었습니다. email, password를 가지고 User, Token 객체를 만들어 로그인을 처리했죠. 근데 이제 네이버, 카카오 로그인을 추가하고 싶어 진 거예요.
struct NaverAccount {
var email: String
var password: String
var naverToken: String
}
class NaverAccountLogin {
func login(email: String,
password: String,
completion: @escaping(NaverAccount?, LoginError?) -> Void) {
if !email.hasSuffix("naver.com") {
completion(nil, LoginError(errorMessage: "email 에러"))
return
}
let token = "아무도 모르는 Naver 비밀 토큰"
let user = NaverAccount(email: email, password: password, naverToken: token)
completion(user, nil)
}
}
struct KakaoAccount {
var email: String
var password: String
var kakaoToken: String
}
class KakaoAccountLogin {
func login(email: String,
password: String,
completion: @escaping(KakaoAccount?, LoginError?) -> Void) {
if !email.hasSuffix("kakao.com") {
completion(nil, LoginError(errorMessage: "email 에러"))
return
}
let token = "아무도 모르는 Kakao 비밀 토큰"
let user = KakaoAccount(email: email, password: password, kakaoToken: token)
completion(user, nil)
}
}
네이버, 카카오 로그인은 위와 같이 생겼다고 해볼게요.
지금 볼 수 있듯이 기존 로그인 서비스와는 다른 형태라 현재 바로 사용할 수는 없어요.
이럴 때 기존 로그인 서비스 프로토콜을 채택한 Adapter를 사용하여 서비스를 확장할 수 있습니다.
// Adapter
class NaverAccountAdapter: LoginService {
private var authenticator = NaverAccountLogin()
func login(email: String,
password: String,
success: @escaping (User?, Token?) -> Void,
failure: @escaping (LoginError?) -> Void) {
authenticator.login(email: email, password: password) { (naverAccount, error) in
guard let naverAccount = naverAccount else {
failure(error)
return
}
let user = User(email: naverAccount.email,
password: naverAccount.password)
let token = Token(value: naverAccount.naverToken)
success(user, token)
}
}
}
이렇게 네이버 로그인 어댑터를 만들었습니다.
기존 서비스의 프로토콜인 LoginService를 채택한 어댑터를 만들어 네이버 로그인 서비스를 기존 서비스처럼 사용할 수 있게 되었어요.
간단하게 로그인을 하는 듯한 로직을 만들어서 테스트해보면 위와 같이 잘 처리되는 것을 볼 수 있습니다.
위와 같이 카카오 로그인인데 네이버 이메일을 넣으면 에러를 발생하는 것도 볼 수 있어요.
이렇게 구조 패턴 중 하나인 어댑터 패턴을 알아보고 간단하게 직접 구현도 해봤습니다.
혹시라도 틀린 부분이 있다면 알려주시면 감사하겠습니다.
전체 코드는 여기에서 볼 수 있습니다.
감사합니다.