구조 패턴 - 어댑터 패턴 (Adapter Pattern)

French Marigold·2024년 3월 12일
4

디자인패턴

목록 보기
1/10
post-thumbnail

정의

  • 해외 여행을 다니다보면 220V 단자를 가지고 갔는데 정작 여행지에서는 완전 다른 단자를 사용하는 경우가 빈번하다. 이 때 서로 호환이 되지 않는 단자를 “어댑터” 라는 것으로 호환시켜 작동시키지 않는가?

  • 이것을 객체 지향 프로그래밍에 접목시키면 어댑터 패턴(Adapter Pattern)이란 함께 동작할 수 없는 클래스들을 함께 동작하도록 변환 역할을 해주는 패턴이라고 할 수 있다.
    • 기존 회사의 코드를 신규 업체의 코드에 접목시키고자 할 때, 서로 호환이 되지 않는 클래스를 호환이 되게끔 중간에 Adapter가 조율해주는 것이 일반적인 Adapter 패턴을 이용한 것이다.

어댑터 패턴의 종류

  • 어댑터 패턴에는 두 가지 방식이 존재한다.
  1. 객체 어댑터 (Object Adapter)
  2. 클래스 어댑터 (Class Adapter)

객체 어댑터 (Object Adapter)

  • Adaptee를 “상속받지 않고" 오직 Target 프로토콜과 Adapter 클래스만을 이용하여 Client와 Adaptee 사이를 연결시켜줄 때 사용되는 방법.

  • Client ⇒ 특정 시스템을 Adapter를 통해 이용하려는 쪽.
  • Target ⇒ Adapter가 구현해야 하는 프로토콜. Client는 Target의 프로토콜 내부의 메소드에 접근하여 Adaptee와 호환할 것이다.
  • Adapter ⇒ Client와 Adaptee 사이에서 호환성이 없는 두 클래스를 연결해주는 역할. Adaptee 클래스를 Adapter 안에서 생성하면 된다.
  • Adaptee ⇒ 어댑터 대상 객체. 외부 시스템, 써드 파티 라이브러리.

객체 어댑터 패턴 적용 과정 예시

  1. 우선 현재 우리 회사(Client)가 사용하고 있는 기존 시스템 코드가 존재한다고 해보자. 간단하게 데이터를 입력하는 코드이다.
class OurCompany {
    static func insertData() {
        // 우리 회사가 사용하고 있는 코드 내역
    }
}
  1. 그런데 새로 계약한 업체(Adaptee)에서 사용하는 시스템 코드가 우리 회사(Client)의 시스템 코드와 판이하게 다르다. 데이터를 입력하는 방법이 insertData()와 insertData(_ specialData: Int)로 다르다.
  • 이러면 새로 계약한 업체(Adaptee)의 코드를 우리 회사(Client)의 코드로 바꿀 수도 없고 (다른 회사의 코드를 마음대로 바꿀 순 없으니까)
  • 반대로 잘 만들어놓은 우리 회사(Client)의 코드를 다른 업체(Adaptee)의 코드에 맞게 모두 뜯어고칠 수도 없다. (시간 너무 오래 걸림) 이럴 때엔 어떻게 해야 할까?
class NewlyContractedCompany {
		// 데이터를 입력하는 코드가 우리 회사의 것과 다르기 때문에 호환 불가능.
		// 어떻게 해야 이 문제를 처리할 수 있을까?
    func insertData(_ specialData: Int) {
        print("기존 서비스 기능 호출 + \(specialData)")
    }
}
  1. 객체 어댑터 패턴에는 두 가지가 필요하다. 반드시 이 두 가지가 존재해야 Adapter 패턴을 제대로 구현할 수 있다.
    • 나의 회사 코드(Client)와 새로운 업체의 코드를 이어줄 때 필요한 "프로토콜(Target)"
      • 우리 회사(Client)는 해당 "프로토콜(Target) 내부에 있는 메소드를 통해서만" 다른 회사(Adaptee)의 코드에 접근할 것이다.
    • 나의 회사 코드(Client)와 새로운 업체(Adaptee)의 코드를 내부적으로 이어줄 "어댑터 클래스(Adapter)"
      • 어댑터 클래스(Adapter)는 어댑터 프로토콜(Target)을 반드시 채택해야 한다.
      • 프로토콜 내부의 메소드를 어댑터 클래스에 구현한 후, "프로토콜에 있는 메소드 내부에다" 다른 업체의 클래스에 접근하여 코드를 실행한다.
