[Design Pattern] 어댑터 패턴

olwooz·2023년 2월 13일

Design Pattern

목록 보기
7/22
호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있게 해주는 구조 패턴

문제

주식 시장 모니터 앱 가정

  • 주식 데이터를 XML 포맷으로 다운받아서 차트와 다이어그램으로 보여주는 앱
  • 써드 파티 분석 라이브러리를 도입하고 싶은데, 라이브러리는 JSON 포맷의 데이터만 사용 가능한 상황

  • 라이브러리를 변경해 XML 데이터를 다룰 수 있게 할 수는 있지만 그렇게 하면 라이브러리에 의존하는 기존 코드들이 작동하지 않거나, 라이브러리 소스 코드 자체에 접근할 수 없는 상황

해결책

한 객체의 인터페이스를 다른 객체가 이해할 수 있게 변환하는 어댑터 사용

어댑터 - 객체 중 하나를 래핑해 복잡한 변환을 숨김, 래핑된 객체는 어댑터의 존재 모름

어댑터는 데이터를 다양한 포맷으로 변환할 수 있을 뿐만 아니라, 다른 인터페이스를 가진 객체들끼리 협업할 수 있게 도와줌

동작 방식:

1. 어댑터는 기존 객체들 중 하나와 호환되는 인터페이스를 받음
2. 기존 객체는 이 인터페이스를 사용해 안전하게 어댑터의 메서드 호출 가능
3. 호출을 받으면 어댑터는 요청을 또 다른 객체가 기대하는 포맷과 순서로 전달

위 문제에서는 코드가 직접적으로 다루는 분석 라이브러리의 모든 클래스에 XML-to-JSON 어댑터를 생성하고, 코드와 라이브러리가 해당 어댑터들을 통해서만 소통하게 해서, 어댑터가 호출을 받으면 들어오는 XML 데이터를 JSON으로 변환해 래핑된 분석 객체의 메서드에게 전달하면 해결

구조

객체 어댑터

객체 합성 구현 - 한 객체의 인터페이스를 구현하고 다른 객체를 래핑함, 대부분 언어로 구현 가능

1. 클라이언트 - 프로그램의 기존 비즈니스 로직을 가지고 있는 클래스

2. 클라이언트 인터페이스 - 클라이언트 코드와 협업하기 위해 다른 클래스들이 따라야 하는 프로토콜 묘사

3. 서비스 - 인터페이스가 호환되지 않아 클라이언트가 직접 사용할 수 없는 클래스

4. 어댑터 - 클라이언트와 서비스 모두를 다룰 수 있는 클래스
   - 클라이언트 인터페이스를 구현하고 서비스 객체를 래핑함
   - 어댑터 인터페이스를 통해 클라이언트로부터 호출을 받고, 
      래핑된 서비스 객체가 이해할 수 있는 포맷을 가진 호출로 변환
    
5. 클라이언트 코드는 클라이언트 인터페이스를 통해 어댑터를 다루는 한 concrete 어댑터 클래스와 결합되지 않음
   - 이 덕분에 기존 클라이언트 코드를 훼손하지 않고도 새로운 유형의 어댑터를 도입할 수 있음
   - 서비스의 인터페이스가 변경되거나 대체되는 상황에서 유용

클래스 어댑터

상속 구현 - 어댑터가 두 객체의 인터페이스를 동시에 상속, 다중 상속을 지원하는 C++같은 언어에서만 구현 가능

1. 클래스 어댑터 - 클라이언트와 서비스 모두의 행위를 상속받기 때문에 어떤 객체도 래핑할 필요가 없음
    - 어댑테이션은 오버라이드된 메서드에서 발생
    - 어댑터는 기존 클라이언트 클래스 대신 사용 가능

적용

기존 클래스를 사용하고 싶은데 인터페이스가 코드와 호환되지 않는 경우

- 어댑터 패턴은 레거시 클래스, 써드 파티 클래스 등 다른 인터페이스를 가진 클래스와 코드 사이의 
  통역사 역할을 하는 중간 레이어 클래스를 생성하게 해줌

수퍼클래스에 추가될 수 없는 특정 공통 기능들이 없는 여러 개의 기존 서브클래스를 재사용하고 싶은 경우

