현실에서 전가기기들의 어댑터가, 콘센트에서 제공되는 전류와 전압을 사용하려는 전가기기가 지원하는 규격에 맞게 바꿔주는 것처럼 Adapter란 주어진 객체를 필요한 상황에 맞게 바꾸는 패턴이다.
Adapter 패턴은 Wrapper 패턴이라고도 불리며, 기존의 객체를 포장하듯이 감싸서 새로운 객체를 만들어내는데 이 때 두 가지 형태의 Adapter 패턴이 존재하게 된다.
Adapter 패턴을 쓰게되면 이미 기존에 검증된 코드를 재사용할 수 있기 때문에 버그가 발생하더라도 디버깅해야하는 범위가 훨씬 좁아진다. 또, 목적이 바뀌어서 기존 코드를 수정해야하는 상황에서도 수정하기보다 Adapter 패턴을 이용해 덧씌울 수 있다면 역시 테스트해야하는 코드의 범위가 훨씬 줄어든다.
때문에 Adapter 패턴은 소프트웨어를 업데이트하면서 하위 호환성(Backward Compatibility)를 보장해야할 때 유용하게 쓰일 수 있다. 구버전의 코드를 그대로 유지한채로 신버전의 기능만 덧씌움으로서 여러 버전을 공존시키고 유지보수가 편해진다.
Adpater 패턴을 구성하는 객체들에는 4가지 역할이 있다.
Target(대상)
지금 필요한 기능들, 즉 메소드를 결정하는 객체
Adaptee(적응 대상자)
새 기능을 덧씌우려하는 기존의 객체
Adapter(적응자)
새 기능을 덧씌운 새로운 객체
Client(의뢰자)
Target을 호출하여 필요한 기능을 사용하는 객체
이를 토대로 주어진 문자열에 정해진 후처리를 거쳐 출력하는 프로그램을 만들어보자.
예를들어, Hello
를 입력했다면,
(Hello)
*Hello*
이렇게 두 가지의 문자열을 출력한다.
먼저 필요한 기능을 정의하는 Target을 만든다.
주어진 문자열에 괄호를 씌우는 printWeak
메소드와 별을 앞뒤로 붙이는 printStrong
메소드를 가지는 Print
인터페이스를 정의한다.
interface Print {
printWeak(): void
printStrong(): void
}
다음, showWithParen
메소드와 showWithAster
메소드를 이미 가지고 있는 기존에 정의되어있던 클래스 Banner
가 있다고 가정한다.
class Banner {
constructor(
private message: string,
) {
this.message = message;
}
showWithParen() {
console.log(`(${this.message})`);
}
showWithAster() {
console.log(`*${this.message}*`);
}
}
이제 Banner
를 상속받아서 Print
인터페이스를 충족하는 새 클래스 PrintBanner
클래스를 만든다.
class PrintBanner extends Banner implements Print {
constructor(message: string) {
super(message);
}
printWeak() {
this.showWithParen();
}
printStrong() {
this.showWithAster();
}
}
마지막으로 Client는 단순히 새로 생성한 PrintBanner
를 호출하는 객체일 뿐이다.
const banner = new PrintBanner('Hello');
banner.printWeak();
banner.printString();
흔히 있는 Java 예시에서는 이 부분을 Target이 인터페이스Interface인 경우와 추상클래스Abstract Class인 경우를 구분한다. 하지만 타입스크립트에서는 인터페이스와 추상클래스는 서로 공유되는 개념이기에 추상클래스도 앞서 본 상속 Adapter처럼 implements
를 사용할 수 있다.
하지만 인스턴스 위임 Adapter의 요지는 하나의 클래스가 두 가지 이상의 클래스를 상속받을 수는 없는 상황에서 둘 중 하나가 곧 Target이 되는 경우를 상정한다.
따라서, 아래 예시가 부적절할 수도 있지만 그 내용보다는 Adapter 클래스가 생성되는 방식에만 초점을 맞추기로 한다.
abstract class Print {
abstract printWeak(): void
abstract printStrong(): void
}
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();
}
}
대부분의 경우에는 위임의 형태로 사용하는 것이 편하다. 상위 클래스를 구성하는 프로퍼티와 메소드가 많을수록 이를 상속받아 사용하는데 제약이나 어려움이 따를 수 있기 때문이다.
Java언어로 배우는 디자인 패턴 입문 - 쉽게 배우는 Gof의 23가지 디자인패턴 (영진닷컴)
Javascript MDN
Typescript Documentation