[Design Pattern] 어댑터 패턴 (Adapter Pattern)

Martin Kim·2022년 3월 20일
0

디자인패턴

목록 보기
5/6

의도

  • 클래스의 인터페이스를 사용자가 기대하는 인터페이스 형태로 적응(변환)시킨다. 서로 일치하지 않는 인터페이스들을 갖는 클래스들을 함께 동작시킨다.

참여자

  • Target: 사용자가 사용할 응용 분야에 종속적인 인터페이스를 정의하는 클래스
  • Client: Target 인터페이스를 만족하는 객체와 동작할 대상
  • Adaptee: 인터페이스의 적응이 필요한 기존 인터페이스를 정의하는 클래스
  • Adapter: Target 인터페이스에 Adaptee의 인터페이스를 적응시키는 클래스

예시

주식 시장 모니터링 앱을 만들고 있다고 가정하자. 이 앱은 XML 형식의 여러 소스에서 주식 데이터를 다운로드한 다음 사용자에게 보기 좋은 차트와 다이어그램을 표시한다.

앱과 호환되지 않는 형식의 데이터를 예상하기 때문에 분석 라이브러리를 "있는 그대로" 사용할 수 없다.

어느 시점에서 스마트 타사 분석 라이브러리를 통합하여 앱을 개선하기로 결정했다. 하지만 문제가 발생했다. 분석 라이브러리는 오직 JSON 형식의 데이터만을 받아들여 작동하는 것이다.

XML과 함께 작동하도록 라이브러리를 변경 할 수 있다. 그러나 이것은 라이브러리에 의존하는 일부 기존 코드를 손상시킬 수 있다. 더군다나 처음부터 라이브러리의 소스 코드에 액세스 할 수는 없어 이 접근 방식은 불가능할 수 있다.


해결방법

Adapter를 생성한다. 이 객체는 한 객체의 인터페이스를 다른 객체가 이해할 수 있도록 변환하는 특수 객체이다.

Adapter는 뒤에서 일어나는 변환의 복잡성을 숨기기 위해 객체 중 하나를 래핑한다. 래핑된 객체는 어댑터를 인식하지도 못한다. 예를 들어 미터 및 킬로미터 단위로 작동하는 객체를 피트/마일과 같은 영국식 단위로 변환하는 어댑터로 래핑할 수 있다.

Adapter는 데이터를 다양한 형식으로 변환할 수 있을 뿐만 아니라 다양한 인터페이스를 가진 객체가 협업하는 데 도움이 될 수 있다. 작동 방식은 다음과 같다.

  1. Adapter는 기존 객체 중 하나와 호환되는 인터페이스를 가져온다.
  2. 이 인터페이스를 사용하면 기존 객체가 어댑터의 메서드를 안전하게 호출할 수 있다.
  3. 호출을 수신하면 Adapter는 요청을 두 번째 객체에 전달하지만 두 번째 객체가 예상하는 형식과 순서로 전달된다.

때로는 양방향으로 호출을 변환하는 양방향 Adapter를 만드는 것도 가능하다.

호환되지 않는 형식이라는 딜레마를 해결하기 위해 코드가 직접 작동하는 분석 라이브러리의 모든 클래스에 대해 XML-JSON Adapter를 만들 수 있다. 그런 다음 이러한 어댑터를 통해서만 라이브러리와 통신하도록 코드를 조정한다. 어댑터가 호출을 수신하면 들어오는 XML 데이터를 JSON 구조로 변환하고 호출을 래핑된 분석 객체의 적절한 메서드에 전달한다.


구현방법

  1. 호환되지 않는 인터페이스가 있는 클래스가 두 개 이상 있는지 확인한다.
    1. 변경할 수 없는 유용한 서비스 클래스
    2. 서비스 클래스를 사용하여 이점을 얻을 수 있는 하나 이상의 클라이언트 클래스
  2. 클라이언트 인터페이스를 선언하고 클라이언트가 서비스와 통신하는 방법을 설명한다.
  3. 어댑터 클래스를 만들고 클라이언트 인터페이스를 따르도록 한다. 일단 이 단계에서는 모든 메서드를 비워둔다.
  4. 서비스 객체에 대한 참조를 저장할 어댑터 클래스에 필드를 추가한다. 일반적인 방법은 생성자를 통해 이 필드에 값을 채우는(의존성 주입) 방법이지만 떄로는 메서드를 호출할 때 어댑터에 전달하는 것이 더 편리하다.
  5. 어댑터 클래스에서 클라이언트 인터페이스의 모든 메서드를 하나씩 구현한다. 어댑터는 인터페이스 또는 데이터 형식 변환만 처리하면서 실제 작업의 대부분은 서비스 객체에 위임시켜야 한다.
  6. 클라이언트는 클라이언트 인터페이스를 통해서만 어댑터를 사용해야 한다. 이런 방식으로 클라이언트 코드에 영향을 주지 않고 어댑터를 변경하거나 확장할 수 있다.