- 각각의 서브클래스를 확장해 새로운 자식 클래스들을 생성해 누락된 기능을 넣어줄 수도 있지만,
  이 경우에는 새로운 코드가 새로운 클래스들 전체에 중복됨
- 더 좋은 해결책은 누락된 기능을 어댑터 클래스에 넣고, 
  객체들을 어댑터로 감싸서 필요한 기능을 동적으로 얻을 수 있게 해주는 것
    - 대상 클래스들에게 공통된 인터페이스가 있고, 어댑터의 필드가 그 인터페이스를 따라야 함 
    → 데코레이터 패턴과 유사

구현방법

1. 호환되지 않는 인터페이스를 가진 두 개 이상의 클래스가 있어야 함
   - 변경이 불가능한, 유용한 서비스 클래스
   - 서비스 클래스를 사용해 이득을 볼 수 있는 클라이언트 클래스
    
2. 클라이언트 인터페이스를 선언하고 클라이언트가 어떻게 서비스와 소통해야 하는지 묘사

3. 어댑터 클래스를 생성하고 클라이언트 인터페이스를 따르게 함, 메서드들은 우선 비어있는 상태로 둠

4. 어댑터 클래스에 서비스 객체의 참조를 저장할 필드 추가
   - 보통 생성자로 이 필드를 초기화하지만, 가끔은 메서드 호출 시 어댑터에 전달해주는 게 편할 때도 있음
    
5. 클라이언트의 모든 메서드들을 어댑터 클래스에 구현
   - 어댑터는 대부분의 실제 작업을 서비스 객체에 위임하고, 인터페이스나 데이터 변환만 해야 함
    
6. 클라이언트는 클라이언트 인터페이스를 통해 어댑터 사용 
   → 클라이언트 코드에 영향을 주지 않고 어댑터 변경/확장 가능

장단점

장점

- SRP - 인터페이스나 데이터 변환 코드를 프로그램의 주요 비즈니스 로직과 분리 가능
- OCP - 클라이언트가 클라이언트 인터페이스를 통해 어댑터를 사용한다면 기존 코드를 훼손하지 않고 새로운 유형의 어댑터 도입 가능

단점

- 새로운 인터페이스들과 클래스들을 도입해야 해서 코드 복잡도 증가, 때로는 서비스 클래스를 코드에 맞게 변경하는 게 더 간단할 수도 있음

다른 패턴과의 관계

- 브리지 - 보통 사전에 설계되어 프로그램의 각 부분들을 독립적으로 개발 가능하게 함
  어댑터 - 대개 기존 프로그램과 함께 사용돼 원래 호환되지 않는 클래스가 잘 동작하도록 만듦
  
- 어댑터 - 기존 객체의 인터페이스 변경
  데코레이터 - 인터페이스를 변경하지 않고 객체 향상, 재귀 합성 지원
  
- 어댑터 - 래핑된 객체에 다른 인터페이스 제공
  프록시 - 같은 인터페이스 제공
  데코레이터 - 향상된 인터페이스 제공
  
- 퍼사드 - 기존 객체에 새로운 인터페이스 정의, 객체들의 전체적인 하위시스템과 동작
  어댑터 - 기존 인터페이스를 사용 가능하게 만듦, 대개 한 객체만 래핑함
  
- 브리지, 파사드, 전략, 어댑터 - 다른 객체에 작업을 위임하는 합성 기반 패턴이라는 점에서 유사하지만 
                             모두 다른 문제를 해결함

TypeScript 예제

/**
 * The Target defines the domain-specific interface used by the client code.
 */
class Target {
    public request(): string {
        return 'Target: The default target\'s behavior.';
    }
}

/**
 * The Adaptee contains some useful behavior, but its interface is incompatible
 * with the existing client code. The Adaptee needs some adaptation before the
 * client code can use it.
 */
class Adaptee {
    public specificRequest(): string {
        return '.eetpadA eht fo roivaheb laicepS';
    }
}

/**
 * The Adapter makes the Adaptee's interface compatible with the Target's
 * interface.
 */
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}`;
    }
}

/**
 * The client code supports all classes that follow the Target interface.
 */
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);
// Output.txt

Client: I can work just fine with the Target objects:
Target: The default target's behavior.

Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS

Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.

참고 자료: Refactoring.guru

0개의 댓글