[Design Pattern] 내가 보려고 정리하는 Swift 디자인 패턴 (8) - 어댑터 패턴(Adapter Pattern)

2024. 8. 5. 21:34Architecture, Design Pattern

1️⃣ 어댑터 패턴(Adapter Pattern)이 뭐야?

💡 어댑터 패턴(Adapter Pattern)서로 호환되지 않던 인터페이스(protocol)를 변환시켜 함께 작동할 수 있도록 만드는 구조 패턴(Structural Pattern)이다.

위의 말이 무슨 말인지 잘 이해가 안된다면 아래와 같이 이해해 보는 건 어떨까?

기존에 사용하고 있던 규격이 이미 있는 상황에서, 새롭게 사용하려는 시스템의 규격이 기존 규격과 맞지 않는다면?
-> 어댑터(Adapter)라는 변환 도구를 중간에 두어 기존 규격과 새로운 규격을 함께 사용할 수 있도록 만드는 것이 바로 어댑터 패턴이다.

어댑터 패턴에서 반복적으로 사용하는 용어는 아래 네 가지다.

  • Target (Interface = Protocol) : 기존 시스템이 사용하고 있던 규격
  • Client : 기존 시스템의 규격(Target)을 채택해서 (구현하고) 작동하는 객체 (시스템 그 자체를 의미한다고 보자)
  • Adpatee : 기존 시스템의 규격과 호환되지 않는 새로운 시스템/외부 시스템
  • Adapter : Target과 Adaptee가 상호 호환될 수 있도록 변환/연결하는 역할

용어와 개념을 더 쉽게 이해하기 위해 예시를 하나 들어보겠다. 질문에 대한 답을 직접 먼저 생각해 본 후 글을 읽어보자.

한국에서 사용하던 맥북 충전기를 미국 여행에 가서도 그대로 사용하려고 했지만... 220볼트와 110볼트 단자의 차이 때문에 사용할 수 없다고 한다면...❓
이때의 Target, Client, Adaptee, Adapter는 각각 어떤 것을 의미할까?


생각보다 헷갈리는가? 답을 이어서 설명해보겠다.

  • Target Interface : 110V 규격
  • Client : 110V를 따르고 있는 미국의 전원 규격 시스템 (Target Interface = 110V 규격을 채택하고 있다)
  • Adaptee : 220V 전원을 사용하던 한국의 맥북 충전기
  • Adapter : 220V를 사용하는 충전기를 110V 규격에서 사용할 수 있도록 Target Interface에 맞춰 변환해 주는 역할


어렵게 생각하지 말고 그냥 단순하게 생각하자!
Adapter Pattern은 220볼트 기기를 110볼트에서도 사용할 수 있도록 변환해 주는 어댑터(Adapter)를 사용하는 것이다! 그냥 이게 끝.

해외여행 갈 때 챙기는 어댑터 생각해보자!

 

2️⃣ 어댑터 패턴(Adapter Pattern)의 구현 방법 2가지 : 예제와 코드로 이해하기

Swift에서 어댑터 패턴(Adapter)을 구현하는 방법은 두 가지다. 위의 예시를 계속 이어서 설명해 보겠다.

  1. 220볼트를 사용하던 충전기에 별도의 변환 어댑터(220 to 110)를 연결해 110볼트 전원에 연결하는 방법 -> 별도의 Adapter 객체를 만들어 Adaptee와 Client를 연결하는 일반적인 방식!
  2. 220볼트 전원에만 연결할 수 있던 충전기를 110볼트 전원에도 연결 가능하도록 기능을 확장시키는 방법 -> Adaptee 클래스를 Extension 시켜서 Adapter 기능을 구현하도록 만드는 변형적인 방식!

Swift에서 Adapter Pattern을 구현하는 두 가지 방법


두 방법 중, 먼저 첫 번째 일반적인 Adapter 객체를 만들어서 사용하는 방법부터 살펴보자.

미국의 전력 시스템 객체는 110V 규격을 준수하도록 만들어진다.
이것을 Client(USVoltageSystem)가 Target Protocol(Using110V)을 채택해서 구현부를 작성(use110 메서드)한다고 설명할 수 있겠다.

// MARK: - Target Protocol
protocol Using110V {
    func use110()
}

// MARK: - Client
class USVoltageSystem: Using110V {
    func use110() {
        print("110V 기기를 사용합니다.")
    }
}

미국의 전력 시스템이 있다면, 우리나라의 전력 시스템은 220V 규격을 준수하도록 만들어질거다.