활용성

  • 기존 클래스를 사용하고 싶지만 해당 인터페이스가 나머지 코드와 호환되지 않는 경우 Adapter 클래스를 사용하여 해결할 수 있다.
    • 어댑터 패턴을 사용하면 코드와 레거시 클래스, 타사 클래스 또는 인터페이스가 있는 다른 클래스 간의 변환기 역할을 하는 중간 계층의 클래스를 만들 수 있다.
  • 상위 클래스에 추가할 수 없는 몇 가지 공통 기능이 없는 여러 기존 하위 클래스를 재사용하려는 경우 패턴을 사용한다.
    • 각 하위 클래스를 확장하고 누락된 기능을 새 하위 클래스에 넣을 수 있다. 그러나 이 모든 새 클래스에 걸쳐 코드를 복제해야 하므로 소요가 매우 크다.
    • 훨씬 나은 방법은 누락된 기능을 어댑터 클래스에 넣는 것이다. 그런 다음 어댑터 내부에 누락된 기능이 있는 객체를 래핑하여 필요한 기능을 동적으로 얻는다. 이것이 작동하려면 대상 클래스에 공통 인터페이스가 있어야 하고 어댑터의 필드가 해당 인터페이스를 따라야 한다. 이 접근 방식은 Decorator 패턴과 매우 유사하다.

구조

객체 어댑터

이 구현은 객체 결합 원칙을 사용한다: 어댑터는 한 객체와 다른 객체를 감싼 것의 인터페이스를 구현한다. 그것은 유명한 프로그래밍 언어 전부에서 구현할 수 있다.

  1. 클라이언트는 프로그램의 기존 비즈니스 로직을 포함하는 클래스이다.
  2. 클라이언트 인터페이스는 다른 클래스가 클라이언트 코드와 협력할 수 있도록 따라야 하는 프로토콜을 설명한다.
  3. 서비스는 유용한 클래스이지만 호환되지 않는 인터페이스를 가지고 있기 때문에 이 클래스를 직접 사용할 수 없다.
  4. 어댑터는 클라이언트와 서비스 모두에서 작동할 수 있는 클래스이다. 서비스 객체를 래핑하는 동안 클라이언트 인터페이스를 구현한다. 어댑터는 어댑터 인터페이스를 통해 클라이언트로부터 호출을 받고 이해할 수 있는 형식으로 래핑된 서비스 객체에 대한 호출로 변환한다.
  5. 클라이언트 코드는 클라이언트 인터페이스를 통해 어댑터와 함께 작동하는 한 구체적인 어댑터 클래스에 연결되지 않는다. 덕분에 기존 클라이언트 코드를 손상시키지 않고 새로운 유형의 어댑터를 프로그램에 도입할 수 있다. 이것은 서비스 클래스의 인터페이스가 변경되거나 교체될 때 유용하게 사용할 수 있다. 클라이언트 코드를 변경하지 않고 새 어댑터 클래스를 생성할 수 있다.

장점

  • 단일 책임 원칙. 프로그램의 기본 비즈니스 로직에서 인터페이스 또는 데이터 변환 코드를 분리할 수 있다.
  • 개방/폐쇄 원칙. 클라이언트 인터페이스를 통해 Adapter와 함께 작동하는 한 기존 클라이언트 코드를 손상시키지 않고 새로운 유형의 어댑터를 프로그램에 도입시킬 수 있다.

단점

  • 새로운 인터페이스와 클래스를 도입해야 하기 떄문에 코드의 전반적인 복잡성이 증가한다.
  • 때로는 코드의 나머지 부분과 일치하도록 서비스 클래스를 변경하는 것이 더 간단할 수 있다.

In Swift

개념적 사례

import XCTest

// Target은 클라이언트 코드에 의해 사용되는 구체적인 도메인 인터페이스를 정의한다.
class Target {

    func request() -> String {
        return "Target: The default target's behavior."
    }
}

