Typescript로 다시 쓰는 GoF - Adapter

아홉번째태양·2023년 8월 3일
0

Adapter란?

현실에서 전가기기들의 어댑터가, 콘센트에서 제공되는 전류와 전압을 사용하려는 전가기기가 지원하는 규격에 맞게 바꿔주는 것처럼 Adapter란 주어진 객체를 필요한 상황에 맞게 바꾸는 패턴이다.

Adapter 패턴은 Wrapper 패턴이라고도 불리며, 기존의 객체를 포장하듯이 감싸서 새로운 객체를 만들어내는데 이 때 두 가지 형태의 Adapter 패턴이 존재하게 된다.

  1. 클래스 상속을 이용한 패턴
  2. 인스턴스 위임을 이용한 패턴

왜 쓸까?

Adapter 패턴을 쓰게되면 이미 기존에 검증된 코드를 재사용할 수 있기 때문에 버그가 발생하더라도 디버깅해야하는 범위가 훨씬 좁아진다. 또, 목적이 바뀌어서 기존 코드를 수정해야하는 상황에서도 수정하기보다 Adapter 패턴을 이용해 덧씌울 수 있다면 역시 테스트해야하는 코드의 범위가 훨씬 줄어든다.

때문에 Adapter 패턴은 소프트웨어를 업데이트하면서 하위 호환성(Backward Compatibility)를 보장해야할 때 유용하게 쓰일 수 있다. 구버전의 코드를 그대로 유지한채로 신버전의 기능만 덧씌움으로서 여러 버전을 공존시키고 유지보수가 편해진다.



Adapter 구현

Adpater 패턴을 구성하는 객체들에는 4가지 역할이 있다.

  1. Target(대상)
    지금 필요한 기능들, 즉 메소드를 결정하는 객체

  2. Adaptee(적응 대상자)
    새 기능을 덧씌우려하는 기존의 객체

  3. Adapter(적응자)
    새 기능을 덧씌운 새로운 객체

  4. Client(의뢰자)
    Target을 호출하여 필요한 기능을 사용하는 객체

이를 토대로 주어진 문자열에 정해진 후처리를 거쳐 출력하는 프로그램을 만들어보자.

예를들어, Hello를 입력했다면,

(Hello)
*Hello*

이렇게 두 가지의 문자열을 출력한다.



클래스 상속 Adapter

Target

먼저 필요한 기능을 정의하는 Target을 만든다.

주어진 문자열에 괄호를 씌우는 printWeak 메소드와 별을 앞뒤로 붙이는 printStrong 메소드를 가지는 Print 인터페이스를 정의한다.

interface Print {
  printWeak(): void
  printStrong(): void
}

Adpatee

다음, showWithParen 메소드와 showWithAster 메소드를 이미 가지고 있는 기존에 정의되어있던 클래스 Banner가 있다고 가정한다.

class Banner {
  constructor(
    private message: string,
  ) {
    this.message = message;
  }

  showWithParen() {
    console.log(`(${this.message})`);
  }

  showWithAster() {
    console.log(`*${this.message}*`);
  }
}

Adapter

이제 Banner를 상속받아서 Print 인터페이스를 충족하는 새 클래스 PrintBanner 클래스를 만든다.

class PrintBanner extends Banner implements Print {
  constructor(message: string) {
    super(message);
  }

  printWeak() {
    this.showWithParen();
  }

  printStrong() {
    this.showWithAster();
  }
}

Client

마지막으로 Client는 단순히 새로 생성한 PrintBanner를 호출하는 객체일 뿐이다.

const banner = new PrintBanner('Hello');
banner.printWeak();
banner.printString();


인스턴스 위임 Adapter

흔히 있는 Java 예시에서는 이 부분을 Target이 인터페이스Interface인 경우와 추상클래스Abstract Class인 경우를 구분한다. 하지만 타입스크립트에서는 인터페이스와 추상클래스는 서로 공유되는 개념이기에 추상클래스도 앞서 본 상속 Adapter처럼 implements를 사용할 수 있다.

하지만 인스턴스 위임 Adapter의 요지는 하나의 클래스가 두 가지 이상의 클래스를 상속받을 수는 없는 상황에서 둘 중 하나가 곧 Target이 되는 경우를 상정한다.

따라서, 아래 예시가 부적절할 수도 있지만 그 내용보다는 Adapter 클래스가 생성되는 방식에만 초점을 맞추기로 한다.


Target

abstract class Print {
  abstract printWeak(): void
  abstract printStrong(): void
}

Adapter

Adaptee는 이전과 똑같은 Banner이고, Print가 구현해야하는 메소드를 가진 Target이므로 Print를 상속받고 Banner는 새 객체 안에서 인스턴스를 생성한다.

class PrintBanner extends Print {
  private banner: Banner;

  constructor(message: string) {
    super();
    this.banner = new Banner(message);
  }

  printWeak() {
    this.banner.showWithParen();
  }

  printStrong() {
    this.banner.showWithAster();
  }
}

Adapter, 상속 vs 위임?

대부분의 경우에는 위임의 형태로 사용하는 것이 편하다. 상위 클래스를 구성하는 프로퍼티와 메소드가 많을수록 이를 상속받아 사용하는데 제약이나 어려움이 따를 수 있기 때문이다.




참고자료

Java언어로 배우는 디자인 패턴 입문 - 쉽게 배우는 Gof의 23가지 디자인패턴 (영진닷컴)
Javascript MDN
Typescript Documentation

0개의 댓글