[Structural Patterns] - Adapter

Lee Jeong Min·2022년 1월 9일
0

디자인 패턴

목록 보기
7/23
post-thumbnail

이번부터 Structural Patterns과 관련한 내용이다. Structural Patterns은 구조물을 유연하고 효율적으로 유지하면서 객체와 클래스를 더 큰 구조로 조립한다.

어댑터 패턴은 다른 명칭으로 래퍼(Wrapper)라고 불리운다.

의도

어댑터는 호환되지 않은 인터페이스를 가진 객체가 협업할 수 있도록 하는 구조 설계 패턴이다.

어댑터는 두 객체 사이에서 래퍼 역할을 한다. 하나의 객체에 대한 호출을 확인하고 두 번째 객체가 인식할 수 있는 포맷과 인터페이스로 변환한다.

문제

자신이 주식시장 모니터링 앱을 만든다고 상상해보자. 이 앱은 XML 형식으로 된 여러 소스에서 주식 데이터를 다운로드하고, 사용자에게 이를 이용하여 보기 좋은 차트와 다이어그램을 표시한다.

어느 시점에서 제 3자 분석 라이브러리를 통합하여 앱을 개선하기로 결정하였다고 하자. 그러나 이 앱은 JSON 형식의 데이터에서만 작동한다고 하면 기존 XML로 작동된 앱과 호환이 되지 않을 것이다.

분석하는 라이브러리를 "있는 그대로" 사용할 수 없다.

XML에서 작동하도록 라이브러리를 변경하게 되면 라이브러리에 의존하는 일부 기존 코드가 바뀔 수 있다. 또한 애초에 라이브러리 소스 코드 권한이 없어 액세스를 하지 못한다면, 이러한 접근 방식이 불가능할 수 있다.

해결책

이를 해결하기 위해 어댑터를 생성할 수 있다. 이것은 한 객체의 인터페이스를 다른 객체가 이해할 수 있도록 변환하는 객체이다.

어댑터는 백그라운드에서 발생하는 복잡한 변환과정을 숨기기 위해 객체 중 하나를 래핑(Wrapping)한다. 포장된 물체는 어댑터도 인식하지 못한다. 예를 들어 미터 및 킬로미터 단위로 작동하는 객체의 모든 데이터를 피트 및 마일 같은 영국식 단위로 변환하는 어댑터를 사용하여 래핑할 수 있다.

어댑터는 데이터를 다양한 형식으로 변환하고, 서로 다른 인터페이스를 가진 객체의 협업을 도울 수 있다. 작동 방식은 다음과 같다.

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

때로는 양방향으로 호출을 변환할 수 있는 양방향 어댑터를 만들 수 있다.

아까의 주식시장 앱으로 돌아와서 호환되지 않는 부분을 해결하기 위해 XML-JSON 어댑터를 만들 수 있다. 그런 다음 어댑터를 통해서만 라이브러리와 통신하도록 코드를 작성하면 어댑터는 호출 수신 시, XML데이터를 JSON 구조로 변환하고 래핑된 분석 객체의 적절한 메서드로 호출을 전달한다.

현실 유사성

나라마다 다른 플러그 타입

미국에서 유럽으로 여행을 가서 노트북을 충전하려고하면 놀랄 수 있다. 왜냐하면 전원 플러그와 소켓 표준이 국가마다 다르기 때문이다. 이를 위해 전원 플러그 어댑터를 사용하여 호환시키는 방법으로 문제를 해결한다.

구조

객체 어댑터

이 구현에서는 객체 구성 원리를 사용한다. 어댑터는 한 객체의 인터페이스를 구현하고 이 인터페이스와 다른 객체(서로 호환이 가능하도록)를 래핑한다.

  1. 클라이언트는 프로그램의 기존 비즈니스 논리를 포함하는 클래스이다.

  2. 클라이언트 인터페이스는 클라이언트 코드와 협업하기 위해 다른 클래스가 따라야 하는 프로토콜을 설명한다.

  3. 서비스는 일부 유용한 클래스(일반적으로 타사 또는 레거시)이다. 클라이언트가 호환되지 않는 인터페이스를 가지고 있기 때문에 이 클래스를 직접 사용할 수 없다.

  4. 어댑터는 클라이언트와 서비스 모두에서 사용할 수 있는 클래스이다. 서비스 객체를 래핑하고 클라이언트 인터페이스를 구현하는데, 이를 통해 클라이언트로부터 호출을 수신하고 이해할 수 있는 형식으로 래핑된 서비스 객체에 대한 호출로 변환한다.

  5. 클라이언트 코드는 클라이언트 인터페이스를 통해 어댑터와 작동하는 한 구체적인 어댑터 클래스에 결합되지 않는다. 덕분에 기존 클라이언트 코드를 깨지 않고 새로운 유형의 어댑터를 프로그램에 도입할 수 있다. 이 기능은 서비스 클래스의 인터페이스가 변경되거나 교체될 때 유용할 수 있다. 클라이언트 코드를 변경하지 않고 새 어댑터 클래스를 생성할 수 있다.