// Adaptee는 몇가지 유용한 행동을 포함한다. 그러나 이것의 인터페이스는 존재하는 클라이언트 코드와 맞지 않는다.
// Adaptee는 클라이언트 코드가 사용하기 전에 몇가지 적응이 필요하다.
class Adaptee {

    public func specificRequest() -> String {
        return ".eetpadA eht fo roivaheb laicepS"
    }
}

// 인터페이스와 호환되도록 한다.
class Adapter: Target {

    private var adaptee: Adaptee

    init(_ adaptee: Adaptee) { // 의존성 주입
        self.adaptee = adaptee
    }

    override func request() -> String {
        return "Adapter: (TRANSLATED) " + adaptee.specificRequest().reversed()
    }
}

// 클라이언트 코드는 Target 인터페이스를 따르는 모든 클래스를 지원한다.
class Client {
    // ...
    static func someClientCode(target: Target) {
        print(target.request())
    }
    // ...
}

/// Let's see how it all works together.
class AdapterConceptual: XCTestCase {

    func testAdapterConceptual() {
        print("Client: I can work just fine with the Target objects:")
        Client.someClientCode(target: Target())

        let adaptee = Adaptee()
        print("Client: The Adaptee class has a weird interface. See, I don't understand it:")
        print("Adaptee: " + adaptee.specificRequest())

        print("Client: But I can work with it via the Adapter:")
        Client.someClientCode(target: Adapter(adaptee))
    }
}

실제사례

import XCTest
import UIKit

/// 어댑터 디자인 패턴
///
/// 의도: 클래스의 인터페이스를 클라이언트가 기대하는 인터페이스로 변환한다.
/// 어댑터를 사용하면 호환되지 않는 인터페이스로 인해, 작동하지 않는 클래스가 함께 작동할 수 없다.

class AdapterRealWorld: XCTestCase {
    /// 예를 들어. 우리의 앱이 완벽하게 페이스북 인증하는 것과 함께 잘 동작한다고 가정하자. 
    /// 그러나, 사용자는 당신에게 Twitter를 통해 등록하는 것을 요구한다.
    /// 불행하게도, Twitter SDK는 다른 인증 메서드를 가지고 있다.

    /// 맨 처음, 새로운 프로토콜인 'AuthService' 프로토콜을 생성하고 Facebook SDK의 authorization 메서드를 삽입한다.
    /// 두 번째, Twitter SDK의 익스텐션을 작성하고 단지 간단하게 리디렉트 하는것으로 AuthService 프로토콜의 메서드를 구현한다.
    /// 세 번째, Facebook SDK의 익스텐션을 작성한다. 이 시점에서 Facebook SDK에 의해 이미 구현된 어떤 메서드 코드도 작성하면 안된다.
    /// 단지 컴파일러에게 두 SDK가 같은 인터페이스를 가진 것을 말한다.
    func testAdapterRealWorld() {

        print("Starting an authorization via Facebook")
        startAuthorization(with: FacebookAuthSDK())

        print("Starting an authorization via Twitter.")
        startAuthorization(with: TwitterAuthSDK())
    }

    func startAuthorization(with service: AuthService) {

        /// The current top view controller of the app
        let topViewController = UIViewController()

        service.presentAuthFlow(from: topViewController)
    }
}

protocol AuthService {

    func presentAuthFlow(from viewController: UIViewController)
}

class FacebookAuthSDK {

    func presentAuthFlow(from viewController: UIViewController) {
        /// SDK 메서드를 호출하고 뷰컨트롤러를 넘긴다.
        print("Facebook WebView has been shown.")
    }
}

class TwitterAuthSDK {

    func startAuthorization(with viewController: UIViewController) {
        /// SDK 메서드를 호출하고 뷰컨트롤러를 넘긴다.
        print("Twitter WebView has been shown. Users will be happy :)")
    }
}

extension TwitterAuthSDK: AuthService {

    /// 이것이 어댑터이다.
    /// 우리는 또 다른 클래스를 생성할 수 없고 기존의 존재하는 것을 확장만 할 수 있다.

    func presentAuthFlow(from viewController: UIViewController) {
        print("The Adapter is called! Redirecting to the original method...")
        self.startAuthorization(with: viewController)
    }
}

extension FacebookAuthSDK: AuthService {
    /// 이 익스텐션은 컴파일러에게 두 SDK가 단지 같은 인터페이스를 공유한다는 것을 말해주기 위함이다.
}

출처: refactoring.guru

profile
학생입니다

0개의 댓글