protocol AdapterProtocol {
    func insertData(_ data: Int)
}

class Adapter: AdapterProtocol {
    var adaptee = NewlyContractedCompany() // 다른 업체의 클래스를 생성

    // "프로토콜에 있는 메소드 내부에다" 다른 업체의 클래스에 접근하여 코드를 실행.
    // 이렇게 하면 후에 우리 회사 코드 측에서 다른 업체의 코드에 값을 전달할 수 있게 된다.
    func insertData(_ data: Int) {
        adaptee.insertData(data)
    }
}
  1. 다시 우리 회사(Client) 코드로 돌아와서
    • 어댑터 프로토콜을 채택한 어댑터 클래스 인스턴스를 회사 코드 내부에 생성한다.
    • 우리 회사(Client)는 어댑터 프로토콜(Target)의 메소드를 통해서 외부 업체(Adaptee)와 호환할 것이다.
class OurCompany {
    static func insertData() {
        // 우리 회사 코드 내부에서 우리 회사와 다른 회사를 이어줄 "어댑터"를 생성
        // 그러면 우리 회사 코드와 다른 업체 코드가 서로 호환되게 됨.
        let adapter: AdapterProtocol = Adapter()

        // "어댑터 프로토콜(Target)을 통해서" 어댑터의 메소드에 값을 전달.
        // 우리 회사 측에서 값을 사용했으나 값이 다른 회사 측으로 넘어가는 것을 확인할 수 있다.
        adapter.insertData(1)
    }
}

// 결과를 확인해보면 분명히 우리 회사 쪽에서 값을 입력했는데
// 다른 회사의 코드로 값이 넘어갔음을 확인할 수 있음.
// 즉, 서로 다른 회사의 코드가 호환된 것이다!
OurCompany.insertData()

클래스 어댑터 (Class Adapter)

  • Adaptee와 Target 프로토콜을 동시에 채택하여 Client와 Adaptee 사이를 연결시켜줄 때 사용되는 방법.
  • Adaptee를 상속하기 때문에 Adaptee 내부의 코드를 바로 사용할 수 있다.
  • 그런데 이 방식은 다중 상속을 지원해야 한다. Swift는 다중 상속을 지원하지 않으므로 해당 방법은 Swift에서 사용하는 것을 비추천함.

  • Client ⇒ 특정 시스템을 Adapter를 통해 이용하려는 쪽.
  • Target ⇒ Adapter가 구현해야 하는 프로토콜. Client는 Target의 프로토콜 내부의 메소드에 접근하여 Adaptee와 호환할 것이다.
  • Adapter ⇒ Client와 Adaptee 사이에서 호환성이 없는 두 클래스를 연결해주는 역할. Adaptee 클래스를 Adapter에서 상속하여 사용한다.
  • Adaptee ⇒ 어댑터 대상 객체. 외부 시스템, 써드 파티 라이브러리.

클래스 어댑터 패턴 적용 과정 예시

  1. 우선 현재 우리 회사(Client)가 사용하고 있는 기존 시스템 코드가 존재한다고 해보자. 간단하게 데이터를 입력하는 코드이다.
class OurCompany {
    static func insertData() {
        // 내가 사용하고 있는 코드 내역
    }
}
  1. 새로 계약한 회사(Adaptee)의 코드를 사용해야 하는데 우리 회사(Client)가 사용하는 시스템 코드와 다르다.
class NewlyContractedCompany {
    func insertData(_ specialData: Int) {
        print("기존 서비스 기능 호출 + \(specialData)")
    }
}
  1. 클래스 어댑터를 사용하여 새로 계약한 회사의 클래스에 직접 접근하는 방법을 택한다. 즉, 상속이라는 방법을 채택한다.
    • 물론 우리 회사(Client)가 어댑터에 접근하기 위해서는 어댑터 프로토콜(Target)이 필요하므로 클래스 어댑터는 어댑터 프로토콜(Target)과 Adaptee를 동시에 채택하기로 한다.
protocol AdapterProtocol {
    func insertData(_ data: Int)
}

// 어댑터 클래스는 다른 회사 클래스와 어댑터 프로토콜을 같이 채택
// 그렇게 함으로써 다른 회사 클래스 내부의 메소드에 직접 접근이 가능하다.
class Adapter: AdapterProtocol, NewlyContractedCompany {
    func insertData(_ data: Int) {
        // 다른 회사 클래스 내부에 있는 메소드를 바로 호출한다. (상속받았으므로 가능한 일)
        specificMethod(data)
    }
}
  1. 다시 우리 회사 코드로 돌아와서
    • 우리 회사 측에서 클래스 어댑터 인스턴스를 생성한다. 어댑터는 어댑터 프로토콜을 채택하므로 우리 회사(Client)는 어댑터 프로토콜(Target)을 통해 타 회사의 코드에 접근할 수 있다.
