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
예시를 통해 자세히 알아보겠습니다. 시스템 내에서 사용 중인 로깅 인터페이스가 있다고 가정해봅시다.
protocol Logger {
func log(message: String)
func warn(message: String)
func error(message: String)
}
Logger
는 Adapter Pattern의 구성 요소 중 Target 역할을 수행합니다. 이를 채택하는 클래스 MyLogger
를 구현합니다. MyLogger
는 Swift의 print()
함수를 이용하여 Termianl 환경에서 로그를 출력합니다.
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 구현체를 주입받아 시스템 전체에 걸쳐 사용합니다.
// App.swift
final class App {
private let logger: Logger
init(logger: Logger) {
self.logger = logger
}
func doSomething() {
logger.log(message: "doSomething")
}
}
// main.swift
let app = App(logger: MyLogger())
app.doSomething()
// output:
// [LOG] doSomething
MyLogger
는 swift의 print()
함수에 의존하고 있습니다. 이제 외부 라이브러리를 통해 terminal 환경이 아닌 외부로 로그 정보를 보내려고 합니다. 외부 라이브러리 ExternalLogger
는 다음 인터페이스를 제공합니다.
enum LogLevel {
case debug
case info
case warn
case error
}
struct ExternalLogger {
func log(level: LogLevel, message: String) {
// 로그를 외부로 전송합니다.
}
}
언뜻 보기에도 ExternalLogger
는 Logger
와 호환되지 않는 것을 알 수 있습니다. 이럴 때 Adapter 클래스를 추가하여 호환성을 확보할 수 있습니다.
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)
}
}
let app = App(logger: ExternalLoggerAdapter())
app.doSomething()
이렇게 Adapter 역할을 수행하는 중간자 클래스를 추가하여 세부 구현에 어떠한 변경도 없이 세부 구현에 어떠한 변경도 없이 확장에 성공하였습니다. 이런 식으로 Adapter 구조체를 추가하면 변경이 아닌 확장(OCP - 개방 폐쇄 원칙)이 되어 Side Effect도 없을 뿐더러 Unit Test를 작성하기도 쉬워집니다.