Self-Dev/Design Patterns R&D

[18] 디자인 패턴 목록 - 구조 패턴 - 어댑터(Adapter, Wrapper)

Khadra 2024. 6. 27. 23:53

출처 : 디자인 패턴에 뛰어들기 - 알렉산더 슈베츠 도서


어댑터(Adapter)란?

호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있도록 하는 구조적 디자인 패턴이다.



문제

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

어느 시점에 당신은 타사의 스마트 분석 라이브러리를 통합하여 당신의 앱을 개선하기로 결정하였으나, 함정이 존재하게 되는데, 이 분석 라이브러리는 JSON 형식의 데이터로만 작동한다는 것이다.


그림과 같이 분석 라이브러리는 '있는 그대로' 사용할 수 없다. 왜냐하면 앱과 호환되지 않는 형식의 데이터를 기다리고 있기 때문이다.

즉, 해당 라이브러리를 XML과 작동하도록 변경할 수 있으나, 그러면 라이브러리에 의존하는 일부 기존 코드가 손상될 가능성이 존재한다. 또한 타사의 라이브러리 소스 코드에 접근하는 것이 불가능하여 위의 해결 방식을 사용하지 못할 수도 있다.



해결책

  • 어댑터는 한 객체의 인터페이스를 다른 객체가 이해할 수 있도록 변환하는 특별한 객체이다.
  • 어댑터는 변환의 복잡성을 숨기기 위하여 객체 중 하나를 래핑(포장)함으로써, 래핑된 객체는 어댑터를 인식하지도 못한다.
    • 예를 들어 미터 및 킬로미터 단위로 작동하는 객체를 모든 데이터를 피트 및 마일과 같은 영국식 단위로 변환하는 어댑터로 래핑할 수 있다.
  • 어댑터는 데이터를 다양한 형식으로 변환할 수 있을 뿐만 아니라 다른 인터페이스를 가진 객체들이 협업하는 데에도 도움을 줄 수 있다.

어뎁터 작동 설명

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

때로는 양방향으로 호출을 변환할 수 있는 양방향 어댑터를 만드는 것도 가능합니다.


주식 시장 앱 예시 그림을 살펴보면,

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


비유 설명

해외 첫 여행과 10번째 여행할 때

미국에서 유럽으로 처음 여행을 가서 노트북을 충전 시 한국에서 사용하던 전원 플러그와 소켓이 맞지 않는다.

  • 즉, 위 해당 그림은 전원 플러그와 소켓은 국가마다 표준이 달라 미국 플러그가 독일 소켓에 맞지 않을 수 있기 때문에 미국식 소켓과 유럽식 플러그가 있는 전원 플러그 어댑터를 사용하면 해결할 수 있다라는 것을 간접적으로 보여주고 있다.


구조

객체 어댑터

  • 객체 합성 원칙을 사용한다.
  • 어댑터는 한 객체의 인터페이스를 구현하고 다른 객체는 래핑한다.
    • 위 합성은 모든 인기 있는 프로그래밍 언어로 구현할 수 있다.



  • 1. 클라이언트는 프로그램의 기존 비즈니스 로직을 포함하는 클래스이다.
  • 2. 클라이언트 인터페이스는 다른 클래스들이 클라이언트 코드와 공동 작업할 수 있도록 따라야 하는 프로토콜을 뜻한다.
  • 3. 서비스는 일반적으로 타사 또는 레거시의 유용한 클래스를 뜻한다.
    • 클라이언트는 서비스 클래스를 직접 사용할 수 없다.
    • 왜냐하면, 서비스 클래스는 호환되지 않는 인터페이스를 가지고 있기 때문이다.
  • 4. 어댑터는 클라이언트와 서비스 양쪽에서 작동할 수 있는 클래스로, 서비스 객체를 래핑하는 동안 클라이언트 인터페이스를 구현한다.
    • 어댑터는 어댑터 인터페이스를 통해 클라이언트로부터 호출들을 수신한 후 이 호출을 래핑된 서비스 객체가 이해할 수 있는 형식의 호출들로 변환한다.
  • 5. 클라이언트 코드는 클라이언트 인터페이스를 통해 어댑터와 작동하는 한 구상 어댑터 클래스와 결합하지 않다.
    • 덕분에 기존 클라이언트 코드를 손상하지 않고 새로운 유형의 어댑터들을 프로그램에 도입할 수 있다.
    • 이것은 서비스 클래스의 인터페이스가 변경되거나 교체될 때 유용할 수 있다.
    • 클라이언트 코드를 변경하지 않은 채 새 어댑터 클래스를 생성할 수 있기 때문이다.

