- Adaptor Pattern (구조적 디자인패턴)2025년 03월 24일
- 2료일
- 작성자
- 2025.03.24.:18
요즘 디자인 패턴에 관한 개발 책을 읽고 있다. 취준 하며.. 그래서 이제 차차 인상 깊고 적용할만한 패턴을 정리할 계획이다. 우선 오늘은 어댑터 패턴이다.
Adaptor Pattern이란?
서로 다른 인터페이스를 가진 두 시스템을 연결하는 구조적 디자인 패턴입니다.
기존 클래스의 코드를 수정하지 않고도 새로운 인터페이스 맞춰 사용할 수 있도록 중간에서 "어댑터"역할을 하는 객체를 제공하는 것!
왜 어뎁터냐면 일본에 가면 한국에서 사용하는 콘센트와 달라서 꽂을 수 없다. 이때 사용하는게 어댑터이다. 비슷한 개념입니당ㅎ
등장배경
- 호환성 문제
: 서로 다른 인터페이스를 가진 모듈이 협력해야 하는 상황이 많다. 기존 레거시 코드와 새로운 시스템 통합하거나 서로 다른 라이브러리 함께 사용할때 인터페이스가 맞지 않는 경우가 많다.- 재사용성
: 기존 코드를 새로운 환경에서 활용하고 싶을때, 그 때 입맞에 맞추기 위해 기존 코드를 수정하는것은 위험하다!! 의존하고 있는 다른 객체에 영향을 주기 때문. 어댑터 패턴은 기존코드에 손대지 않고도 새로운 요구사항에 맞춰 재사용할 수 있게 해줍니다.-> 기존 시스템과 새로운 요구사항 사이 간극을 최소화하기 위해 등장(결국 확장성)
핵심 구성 요소
1. 타겟: 클라이언트가 사용하는 인터페이스(일본의 규격)
2. 어뎁티: 레거시 객체로 호환성 문제가 있는 인터페이스(한국 규격충전기)
3. 어뎁터: 어뎁티의 인터페이스를 타겟 인터페이스로 변환
4. 클라이언트: 타겟 인터페이스를 사용하는 코드. 기존 시스템의 규격(타겟)을 채택하고 구현하고 작동하는 객체. (일본의 전기 규격 시스템)
어뎁터를 사용하는 객체는 프로토콜(타겟)에 의존하고 레거시 객체(어댑티)는 프로토콜이 생기기 전에 존재하던 객체를 의미한다. 어뎁터는 프로토콜을 구현하고 레거시 객체에 호출을 위임한다.
그래서 언제 쓰는데?
ex1) AuthService를 만들었을 때 네이버, 카카오,애플 등 모든 인터페이스를 기존의 AuthService와 동일하게 하는 Adaptor를 구현.
// 앱에서 사용하는 인증 관리자 class AuthManager { func loginWithKakao() { // 카카오 SDK에 직접 의존 KakaoSDK.shared.login { token, error in if let token = token { self.handleKakaoLogin(token: token) } else { self.handleLoginError(error) } } } func loginWithApple() { // 애플 로그인에 직접 의존 let appleIDProvider = ASAuthorizationAppleIDProvider() let request = appleIDProvider.createRequest() request.requestedScopes = [.fullName, .email] let authorizationController = ASAuthorizationController(authorizationRequests: [request]) authorizationController.delegate = self authorizationController.performRequests() } func loginWithNaver() { // 네이버 SDK에 직접 의존 } // 각 서비스별 콜백 메서드들... private func handleKakaoLogin(token: String) { /* ... */ } private func handleAppleLogin(credential: ASAuthorizationCredential) { /* ... */ } private func handleNaverLogin(isSuccess: Bool) { /* ... */ } private func handleLoginError(_ error: Error?) { /* ... */ } } // ASAuthorizationControllerDelegate 구현 extension AuthManager: ASAuthorizationControllerDelegate { } // 네이버 로그인 델리게이트 구현 extension AuthManager: NaverThirdPartyLoginConnectionDelegate { }
실제 이런식으로 코드를 짜겟죠. 저도 그렇고. 아니 뭐 분리해서 짤수 있긴한데 쨋든 새로운 인증 서비스 추가시 AuthManager클래스를 직접 수정해야한다. 또한 현재는 SRP도 위반하고 있다. 그러면 이걸 어떻게 어댑터 패턴을 적용할 수 있을까?
// 1. 타겟 인터페이스 - 모든 인증 서비스가 준수해야 할 인터페이스 protocol AuthServiceType { func login(completion: @escaping (Result<UserCredential, AuthError>) -> Void) func logout(completion: @escaping (Result<Void, AuthError>) -> Void) var currentUser: User? { get } } // 통합된 사용자 자격 증명 모델 struct UserCredential { let id: String let token: String let name: String? let email: String? let provider: AuthProvider } enum AuthProvider { case kakao, apple, naver } enum AuthError: Error { case cancelled case failed(Error) case invalidCredential case notAuthenticated } // 2. 어댑터 구현 - 각 서비스별 어댑터 // 카카오 어댑터 class KakaoAuthAdapter: AuthServiceType { private let kakaoSDK = KakaoSDK.shared var currentUser: User? { guard let token = kakaoSDK.tokenManager.getToken() else { return nil } return User(id: token.userID, provider: .kakao) } func login(completion: @escaping (Result<UserCredential, AuthError>) -> Void) { } func logout(completion: @escaping (Result<Void, AuthError>) -> Void) { kakaoSDK.logout { error in if let error = error { completion(.failure(.failed(error))) } else { completion(.success(())) } } } private func isUserCancelledError(_ error: Error) -> Bool { // 카카오 SDK의 사용자 취소 에러 확인 로직 return false // 실제 구현 필요 } } // 애플 어댑터 class AppleAuthAdapter: NSObject, AuthServiceType { private var loginCompletion: ((Result<UserCredential, AuthError>) -> Void)? var currentUser: User? { return nil } func login(completion: @escaping (Result<UserCredential, AuthError>) -> Void) { } func logout(completion: @escaping (Result<Void, AuthError>) -> Void) { // 애플은 명시적 로그아웃 API가 없으므로 로컬 토큰만 제거 // 키체인에서 토큰 삭제 로직 completion(.success(())) } } // 애플 로그인 델리게이트 구현 extension AppleAuthAdapter: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { } // 네이버 어댑터 class NaverAuthAdapter: NSObject, AuthServiceType { private let naverLogin = NaverThirdPartyLoginConnection.getSharedInstance() private var loginCompletion: ((Result<UserCredential, AuthError>) -> Void)? override init() { super.init() naverLogin?.delegate = self } var currentUser: User? { // 저장된 사용자 정보가 있으면 반환 // 실제 구현 필요 return nil } func login(completion: @escaping (Result<UserCredential, AuthError>) -> Void) { self.loginCompletion = completion naverLogin?.requestThirdPartyLogin() } func logout(completion: @escaping (Result<Void, AuthError>) -> Void) { naverLogin?.requestDeleteToken() completion(.success(())) } // 네이버 API로 사용자 정보 요청 private func fetchUserProfile() { } } // 네이버 로그인 델리게이트 구현 extension NaverAuthAdapter: NaverThirdPartyLoginConnectionDelegate { } // 3. 통합 인증 관리자 - 클라이언트 코드 class AuthManager { private var authServices: [AuthProvider: AuthServiceType] = [:] private var currentProvider: AuthProvider? init() { // 서비스 어댑터 등록 registerAuthService(.kakao, service: KakaoAuthAdapter()) registerAuthService(.apple, service: AppleAuthAdapter()) registerAuthService(.naver, service: NaverAuthAdapter()) } func registerAuthService(_ provider: AuthProvider, service: AuthServiceType) { authServices[provider] = service } func login(with provider: AuthProvider, completion: @escaping (Result<User, AuthError>) -> Void) { guard let authService = authServices[provider] else { completion(.failure(.invalidCredential)) return } authService.login { [weak self] result in switch result { case .success(let credential): // 사용자 정보 저장 및 세션 관리 let user = User(credential: credential) self?.currentProvider = provider // 로그인 정보 저장 로직 completion(.success(user)) case .failure(let error): completion(.failure(error)) } } } func logout(completion: @escaping (Result<Void, AuthError>) -> Void) { guard let provider = currentProvider, let authService = authServices[provider] else { completion(.failure(.notAuthenticated)) return } authService.logout { [weak self] result in if case .success = result { self?.currentProvider = nil // 로그인 정보 삭제 로직 } completion(result) } } var currentUser: User? { guard let provider = currentProvider, let authService = authServices[provider] else { return nil } return authService.currentUser } } }
이렇게 되면 기존의 AuthManager클래스가 모든 인증 로직을 처리했던거에 비해 적용 후 서비스 관리와 공통 인터페이스 사용에만 집중 할 수 있다. 각 인증 서비스를 별도의 어댑터 클래스로 캡슐화 했기 때문.
새 인증 서비스를 추가하려면 AuthManager를 건드렸던것과 달리 적용 후 새 서비스는 기존 코드 수정없이 새 어뎁터클래스 구현하면 된다. 그 후 registorAuthService를 통해 등록해줄 수 있다.
추가로 현재 내가 프로젝트에서 Alamofire로 네트워킹을 하고 있다. 만약 내가 라이브러리를 뭐 moya 등 다른것으로 마이그레이션 할 계획이 있다면 어댑터 패턴이 용이할거 같다.
protocol NetworkService { func request(endpoint: String, completion: @escaping (Result<Data, Error>) -> Void) } class AlamofireAdapter: NetworkService { func request(endpoint: String, completion: @escaping (Result<Data, Error>) -> Void) { // Alamofire로 요청 구현 } } class MoyaAdapter: NetworkService { func request(endpoint: String, completion: @escaping (Result<Data, Error>) -> Void) { // Moya로 요청 구현 } }
모 이런식으로 인터페이스타겟을 정의하고 각각의 어댑터로 구현해줄수 있다. 클라이언트는 동일한 인터페이스를 사용하니 마이그레이션이 훨씬 수월해질거같다.
'디자인패턴' 카테고리의 다른 글
파사드 패턴(Facade Pattern) (0) 2025.03.25 팩토리 패턴 (0) 2025.03.25 다음글이전글이전 글이 없습니다.댓글