어댑터는 호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있도록 하는 구조적 디자인 패턴입니다.
- Client: 프로그램의 기존 비즈니스 로직을 포함하는 클래스, 우리가 작성하는 클래스가 됨
- Client Interface: 다른 클래스들이 클라이언트 코드와 공동 작업할 수 있도록 따라야 하는 프로토콜, 공통의 동작을 정의함
- Service: 타사 또는 레거시의 유용한 클래스, 호환성을 위해 Client Interface를 거쳐 사용해야 함
- Adapter: 클라이언트와 서비스 양쪽에서 작동할 수 있는 클래스, 클라이언트 인터페이스를 구현하여 서비스 객체를 래핑함
- Wrapper 패턴으로도 불리며,
어댑터
는 한 객체의 인터페이스를 다른 객체가 이해할 수 있도록 변환하는 객체를 의미함 - 구현체로서는
객체 어댑터
,클래스 어댑터
두가지가 존재
합성
을 사용하는 구현어댑터
는 한 객체의 인터페이스를 구현하고, 다른 객체는 래핑함
/**
* 직접 컨트롤할수 없는 서드파티 라이브러리 등의 객체
* 서드파티 라이브러리가 아니더라도, 형식이 맞지 않는 다른 어떠한 클래스도 여기에 해당될 수 있음
* 예시로서는 숫자를 받아 10을 더해 특정 포맷으로 출력하는 기능을 제공
*/
class Service {
public method(data: number) {
console.log(`${data} + 10 = ${data + 10}`);
}
}
/**
* 다른 클래스들이 클라이언트와 공동 작업할 수 있도록 하는 공통의 구조
* 예시로서 숫자 대신 문자열을 전달해야 동작 가능하도록 어댑터 클래스를 구현할 것임
*/
interface IClient {
method(data: string): void;
}
/**
* 클라이언트와 클래스 양쪽에서 사용가능한 클래스
* 서비스 객체를 래핑하며 클라이언트 인터페이스를 구현
*/
class Adapter implements IClient {
service: Service = new Service();
public method(data: string) {
if (typeof data === 'number') return this.service.method(data);
const convertedData = parseInt(data);
this.service.method(convertedData);
}
}
/**
* Adapter 클래스를 실제로 사용하는 Client 객체
* 하지만 구상 클래스 Adapter 가 아닌 인터페이스 IClient 에 의존함
*/
class Client {
some: IClient = new Adapter();
public static main() {
const client = new Client();
client.some.method("4"); // 4 + 10 = 14
}
}
Client.main();
상속
을 사용하는 구현- 언어 디자인이
다중 상속
을 허용해야 함 어댑터
클래스가클라이언트 인터페이스
를 구현하며 내부에서비스
객체를 가지지 않음어댑터
클래스는서비스
객체와 다른 객체를 동시에 상속받아 사용됨
다중 상속을 허용하는 언어들: C++, Python, Perl, Eiffel, Common Lisp, Dylan, Tcl, Lua - by ChatGPT
- 타입스크립트는 다중 상속을 지원하지 않아 가장 신택스가 비슷한(아마도) C++ 로 작성
#include <iostream>
#include <string>
/**
* 서드파티 라이브러리에서 제공하는 클래스
* 메소드의 파라미터는 int 타입
*/
class Service {
public:
void special_method(int special_data) {
std::cout << special_data << " + 10 = " << special_data + 10 << std::endl;
}
};
/**
* 우리 애플리케이션의 클래스
* 메소드의 파라미터는 문자열 타입
*/
class ExistingClass {
public:
void method(std::string data) {
int i_data = std::stoi(data);
std::cout << data << " + 10 = " << i_data + 10 << std::endl;
}
};
/**
* 기존 ExistingClass의 메소드를 오버라이드하여 Service 클래스를 문자열 타입도
* 사용가능하도록 만들어줌
*/
class Adapter : public Service, public ExistingClass {
public:
void method(int data) {
Service::special_method(data);
}
// 실제 Adaptation이 발생하는 오버라이드된 메소드
void method(std::string data) {
int special_data = std::stoi(data);
Service::special_method(special_data);
}
};
class Client {
private:
ExistingClass* _ec;
public:
Client(ExistingClass* ec) : _ec(ec) {}
~Client() { delete this->_ec; }
void main() {
this->_ec->method("4"); // 4 + 10 = 14
}
};
int main() {
// ExistingClass 타입에 Adapter 클래스 할당 가능
Client* client = new Client(new Adapter());
client->main();
delete client;
}
- 칸반보드 형태의 투두리스트 앱
- 이때 supabase 데이터베이스를 사용하여 앱의 백엔드를 지원한다면
// src/Kanban/infrastructure/repository.ts
// Service 클래스
// 우리가 통제할 수 없는 인터페이스
export const supabaseClient: SupabaseClient = createClient(
import.meta.env.VITE_SUPABASE_URL as string,
import.meta.env.VITE_SUPABASE_KEY as string,
{ auth: { storage: window.localStorage } }
);
// Client interface
// SupabaseClient 타입 객체를 우리 애플리케이션에서 사용할 수 있도록 어댑터의 역할을 함
export interface ColumnRepositoryAdapter {
save(column: ColumnDto): Promise<void>;
saveAll(columns: ColumnDto[]): Promise<void>;
find(id: string): Promise<ColumnDto | null>;
findAll(): Promise<ColumnDto[]>;
findAllWithCards(): Promise<(ColumnDto & { cards: CardDto[] })[]>;
}
// Adapter 클래스
// 우리가 사용할 포맷을 따르는 객체, 내부에 Service 클래스를 가지고 사용할 수 있는 형태로 바꿔줌
export class ColumnRepositoryAdapterImpl implements ColumnRepositoryAdapter {
private client: SupabaseClient = supabaseClient;
// ...
public async save(column: ColumnDto) {
// ...
}
public async saveAll(columns: ColumnDto[]) {
// ...
}
public async find(id: string) {
// ...
}
public async findAll() {
// ...
}
public async findAllWithCards() {
const result = await this.client.from(this.tableName).select(`
id,
name,
cards: "${this.cardsTableName}" (*)
`);
if (result.error) throw result.error;
return result.data as (ColumnDto & { cards: CardDto[] })[];
}
}
// src/Kanban/domain/Column.ts
// 클라이언트 클래스
// Adapter 클래스에 해당하는 ColumnRepositoryAdapterImpl 클래스를 실제로 사용하는 코드
export class ColumnService {
private readonly repository: ColumnRepositoryAdapter = new ColumnRepositoryAdapterImpl();
public async save(column: Column) {
// ...
}
public async saveAll(columns: Column[]) {
// ...
}
public async find(name: string) {
// ...
}
public async findAll() {
// ...
}
public async findAllWithCards() {
const columns = await this.repository.findAllWithCards();
return columns.map(column => ({
...column,
cards: column.cards.map(Card.fromDto),
}));
}
}
- Full code reference: https://github.com/rudy3091/app
- 일단은 그렇지 않다고 생각중
- ref: nestjs의 AbstractHttpAdapter는 express와 fastify의 전혀 다른 api를 하나로 통합
- ref2: axios의 adapter는
node:http
모듈과xhr
의 인터페이스를 하나로 통합 - 엄밀하게 객체지향의 관점에서 따졌을 때에도 적절한 예시인지?