클래스 어댑터

  • 이 구현은 상속을 사용한다.
  • 어댑터는 동시에 두 객체의 인터페이스를 상속한다.
  • 이 방식은 C++ 와 같이 다중 상속을 지원하는 프로그래밍 언어에서만 구현할 수 있다.

  • 클래스 어댑터는 객체를 래핑할 필요가 없다.
    • 그 이유는 클라이언트와 서비스 양쪽에서 행동들을 상속받기 때문이다.
  • 위의 어댑테이션(적용)은 오버라이딩된 메서드 내에서 발생한다.
  • 위 어댑터는 기존 클라이언트 클래스 대신 사용할 수 있다.


의사코드

해당 어댑터 패턴은 서로 맞지 않는 정사각형 못과 둥근 구멍이라는 고전적인 예시로 기초 설명을 해준다.


둥근 구멍에 정사각형 못을 맞춰 넣기


어댑터는 정사각형 지름의 절반(즉, 사각형 못을 수용할 수 있는 가장 작은 원의 반지름)을 반지름으로 가진 둥근 못인 척 합니다.

// RoundHole(둥근 구멍) 및 RoundPeg(둥근 못)라는 호환되는 
// 인터페이스들이 있는 두 개의 클래스가 있다고 가정한다.
class RoundHole is
  constructor RoundHole(radius) { ... }

  method getRadius() is
    // 구멍의 반지름을 반환하세요.

  method fits(peg: RoundPeg) is
    return this.getRadius() >= peg.getRadius()

class RoundPeg is
  constructor RoundPeg(radius) { ... }

  method getRadius() is
    // 못의 반지름을 반환하세요.


// 그러나 SquarePeg(직사각형 못)라는 호환되지 않는 클래스가 있다.
class SquarePeg is
  constructor SquarePeg(width) { ... }

  method getWidth() is
    // 직사각형 못의 너비를 반환하세요.


// 어댑터 클래스를 사용하면 정사각형 못을 둥근 구멍에 맞출 수 있다. 
// 어댑터 객체들은 RoundPeg(둥근 못) 클래스를 확장해 둥근 못들처럼 작동하게 해준다.
class SquarePegAdapter extends RoundPeg is
  // 실제로 어댑터에는 SquarePeg(정사각형 못) 클래스의 인스턴스가 포함되어 있다.
  private field peg: SquarePeg

  constructor SquarePegAdapter(peg: SquarePeg) is
    this.peg = peg

  method getRadius() is
    // 어댑터는 이것이 어댑터가 실제로 감싸는 정사각형 못에 맞는 반지름을
    // 가진 원형 못인 것처럼 가장한다.
    return peg.getWidth() * Math.sqrt(2) / 2


// 클라이언트 코드 어딘가에…
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // 참

small_sqpeg = new SquarePeg(5)
large_sqpeg = new SquarePeg(10)
hole.fits(small_sqpeg) // 이것은 컴파일되지 않는다(호환되지 않는 유형)

small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // 참
hole.fits(large_sqpeg_adapter) // 거짓




적용

어댑터 클래스는 기존 클래스를 사용하고 싶지만 그 인터페이스가 나머지 코드와 호환되지 않을 때 사용하세요.

  • 어댑터 패턴은 당신의 코드와 레거시 클래스, 타사 클래스 또는 특이한 인터페이스가 있는 다른 클래스 간의 변환기 역할을 하는 중간 레이어 클래스를 만들 수 있도록 한다.

이 패턴은 부모 클래스에 추가할 수 없는 어떤 공통 기능들이 없는 여러 기존 자식 클래스들을 재사용하려는 경우에 사용하세요.

  • 각 자식 클래스를 확장한 후 누락된 기능들을 새 자식 클래스들에 넣을 수 있다.
  • 하지만 해당 코드를 모든 새 클래스들에 복제해야 하며, 그건 정말 나쁘고 복잡한 코드일 것입니다.

