적응자 패턴
adapter란?
- adapter는 다른 전기나 기계 장치를 서로 연결해서 작동할 수 있도록 만들어 주는 결합 도구를 뜻합니다.
디자인 패턴 분류
- 디자인 패턴에는 생성, 구조, 행위, 3가지 분류가 있습니다.
- 어댑터 패턴은
구조
에 대한 패턴입니다.
구조 패턴
- 구조 패턴이란 작은 클래스들을
상속
과합성
을 이용하여 더 큰 클래스를 생성하는 방법을 제공하는 패턴입니다. - 서로 독립적으로 개발한 클래스 라이브러리를 마치
하나인 것
처럼 사용할 수 있습니다. 또, 여러 인터페이스를 합성(Composite)하여 서로 다른 인터페이스들의 통일된 추상을 제공합니다. - 구조 패턴의 중요한 포인트는 인터페이스나 구현을 복합하는 것이 아니라 객체를
합성
하는 방법을 제공한다는 것입니다. 이는 컴파일 단계에서가 아닌런타임 단계
에서 복합 방법이나 대상을 변경할 수 있다는 점에서 유연성을 갖습니다.
호환성
을 위한 패턴
- 일상 생활에서와 동일하게 어떤 인터페이스를 클라이언트에서 요구하는 형태의 인터페이스에 적응시켜주는 역할을 한다.
- 어뎁터 패턴은 B를 A처럼 포장하여 A로 사용할 수 있게 하는 패턴입니다.
- 이미 잘 구축되어 있는 것을 새로운 어떤 것이 사용 할 때 양쪽 간의 호환성을 유지해주기 위해 사용합니다.
- 한 클래스의 인터페이스를 클라이언트에서 사용하고자하는 다른 인터페이스로 변환합니다.
- 어댑터를 구현할 때는 타켓 인터페이스의 크기와 구종에 따라 코딩해야 할 분량이 결정됩니다.
활용 상황
- 이미 만든 것을 재사용하고자 하나 이 재사용 가능한 라이브러리를 수정할 수 없을 때 사용합니다.
- 서버 코드에 손대지 못한다면 어댑터 패턴을 사용해 API를 변경한 후 잠금을 추가 합니다.
- 기존의 코드에 새로운 코드(써드파티 라이브러리 등)을 연동하여 사용하고 싶은데, 두 코드의 인터페이스가 달라, 이를 하나로 통일하여 사용하고 싶을 때 사용합니다.
- 리팩토링 없이도 기존의 클래스를 이용해 새로운 클래스를 만들 수 있습니다.
- [객체 적응자] 이미 존재하는 여러 개의 서브 클래스를 사용해야 하는데, 이 서브클래스들의 상속을 통해서 이들의 인터페이스를 다 개조 한다는 것이 현실성이 없을 때, 객체 적응자를 써서 부모 클래스의 인터페이스를 변경하는 것이 더 바람직합니다.
장점
- 관계가 없는 인터페이스를 같이 사용할 수 있습니다.
- 기존 클라이언트 단의 코드 수정을 최소화 할 수 있습니다.
- 클래스 재활용성이 증가합니다.
- 클라이언트는 연동부분을 몰라도, 새로운 코드의 기능을 일관되게 사용 가능합니다.
장점을 조금 더 길게 설명해 보겠습니다.
- 기존 클래스의 소스코드를 수정해서 인터페이스에 맞추는 작업보다는 기존 클래스의 소스 코드의 수정을 전혀 하지 않고 타겟 인터페이스에 맞춰서 동작을 가능하게 합니다. 즉, 기존 클래스의 명세만 알면 얼마든지 새로운 클래스를 작성할 수 있습니다. 이를 통해 소스코드가 간단해지고 유지볻보수도 원할하게 하는 이점이 있습니다.
- 어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있습니다. 이로 인해 호환되지 않는 인터페이스를 사용하는 클라이언트를 그대로 활용할 수 있습니다.
- 어댑터 패턴을 통해 클라이언트와 구현된 인터페이스를 분리시킬수 있으며, 향후 인터페이스가 바뀌더라도 그 변경 내역은 어댑터에
캡슐화
되기 때문에 클라이언트는 바뀔 필요가 없어집니다. - 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리합니다. 코드 가독성이 높아지며, 경계 인터페이스를 사용하는 일관성도 높아지며, 외부 패키지가 변했을 때 변경할 코드도 줄어듭니다.
단점
- 어댑터 클래스에서 통일 시켜주는 부분을 구현해야 합니다.
객체지향 원칙
- 어댑터 패턴에는 여러 객체지향 원칙이 반영되어 있습니다. 어댑터를 새로 바뀐 인터페이스로 감쌀 때는 객체
구성(composition)
을 사용합니다. 이런 접근법을 쓰면 어탭티의 어떤 서브클래스에 대해서도 어댑터를 쓸 수 있다는 장점이 있지요.
클라이언트에서 어댑터를 사용하는 방법에 대해 살펴 보겠습니다.
- 클라이언트에서 타겟 인터페이스를 사용하여 메소드를 호출함으로써 어댑터에 요청합니다.
- 어댑터에서는 어댑티 인터페이스를 사용하여 그 요청을 어댑티에 대한 하나 이상의 메소드를 호출로 변환합니다.
- 클라이언트에서는 호출 결과를 받긴 하지만 중간에 어댑터가 껴 있는지는 전혀 알지 못합니다.
- 클라이언트 -> request() -> 어댑터 - specificRequest() -> 어댑티.
- 클라이언트에서는 Target Interface를 호출하는 것처럼 보입니다. 하지만 클라이언트의 요청을 전달받은 (Target Interface 를 구현한) Adapter 는 자신이 감싸고 있는 Adaptee에게 실질적인 처리를 위임합니다. Adapter가 Adaptee를 감싸고 있는 것 때문에
Wrapper 패턴
이라고도 불립니다.
어댑터에는 두종류가 있다.
클래스 어댑터 패턴
- 상속을 이용한 어댑터 패턴입니다.
- 클래스 어댑터에서는 어댑터를 만들 때 타겟과 어댑티 모두의 서브클래스로 만들고, 객체 어댑터에서는 구성을 통해서 어댑티에 요청을 전달한다는 점을 제외하면 별 다르 차이점이 없습니다.
- 특정 어댑티 클래스에만 적용된다는 단점이 있습니다. 대신 어댑티 전체를 다시 구현하지 않아도 된다는 장점이 있습니다. 그리고 서브클래스기 때문에 어댑티의 행동을 오버라이드할 수 있습니다.
- 클래스 어댑터 패턴은 다중 상속을 허용하는 프로그래밍 언어에서만 가능한 패턴이다.
객체 어댑터 패턴
- 구성(composition)을 사용하기 때문에 더 뛰어납니다. 어댑티 클래스 뿐 아니라 그 서브 클래스에 대해서도 어댑터 역할을 할 수 있습니다.
- 상속이 아닌 구성을 활용합니다. 상속을 이용하면 코드 분량을 줄일 수 있긴 하겠지만, 구성을 이용하더라도 어댑티한테 필요한 일을 시키기 위한 코드만 만들면 되기 때문에 별로 코드가 많이 필요한 건 아닙니다. 유연성을 확보할 수 있습니다.
클래스 어댑터 vs 객체 어댑터
- 클래스 어댑터는
다중 상속이 불가능
합니다. - 클래스의 상속 기능을 사용할 경우
조합 폭발이 일어나서 제어 불가능
합니다.- 상속의 남용으로 하나의 기능을 추가하기 위해 필요 이상으로 많은 수의 클래스를 추가해야 하는 경우를 가르켜
클래스 폭발(class explosion)
문제 또는조합 폭발(combinational explosion)
문제라고 부릅니다. - 런타임에 타입선택(세트)
- 추상메소드로 의존성 역전
- 상속의 남용으로 하나의 기능을 추가하기 위해 필요 이상으로 많은 수의 클래스를 추가해야 하는 경우를 가르켜
- 객체 어댑터의 경우
의존성 폭발이 일어나지만 제어 가능
합니다.- 런타임에 합성(조립)
- 추가 인터페이스로 의존성 분산
유사 패턴과의 비교
- 어댑터 : 한 인터페이스를 다른 인터페이스로 변환
- 데코레이터 : 인터페이스는 바꾸지 않고 책임(기능)만 추가
- 퍼사드 : 인터페이스를 간단하게 바꿈
퍼사드 vs 어댑터
- 퍼사드는 인터페이스를 단순화 시킬 뿐 아니라 클라이언트와 구성요소들로 이루어진 서브시스템을 분리시키는 역할도 합니다.
- 퍼사드와 어댑터는 모두 여러 개의 클래스를 감쌀 수 있습니다. 하지만 퍼사드는 인터페이스를 단순화시키기 위한 용도로 쓰이는 반면, 어댑터는 인터페이스를 다른 인터페이스로 변환하기 위한 용도로 쓰입니다.
- 퍼사드는 한 객체에 다른 인터페이스를 구현한다는 면에서 어댑처와 유사합니다. 그러나 퍼사드는 새로운 인터페이스를 생성하지만 어댑터는 기존 인터페이스를 재활용한다는 점이 다릅니다.
어댑터와 프록시의 차이
- 공통점 : 클라이언트와 다른 객체 사이에 끼어들어서 클라이언트로부터 요청을 받아와서 다른 객체한테 전달해주는 역할을 합니다.
- 어댑터 패턴 : 다른 객체의 인터페이스를 바꿔줍니다.
- 프록시 패턴 : 똑같은 인터페이스를 사용합니다.
- 보호 프록시 : 보호 프록시에서는 클라이언트의 역할에 따라서 객체에 있는 특정 메소드에 대한 클라이언트 접근을 제어합니다. 그러다 보니 보호 프록시에서는 클라이언트한테 인터페이스의 일부분만을 제공할 수 있습니다. 이런 점은 어댑터하고 비슷하다고 할 수 있습니다.
구성
Target Interface
Adapter 가 구현(implements) 하는 인터페이스이다. 클라이언트는 Target Interface 를 통해 Adaptee 인 써드파티 라이브러리를 사용하게 된다.
interface MediaPlayer {
play: (fileName: string) => void;
}
class MP3 implements MediaPlayer {
play(fileName) {
console.log(`MP3 : ${fileName}`);
}
}
Adaptee
- 써드파티 라이브러리나 외부시스템을 의미한다.
interface MediaPackage {
playFile: (fileName: string) => void;
}
class MP4 implements MediaPackage {
playFile(fileName) {
console.log(`MP4 : ${fileName}`);
}
}
class MKV implements MediaPackage {
playFile(fileName) {
console.log(`MKV : ${fileName}`);
}
}
Adapter
- Client 와 Adaptee 중간에서 호환성이 없는 둘을 연결시켜주는 역할을 담당한다. Target Interface 를 구현하며, 클라이언트는 Target Interface 를 통해 어댑터에 요청을 보낸다. 어댑터는 클라이언트의 요청을 Adaptee 가 이해할 수 있는 방법으로 전달하고, 처리는 Adaptee 에서 이루어진다.
// 객체 어댑터
// MP3를 상속하여 사용하는 것도 가능
class FormatAdapter implements MediaPlayer {
media: MediaPackage;
constructor(media: MediaPackage) {
this.media = media;
}
play(fileName) {
console.log('Using Adapter');
this.media.playFile(fileName);
}
}
// 클래스 어댑터
// MP3를 상속하여 사용하는 것은 불가능
class FormatAdapterMP4 extends MP4 implements MediaPlayer {
play(fileName) {
console.log('Using Adapter');
this.playFile(fileName);
}
}
class FormatAdapterMKV extends MKV implements MediaPlayer {
play(fileName) {
console.log('Using Adapter');
this.playFile(fileName);
}
}
Client
- 써드파티 라이브러리나 외부시스템을 사용하려는 쪽이다.
// 객체 어댑터
const playerMP3 = new MP3();
playerMP3.play('file.mp3');
const playerMP4 = new FormatAdapter(new MP4());
playerMP4.play('file.mp4');
const playerMKV = new FormatAdapter(new MKV());
playerMKV.play('file.mkv');
// 클래스 어댑터
const playerMP3 = new MP3();
playerMP3.play('file.mp3');
const playerMP4 = new FormatAdapterMP4();
playerMP4.play('file.mp4');
const playerMKV = new FormatAdapterMKV();
playerMP4.play('file.mkv');
어댑터 패턴 정리
- Adaptee를 감싸고, Target Interface만을 클라이언트에게 드러냅니다.
- Target Interface를 구현하여 클라이언트가 예상하는 인터페이스가 되도록 Adaptee의 인터페이스를 간접적으로 변경합니다.
- Adaptee가 기대하는 방식으로 클라이언트의 요청을 간접적으로 변경합니다.
- 호환되지 않는 우리의 인터페이스와 Adaptee를 함께 사용할 수 있습니다.