관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - Swift] 5. 디자인 패턴 (구조 패턴) - 어댑터 패턴 (Adapter, Wrapper) 본문

Design Pattern (디자인 패턴)

[iOS - Swift] 5. 디자인 패턴 (구조 패턴) - 어댑터 패턴 (Adapter, Wrapper)

jake-kim 2023. 1. 5. 23:11

어댑터 패턴 (Adapter, Wrapper)

  • 현재 A 기능을 사용중일때 이와 유사한 B, C 기능이 계속 생겨나면서, B, C 인터페이스도 A와 동일하게 만드는 것을 목적으로 A의 프로토콜을 준수하는 BAdapter, CAdapter로 만들어서 사용하는쪽에서 코드의 변경을 최소화 하는 방법
  • 가장 대표적인 예는, AuthService를 만들어 놓았을때, 네이버 로그인 및 카카오 로그인이 생겨나면서 이 것들의 인터페이스를 기존의 AuthService와 동일하게 하는 Adapter구현체를 만드는 방법

왜 Adapter 패턴을 사용하는가?

  • 서비스를 사용하는 코드 레이어에서 최소한의 변경사항으로 확장성 있는 기능을 추가하기 위함
  • 코드의 중복을 막을 수 있는 방법

Adapter 패턴의 예시

  • 현재 AuthService 기능이 존재하는 상태
    • AuthServiceType을 준수하고 있으며, 사용하는 쪽에서는 AuthServiceType을 바라보며 login(email:password:completion:) 메소드 사용
    • 사용하는 쪽에서 원하는 데이터는 AuthServiceType인 상태
enum AuthError: Error {
}

struct User {
    let id: String
    let name: String
    let age: Int
}

protocol AuthServiceType {
    func login(email: String, password: String, completion: (Result<(User), AuthError>) -> ())
}

final class AuthService: AuthServiceType {
    func login(email: String, password: String, completion: (Result<(User), AuthError>) -> ()) {
        // login 시도
        completion(.success(()))
    }
}
  • 사용하는 쪽 - AuthServiceType을 준수하는 authService 기능 존재
class ViewController: UIViewController {
    // 에시 편의상 해당 코드에서 인스턴스 생성 (정석은 해당 VC 생성하는 쪽에서 로 인스턴스 주입해주기)
    let authService: AuthServiceType = AuthService()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        authService.login(email: "email", password: "password") {
            print($0)
        }
    }
}
  • 사용하는 쪽에서는 로그인하는데 필요한 정보는 AuthServiceType에 나와있으며, email과 password 뿐이라고만 인식하는 상태라서 AuthServiceType만을 고려
    • 이때 네이버로그인, 카카오로그인이 생겨나게 되면?
    • 네이버, 카카오 모두 다른 User 모델을 사용하고 parameter로 naverKey, kakaoKey를 넘겨야 하는 상황
struct NaverUser {
    let naverID: String
    let name: String
    let email: String
}

class NaverAuthService {
    func login(email: String, password: String, naverKey: String, completion: (Result<(NaverUser), AuthError>) -> ()) {
    }
}

struct KakaoUser {
    let kakaoID: String
    let name: String
    let email: String
}

class KakaoAuthService {
    func login(email: String, password: String, kakaoKey: String, completion: (Result<(KakaoUser), AuthError>) -> ()) {
    }
}
  • 만약 Adapter없이 바로 사용하면 사용하는쪽에서는 NaverUser 키, KakaoUser 키 각각 넣어주어야 하는 상태이며, 둘 다 로그인하는 공통적인 목적이 있지만 코드가 나위어진 상태
  • 기존 코드에서 바라 보고 있던 인터페이스인 AuthService를 준수하는 구현체 NavetAuthAdapter와 KakaoAuthAdapter를 만들어서 사용

Adapter를 사용하여 개선하기

  • Adapter를 사용하지 않은 경우 
    • 사용하는 쪽에서 authService만 따르다가 새로운 naver, kako auth service가 생겨나면서 새로운 값(naverKey, kakaoKey, KakaoUser, NaverUser)에 신경써야 하는 점이 존재
class ViewController: UIViewController {
    let authService: AuthServiceType = AuthService()
    
    // new
    let naverAuthService = NaverAuthService()
    let kakaoAuthService = KakaoAuthService()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        authService.login(email: "email", password: "password") {
            print($0)
        }
        
        naverAuthService.login(email: "email", password: "password", naverKey: "naverKey") {
            print($0)
        }
        
        kakaoAuthService.login(email: "email", password: "password", kakaoKey: "kakaoKey") {
            print($0)
        }
    }
}
  • NaverAuthService, KakaoAuthService 모두 사용하는 쪽에서 관심을 가지던 AuthServiceType을 준수하는 Adapter 구현체를 만들어서 사용
class NaverAuthAdapter: AuthServiceType {
    let naverAuthService = NaverAuthService()
    
    func login(email: String, password: String, completion: (Result<(User), AuthError>) -> ()) {
        naverAuthService.login(email: "", password: "", naverKey: "naverKey") { result in
            completion(.success(.init(id: "", name: "", age: 1)))
        }
    }
}

class KakaoAuthAdapter: AuthServiceType {
    let kakaoAuthService = KakaoAuthService()
    
    func login(email: String, password: String, completion: (Result<(User), AuthError>) -> ()) {
        kakaoAuthService.login(email: "", password: "", kakaoKey: "kakaoKey") { result in
            completion(.success(.init(id: "", name: "", age: 1)))
        }
    }
}
  • 사용하는 쪽
    • 기존 AuthServiceType 그대로 login 요청 메소드 시그니쳐도 같고, user 모델도 기존에 쓰던 모델 그대로 사용하면 되므로 사용하는쪽에서 코드 변경이 최소화 되었으므로 어뎁터 패턴의 목표 달성
class ViewController2: UIViewController {
    let authService: AuthServiceType = AuthService()
    
    // new
    let naverAuthAdapter: AuthServiceType = NaverAuthAdapter()
    let kakaoAuthAdapter: AuthServiceType = KakaoAuthAdapter()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        authService.login(email: "email", password: "password") {
            print($0)
        }
        
        naverAuthAdapter.login(email: "email", password: "password") {
            print($0)
        }
        
        kakaoAuthAdapter.login(email: "email", password: "password") {
            print($0)
        }
    }
}

* 전체 코드: https://github.com/JK0369/ExAdapter

* 참고

https://www.kodeco.com/books/design-patterns-by-tutorials/v3.0/chapters/12-adapter-pattern

https://refactoring.guru/ko/design-patterns/adapter

 

Comments