class OurCompany {
    static func insertData() {
        // 우리 회사 클래스(Client) 측에서 어댑터를 생성한 후
        let adapter: AdapterProtocol = Adapter()

        // 어댑터 프로토콜(Target)에 존재하는 메소드를 통해서 값을 실행하면
        // 우리 회사에서 값을 사용하지만 다른 회사의 코드에서 값이 실행되는 것을 확인할 수 있다.
        adapter.insertData(1)
    }
}

패턴 사용 시기

  • 오래된 레거시 코드를 사용하고 싶으나 현재의 코드가 레거시 코드와 호환되지 않을 경우
  • 이미 만들어진 클래스를 새로운 클래스 및 인터페이스에 맞게 개조해야 할 때

패턴의 장점

  • 기존의 Client 코드를 직접 변경하지 않고도 어댑터 프로토콜을 이용해 기능을 확장시킬 수 있다. 즉, OCP를 만족한다. (코드를 직접 수정하는 것에는 닫혀있고, 기능을 추가하는 데에는 열려있음)
  • 버그 수정이 쉽다. 즉, 버그가 발생해도 기존의 Client 코드를 건드린 것이 아니므로 Adapter 역할의 클래스를 중점적으로 조사하면 된다.

패턴의 단점

  • 어댑터 패턴을 사용하기 위해서 어댑터 클래스 및 프로토콜을 추가해야 하기 때문에 코드의 복잡성을 증가시킬 수 있음.
  • 서로 다른 클래스끼리 호환하는 과정에서 추가적인 처리 시간과 오버헤드가 발생할 수도 있음.

참고 문헌

profile
꽃말 == 반드시 오고야 말 행복

8개의 댓글

comment-user-thumbnail
2024년 3월 13일

한 언어에서 다른 언어의 문법을 사용해야 할 때도 유용하게 쓸 수 있었습니다

Objective-C / Swift에서 C/C++ 헤더를 가진 파일을 사용해야 할때요
C++ 문법을 사용해야 되는 메서드를 Adapter 클래스에 다 넣고 그 클래스 외에서는 기존 언어의 문법을 사용하면 되더라고요

디자인패턴은 평소에 머리에 박혀 있어야 상황을 만났을 때 적용할 수 있는 것 같아요
어떤 패턴이 적합한 지 아는 것도 실력인가봐요

1개의 답글
comment-user-thumbnail
2024년 3월 15일

이전 프로젝트를 할 때 두 레이어를 연결해주기 위해 어댑터패턴을 사용한적이있었는데
원하고자하는 바는 이루었지만 레이어가 하나더 생긴다는 단점이존재했던것같아요
이미 여러 레이어를 쓰고있는데 추가로 하나의 레이어가 추가된다는것 자체가 부담이되었던지라 다시 어뎁터패턴을 지웠던경험이있는데 아무래도 이런부분도 꼭 고려를 해야한다는걸 느꼈던 경험이 있네요 ㅎㅎ

1개의 답글
comment-user-thumbnail
2024년 3월 15일

객체 어댑터와 클래스 어댑터로 분리 표현할 수 있군요! 어댑터 패턴만 알고 있었는데, 너무나 유용히 읽었습니다.
읽으며 궁금한 점이 있었는데, 객체 어댑터의 설명 예시에서 Adapter를 생성하기 위해 NewlyContractedCompany를 Client에서 직접 생성 주입하는 데에는 의도가 있었을까요? 읽으며 Client와 Apdatee의 결합을 낮추기 위해 Adapter라는 중간 객체가 있다고 이해하였습니다. 그렇기 때문에 Client 또한 Apadater 생성을 위해 필요한 NewlyContractedCompany를 상위에서 주입 받을 필요를 느끼기도 했습니다. Client가 외부 코드인 NewlyContractedCompany를 알지 못해야 (결합도를 낮추어) AdapterProtocol 프로토콜을 생성해 둔 것의 쓰임이 명확해지지 않을까 생각했기 때문입니다! 혹 나눠주실 생각이 있다면 감사히 듣겠습니다!

1개의 답글