Adapter Pattern in Swift

design-pattern
Written on Aug 9, 2023


Adapter Pattern을 활용하면 외부 라이브러리의 인터페이스와 내가 사용하고자 하는 인터페이스가 호환되지 않을 때, 중간에 Adapter를 추가하는 방법으로 호환성을 확보할 수 있습니다. Wrapper Pattern으로 불리기도 합니다.

Adapter Pattern은 GoF의 분류 체계에서 구조(Structural) 패턴에 속합니다.

Adapter Pattern을 구현하기 위해 필요한 역할은 다음과 같습니다.

  • Target(대상): 서비스 내에서 사용 중인 인터페이스입니다.
  • Client(의뢰자): Target을 사용하는 클래스입니다.
  • Adaptee(적응 대상자): Target과 호환되지 않는 인터페이스입니다.
  • Adapter(적응자): Adaptee를 Target으로 변환하는 클래스입니다.

Adapter를 구현하는 방법은 상속을 이용한 방법과 위임(인스턴스)을 이용한 방법으로 나뉘지만 Is-A보단 Has-A가 더 좋은 방법이므로 위임을 이용한 방법을 사용하도록 합니다. Is-A Has-A Relationship


예시를 통해 자세히 알아보겠습니다. 시스템 내에서 사용 중인 로깅 인터페이스가 있다고 가정해봅시다.

Language:swift
protocol Logger {
    func log(message: String)
    func warn(message: String)
    func error(message: String)
}

Logger는 Adapter Pattern의 구성 요소 중 Target 역할을 수행합니다. 이를 채택하는 클래스 MyLogger를 구현합니다. MyLogger는 Swift의 print()함수를 이용하여 Termianl 환경에서 로그를 출력합니다.

Language:swift
struct MyLogger: Logger {
  func log(message: String) {
    print("[LOG] \(message)")
  }

  func warn(message: String) {
    print("[WARN] \(message)")
  }

  func error(message: String) {
    print("[ERROR] \(message)")
  }
}

Client 역할을 수행하는 App 클래스에서 Logger 구현체를 주입받아 시스템 전체에 걸쳐 사용합니다.

Language:swift
// App.swift
final class App {
    private let logger: Logger

    init(logger: Logger) {
        self.logger = logger
    }

    func doSomething() {
        logger.log(message: "doSomething")
    }
}
Language:swift
// main.swift
let app = App(logger: MyLogger())
app.doSomething()

// output:
// [LOG] doSomething

MyLogger는 swift의 print() 함수에 의존하고 있습니다. 이제 외부 라이브러리를 통해 terminal 환경이 아닌 외부로 로그 정보를 보내려고 합니다. 외부 라이브러리 ExternalLogger는 다음 인터페이스를 제공합니다.

Language:swift
enum LogLevel {
    case debug
    case info
    case warn
    case error
}

struct ExternalLogger {
    func log(level: LogLevel, message: String) {
      // 로그를 외부로 전송합니다.
    }
}

언뜻 보기에도 ExternalLoggerLogger와 호환되지 않는 것을 알 수 있습니다. 이럴 때 Adapter 클래스를 추가하여 호환성을 확보할 수 있습니다.

Language:swift
struct ExternalLoggerAdapter: Logger {
    private let logger = ExternalLogger()

    func log(message: String) {
        logger.log(level: .info, message: message)
    }

    func warn(message: String) {
        logger.log(level: .warn, message: message)
    }

    func error(message: String) {
        logger.log(level: .error, message: message)
    }
}
Language:swift
let app = App(logger: ExternalLoggerAdapter())
app.doSomething()

이렇게 Adapter 역할을 수행하는 중간자 클래스를 추가하여 세부 구현에 어떠한 변경도 없이 세부 구현에 어떠한 변경도 없이 확장에 성공하였습니다. 이런 식으로 Adapter 구조체를 추가하면 변경이 아닌 확장(OCP - 개방 폐쇄 원칙)이 되어 Side Effect도 없을 뿐더러 Unit Test를 작성하기도 쉬워집니다.