본문 바로가기
Engineering/SW Design

[디자인 패턴] 구조패턴 - 어댑터

by 쿨쥰 2024. 3. 25.

구조패턴 - 어댑터

 


이전 글 : 구조패턴 - 싱글턴

2024.03.11 - [Engineering/SW Design] - [디자인 패턴] 생성패턴 - 싱글턴

 

[디자인 패턴] 생성패턴 - 싱글턴

생성패턴 - 싱글턴 이전 글 : 생성패턴 - 프로토타입 2024.03.03 - [Engineering/SW Design] - [디자인 패턴] 생성패턴 - 프로토타입 [디자인 패턴] 생성패턴 - 프로토타입 생성패턴 - 프로토타입 이전 글 : 생

skidrow6122.tistory.com

 

 

요약

인터페이스를 사용함으로써 각각 구현체의 세부로직과 변경에 관계없이 일관성있는 사용이 가능하게 만들어주는 패턴이다.

즉, 서로 일치하지 않는 인터페이스를 갖는 클래스들을 함께 동작시키기 위해 클래스의 인터페이스를 사용자가 기대하는 인터페이스의 형태로 변환 시켜준다.

특정 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 목적의 다른 인터페이스로 변환 해줌으로서 이런 어댑터를 이용하면 인터페이스 호환성 문제로 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있게 만들어 준다.

이는 특정 인터페이스를 지원하지 않는 대상 객체(adaptee)를 인터페이스를 지원하는 adapter 에 집어넣어서 사용된다.

 

키워드 : 변환 / Service-Adaptee / Adapter 에 adaptee 객체를 파라미터로 넣음

 

 

문제

하나의 중앙제어 시스템이 있다고 가정하고, 이 시스템은 여러 소스에서로 부터의 데이터를 XML 형태로 다운로드 할 수 있다고 해보자.

운영 중 고도화 작업을 통해 특정 라이브러리를 쓰기로 했는데, 이 라이브러리가 JSON 형태의 포맷만 지원하는 상황이다.

(실제 개발하다보면 허다하게 일어나는 일이다)

당연히 이 상황에서 해당 라이브러리를 그대로 사용할 수 없다.

시스템은 XML 데이터만 받아서 처리할 수 있기 때문이다.

사실 이 라이브러리를 XML 과 호환 되도록 native code를 수정할 수 도 있지만 자칫 코어를 건드렸다가 해당 라이브러리에 의존하는 기존 코드가 손상될 가능성이 있으므로 위험하다.

 

 

해결책

출장갈때 마다 찾게 되는 ‘돼지코’ 를 떠올려 보자.

220v 와 100v 를 호환시켜주는 어댑터 역할을 하는 이 돼지코는 하나의 어댑터다.

이 돼지코 역할을 하는 어댑터 객체가 바로 한 객체의 인터페이스를 다른 객체가 이해할 수 있도록 변환해 준다.

어댑터는 변환의 복잡성을 은닉하기 위하여 객체 중 하나를 포장한다.

포장된 객체는 어댑터를 물론 인식하지도 못할 것이다.

어댑터는 데이터를 다양한 형식으로 변환할 수 있을 뿐만 아니라 다른 인터페이스를 가진 객체들이 협업하는데에도 도움을 줄 수 있으며 아래와 같이 작동한다.

  • 어댑터는 기존에 있던 객체 중 하나와 호환되는 인터페이스를 받는다.
  • 이 인터페이스를 사용하면 기존 객체는 어댑터의 메서드들을 안전하게 호출할 수 있다.
  • 호출을 수신하면 어댑터는 이 요청을 두 번째 객체에 해당 객체가 예상하는 형식과 순서대로 전달한다.

다시 문제의 시스템으로 돌아가보자.

XML → Json 변환 어댑터를 통해서 호출을 받으면 JSON 포맷 구조로 변환후 해당 호출을 포장된 분석 객체의 메서드들에게 전달 해준다.

 

 

구조

  • 객체 어댑터
    • 이 패턴은 객체 합성 원칙을 사용한다.
    • 어댑터는 한 객체의 인터페이스를 구현하고 다른 객체는 포장한다.
     

  • 클래스 어댑터
    • 이 패턴은 상속을 활용하고, 어댑터는 동시에 두 객체의 인터페이스를 상속받는데 C++ 같은 다중 상속 지원 언어에서만 활용가능하므로 생략 한다.

 

 

Hands on

  • Client 는 Target 인터페이스를 구현한 Adaptee 가 필요하다
  • Adaptee 는 Target 인터페이스를 구현하고 있지않다
    • Adaptee 는 이미 개발이 완료되었으므로 이를 변경하기가 쉽지 않다.
// Adaptee 는 이미 개발이 완료됐고, 수정하기 곤란한 상황이다.
class Adaptee {
  Response specificRequest() {
    return new Response();
  }
}

// 클라이언트가 사용하려는 인터페이스.
interface Target {
  Response request();
}

즉, 이와 같은 상황에서..

 

  • Adapter 클래스를 만든 후, Adaptee를 내부에 갖고 있게 한다.
    • Adapter는 Target을 구현한다.
    • Adapter의 request()는 Adaptee의 specificRequest()를 감싼다.
// Adapter는 Adaptee를 감싸고 있으며, Target 인터페이스를 구현한다.
class Adapter implements Target {
  private final Adaptee adaptee;

  public Adapter(Adaptee adaptee) {
    this.adaptee = adaptee;
  }

  @Override
  public Response request() {
    return this.adaptee.specificRequest();
  }
}

 

이후 client 에서는 아래와 같이 Adaptee를 Adapter에 집어 넣어서 Target 인터페이스로 사용할 수 있다.

Target target = new Adapter(adaptee)

 

결과적으로 클라이언트는 어떤 서비스를 사용할지 스스로 판단하여 필요한 로직을 추가 구현할 필요없이,

어댑터 인터페이스만 사용하면 된다.

 

 

핵심

  • 어댑터 클래스는 기존 클래스를 사용하고 싶지만 그 인터페이스가 나머지 코드와 호환되지 않을때 사용
  • 기존 코드와 레거시 클래스, 타사 클래스, 특이 인터페이스가 있을때 돼지코 역할을 할 수 있는 중간 레이어 클래스로 이용
  • 브릿지 패턴은 일반적으로 사전에 예측되어 시스템의 다양한 부분을 독립적으로 개발할 수 있게 지원하는 반면, 어댑터 패턴은 예기치 않은 상황으로 인해 기존 시스템과 호환되지 않는 일부 클래스들이 잘 작동하도록 만들어 줌
  • 어댑터는 기존 객체의 인터페이스를 변경하는 반면, 데코레이터는 객체를 해당 객체의 인터페이스를 변경하지 않고 개선 함
  • 어댑터는 다른 인터페이스를, 프록시는 같은 인터페이스를, 데코레이터는 개선된 인터페이스를 포장된 객체에 제공

 


간단히 hands on 을 통해 어댑터 패턴을 살펴 보았다.

구조패턴은 인터페이스 조합 방식에 따라 데코레이터, 프록시 패턴과 유사한데 나머지도 이어서 살펴볼 예정이다.

댓글