여기서 우리가 Target 프로토콜의 규격과 맞추고 싶은 부분 = 우리나라의 전력 시스템(KoreaVoltageSystem)을 Adaptee라고 부른다.

protocol Using220V {
    func use220()
}

// MARK: - Adaptee
class KoreaVoltageSystem: Using220V {
    func use220() {
        print("220V 기기를 사용합니다.")
    }
}

Adapter가 필요한 상황은 바로 아래와 같은 상황이다.

KoreaVoltageSystem을 따르는 우리나라에서 사용하던 맥 충전기는 220볼트에서만 사용이 가능할 뿐 (koreanMacCharger.use220()),
110볼트 상황에서는 사용할 방법이 없게 된다. (koreanMacCharger.use110()은 호출할 수 없는 메서드다.)

let koreanMacCharger = KoreaVoltageSystem()
koreanMacCharger.use220()   // 220V 기기를 사용합니다.
koreanMacCharger.use110()   // Error: Value of type 'KoreaVoltageSystem' has no member 'use110'

koreanMacCharger가 110볼트를 사용할 수 있도록, 220볼트를 110볼트로 변환해주는 별도의 Adapter 클래스를 만들어보자.

Adapter는 Target 프로토콜을 채택해서 구현한다.
리마인드 해보자면, 위에서 정의했던 Target 프로토콜은 110V 규격을 준수하고 있던 애였다.

Adapter는 Adaptee를 의존한다.
Adaptee를 받아 Target Protocol에서 정의했던 메서드(use110)를 호출하면, Adaptee의 내부 메서드(use220)가 자동으로 호출되도록 구현하면 된다.

class Voltage220To110Adapter: Using110V {
    
    let koreaVoltageSystem: KoreaVoltageSystem

    init(koreaVoltageSystem: KoreaVoltageSystem) {
        self.koreaVoltageSystem = koreaVoltageSystem
    }
    
    func use110() {
        koreaVoltageSystem.use220()
    }
}

그럼 아래와 같이 220볼트를 사용하던 koreanMacCharger도 위에서 정의한 어댑터를 사용하면, 110볼트를 사용할 수 있게 된다.

let useAdapterInUs = Voltage220To110Adapter(koreaVoltageSystem: koreanMacCharger)
useAdapterInUs.use110()     // 220V 기기를 사용합니다.

별도의 Adapter 객체를 만들어 Adaptee와 Client를 연결하는 일반적인 Adapter Pattern의 구조


두 번째는 Adaptee 클래스의 기능을 확장해서 Adapter처럼 동작하도록 만드는 변형적인 방법을 살펴보겠다.

위의 예시 Client-Target Protocol-Adaptee 코드를 그대로 사용하는 상황이다.
차이점은 별도의 Adapter 클래스를 만들어주는 것이 아니라 Adaptee였던 KoreaVoltageSystem에서 Target Protocol을 채택하는 Extension을 만들어준다는 것이다.
채택한 Target Prtocol에서 구현부 메서드를 Adaptee의 메서드가 호출되도록 한다면, 우리는 Adaptee를 Adapter처럼 동작하도록 만들 수 있게 된다.

실제 사용하는 부분을 보면, 220볼트와 110볼트를 모두 사용할 수 있게 되는 것을 볼 수 있다.

extension KoreaVoltageSystem: Using110V {
    func use110() {
        use220()
    }
}

koreanMacCharger.use220()   // 220V 기기를 사용합니다.
koreanMacCharger.use110()   // 220V 기기를 사용합니다.

종합해 보면 Swift에서는 Adapter Pattern을 두 가지 방법으로 구현할 수 있었다.

별도의 Adapter 클래스를 만들었던 것과 Extension으로 기능을 확장했던 것은 어댑터를 구현하는 것에 있어서는 상호 차이가 있었지만,
결론적으로 우리는 한국에서 사용하던 220V 전용 맥 충전기를 110V인 미국에서도 사용할 수 있게 되었다!


참고로, 실전 코드에서는 위 두 방법 중 별도의 어댑터(Adapter) 클래스를 만들어서 사용하는 경우가 거의 대부분이다.
예제 코드와 같이 이렇게 단순한 변환 한 번으로 사용할 수 있는 경우보다는, 상호 호환을 위해 복잡한 코드가 필요한 경우가 많기 때문!

다음에 실전 코드에서 Adapter Pattern을 사용하는 예제가 생기는 경우 글을 이어가기로 하면서 오늘 글은 여기까지!