해결책

  • 이보다 훨씬 더 깔끔한 해결책은 누락된 기능을 어댑터 클래스에 넣는 것이다.
    • 그 후 어댑터 내부에 누락된 기능이 있는 객체들을 래핑하면 필요한 기능들을 동적으로 얻을 것이다.

  • 이 해결책이 작동하려면 대상 클래스들에는 반드시 공통 인터페이스가 있어야 하며 어댑터의 필드는 해당 인터페이스를 따라야 한다.
    • 위 접근 방식은 데코레이터 패턴과 매우 유사합니다.


구현 방법

  • 1. 호환되지 않는 인터페이스가 있는 클래스가 최소 두 개 이상 있는지 확인하세요.
    • 당신이 변경할 수 없는 유용한 서비스 클래스가 있습니다.
      • (종종 타사 코드, 레거시 코드 또는 기존 의존성이 많은 코드).
    • 위 서비스 클래스를 사용하여 이득을 얻을 수 있는 하나 또는 여러 개의 클라이언트 클래스들이 있다.
  • 2. 클라이언트 인터페이스를 선언하고 클라이언트들이 서비스와 통신하는 방법을 기술하세요.
  • 3. 어댑터 클래스를 생성한 후 클라이언트 인터페이스를 따르게 하세요.
    • 일단은 모든 메서드들을 비워 두세요.
  • 4. 서비스 객체에 참조를 저장하기 위하여 어댑터 클래스에 필드를 추가하세요.
    • 일반적으로 사용되는 방법은 생성자를 통해 이 필드를 초기화하는 것이지만, 때때로 어댑터의 메서드들을 호출할 때는 이 필드를 어댑터에 전달하는 것이 더 편리하기도 하다.
  • 5. 클라이언트 인터페이스의 모든 메서드를 어댑터 클래스에서 하나씩 구현하세요.
    • 어댑터는 인터페이스 또는 데이터 형식 변환만 처리해야 하며, 실제 작업의 대부분을 서비스 객체에 위임해야 한다.
  • 6. 클라이언트들은 클라이언트 인터페이스를 통해 어댑터를 사용해야 한다.
    • 이렇게 하면 클라이언트 코드에 영향을 주지 않고 어댑터들을 변경하거나 확장할 수 있다.

장단점

장점

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

단점

  • 다수의 새로운 인터페이스와 클래스들을 도입해야 하므로 코드의 전반적인 복잡성이 증가한다.
    • 때로는 코드의 나머지 부분과 작동하도록 서비스 클래스를 변경하는 것이 더 간단하다.

다른 패턴과의 관계

  • 브리지는 일반적으로 사전에 설계되며, 앱의 다양한 부분을 독립적으로 개발할 수 있도록 한다.
    • 반면에 어댑터는 일반적으로 기존 앱과 사용되어 원래 호환되지 않던 일부 클래스들이 서로 잘 작동하도록 한다.
  • 어댑터는 기존 객체의 인터페이스를 변경하는 반면 데코레이터는 객체를 해당 객체의 인터페이스를 변경하지 않고 향상한다.
    • 또한 데코레이터는 어댑터를 사용할 때는 불가능한 재귀적 합성을 지원합니다.
  • 어댑터는 다른 인터페이스를, 프록시는 같은 인터페이스를, 데코레이터는 향상된 인터페이스를 래핑된 객체에 제공한다.
  • 퍼사드는 기존 객체들을 위한 새 인터페이스를 정의하는 반면 어댑터는 기존의 인터페이스를 사용할 수 있게 만들려고 노력한다.
    • 또 어댑터는 일반적으로 하나의 객체만 래핑하는 반면 퍼사드는 많은 객체의 하위시스템과 함께 작동한다.
  • 브리지, 상태, 전략 패턴은 매우 유사한 구조로 되어 있으며, 어댑터 패턴도 이들과 어느 정도 유사한 구조로 되어 있다.
    • 위 모든 패턴은 다른 객체에 작업을 위임하는 합성을 기반으로 한다. 하지만 이 패턴들은 모두 다른 문제들을 해결한다.
    • 패턴은 특정 방식으로 코드의 구조를 짜는 레시피에 불과하지 않다. 왜냐하면 패턴은 해결하는 문제를 다른 개발자들에게 전달할 수도 있기 때문이다.