클래스 어댑터

이 구현에서는 상속을 사용한다. 어댑터는 두 객체 모두에서 동시에 상속한다.
→ C++과 같이 다중 상속을 지원하는 경우에만 사용 가능

클래스 어댑터는 클라이언트와 서비스 모두에서 동작을 상속하여 객체를 래핑할 필요가 없다. adaption은 재정의된 메서드 내에서 작동하기 때문에 어댑터를 기존 클라이언트 클래스 대신 사용할 수 있다.

적용가능성

  • 일부 기존 클래스를 사용하고, 해당 인터페이스가 코드의 나머지 클래스와 호환되지 않는 경우 어댑터 클래스를 사용한다.

  • 슈퍼 클래스에 추가할 수 없는 일부 공통 기능이 없는 기존의 여러 하위 클래스에서 재사용하려면 이 패턴을 사용한다.

장단점

장점

  • 단일책임 원칙. 인터페이스 똔느 데이터 변환 코드를 프로그램의 주요 비즈니스 로직과 분리 가능

  • 열기/닫기 원리. 클라이언트 인터페이스를 통해 어댑터와 함께 작동하는 한 기존 클라이언트 코드를 손상하지 않고 프로그램에 새로운 유형의 어댑터를 도입할 수 있다.

단점

  • 새로운 인터페이스와 클래스의 도입으로 코드의 전반적인 복잡성 증가. 때때로 서비스 클래스를 코드의 나머지 부분과 일치하도록 변경하는 것이 더 간단

Adapter in TypeScript

TypeScript의 패턴 사용

복잡도: ★☆☆

인기: ★★★

사용 예: TypeScript 코드에서 매우 일반적이다. 레거시 코드에 기반한 시스템에서 자주 사용되어 어댑터는 레거시 코드가 최신 클래스와 함께 작동하도록 한다.

식별: 다른 추상/인터페이스 유형의 인스턴스를 사용하는 생성자가 어댑터를 인식할 수 있다. 어댑터는 메서드에 대한 호출을 수신하면 매개 변수를 적절한 형식으로 변환한 다음 래핑된 객체의 하나 또는 여러 메서드로 호출을 전달한다.

index.ts

// 타겟은 클라이언트 코드에 의해 사용될 특정 영역을 인터페이스를 정의
class Target {
  public request(): string {
    return "Target: The default targets's behavior.";
  }
}

// Adaptee는 유용한 메서드들을 포함하고 있는데 기존 클라이언트 코드와 호환 불가
// 그래서 클라이언트 코드가 사용할 수 있게 어댑터가 필요하다.
class Adaptee {
  public specificRequest(): string {
    return '.eetpadA eht fo roivaheb laicepS';
  }
}

// 어댑터는 어댑티의 인터페이스를 만들고 타겟 인터페이스와 호환 가능하도록 만들어줌
class Adapter extends Target {
  private adaptee: Adaptee;

  constructor(adaptee: Adaptee) {
    super();
    this.adaptee = adaptee;
  }

  public request(): string {
    const result = this.adaptee.specificRequest().split('').reverse().join('');
    return `Adapter: (TRANSLATED) ${result}`;
  }
}

// 클라이언트 코드는 타겟의 인터페이스를 따르는 모든 클래스를 지원
function clientCode(target: Target) {
  console.log(target.request());
}

console.log('Client: I can work just fine with the Target objects:');
const target = new Target();
clientCode(target);

console.log('');

const adaptee = new Adaptee();
console.log("Client: The Adaptee class has a weird interface. See, I don't understand it:");
console.log(`Adaptee: ${adaptee.specificRequest()}`);

console.log('');

console.log('Client: But I can work with it via the Adapter:');
const adapter = new Adapter(adaptee);
clientCode(adapter);

실행 결과

요약

어댑터 패턴은 두 객체 사이에서 래퍼 역할
→ 한 객체가 다른 객체를 사용할 수 있도록

이를 구현하기 위해 한 객체의 인터페이스를 다른 객체가 이해할 수 있게 변환해야하며 때로는 양방향 호출이 가능하도록 양방향 어댑터를 만들 수 있다.

어댑터 패턴 구현 2가지 방법: 객체 어댑터(인터페이스 구현), 클래스 어댑터(다중 상속 이용)
클라이언트 인터페이스를 통해 어댑터와 작동하는 한 어댑터 클래스에 결합X

참고 사이트

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글