어댑터 패턴(Adapter Pattern)
어댑터패턴은 일치하지 않는 인터페이스를 가져 호환성이 없는 객체들을 같이 동작시킬 수 있는 구조적 디자인 패턴이다.
클래스의 인터페이스를 사용자가 기대하는 다른 인터페이스로 변환하는 패턴이다.
클라이언트가 사용하는 인터페이스를 따르지 않는 기존 코드를 재사용할 수 있게 해준다
- 다음과 같이 불린다.
- 어댑터(Adapter, Adaptor)
- 적응자
- 래퍼(Wrapper)
어댑터 패턴이 해결하려고 하는 문제는 다음과 같다
- 현재 사용하고 있는 라이브러리가 더 이상 요구에 부합하지 않아 재작성하거나, 다른 라이브러리를 사용해야 할 때가 있다.
- 다른 라이브러리를 사용하는 경우 Adapter 패턴을 이용해 기존 코드를 가능한 적게 변경하면서 새로운 라이브러리로 교체할 수 있다.
- 기존 버전의 클래스가 다른 인터페이스를 가지도록, 예를 들어 메소드의 파라미터를 변경하거나 반환 값의 타입을 변경해야 한다면?
- 기존 버전과 새로운 버전의 메소드를 모두 갖는 비대한 클래스를 만들 수도 있을 것이다.
- 하지만 대부분의 경우 심플한 클래스(새로운 버전)를 만들고 Adapter를 이용하여 새로운 객체가 기존 코드에 존재하는 것처럼 보이게 하는 것이 더 낫다.
다이어그램
- Client는 Target 인터페이스를 구현한 Adaptee가 필요하다.
- Adaptee는 Target 인터페이스를 구현하지 않고 있다. (우리가 구현한 구현체이다.)
- Adaptee는 이미 개발이 완료되어 사용중이며, 변경하는 것이 적절하지 않은 상황이다.
Target과 Adaptee 사이를 매궈주는 Adpater를 구현하는것이 우리의 목표이다
// Adaptee 는 이미 개발이 완료됐고, 변경하기 곤란한 상황이다.
class Adaptee {
Response specificRequest() {
return new Response();
}
}
// 클라이언트가 사용하려는 인터페이스.
interface Target {
Response request();
}
// 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();
}
}
- Adapter클래스를 만들고, Adaptee를 내부에 갖고 있게 한다.
- Adapter는 Target을 구현한다.
- Adapter의
request()
는 Adaptee의specificRequest()
를 감싸고 있다.
Adaptee는 인터페이스 Target 을구현하지 않으므로 Target에서 사용할 수 없다.
이 때, Target을 구현하고 Adaptee를 내부로 갖고 있는 Adapter 클래스를 정의해서 대신 사용할 수 있다.
Target target = new Adapter(adaptee)
즉 Adapter는 우리가 사용하려는 인터페이스를 상속받지 못한 객체를 내부로 포함한 다음,
인터페이스의 연산을 호출하면, 상속받지 못한 클래스의 연산을 대신 호출하고, 그 반환값을 인터페이스의 연산 호출 값으로 돌려주면 되는것이다.
어댑터를 구현하는 2가지 기법
- 합성을 사용하는 기법 (Composite, 위의 예제이다)
- 상속을 사용하는 기법
상속을 사용하는 대부분의 경우 상속이 바람직하지 않은 해결책일 수 있음을 염두에 두어야 한다.
// Adaptee 는 이미 개발이 완료됐고, 변경하기 곤란한 상황이다.
class Adaptee {
Response specificRequest() {
return new Response();
}
}
// 클라이언트가 사용하려는 인터페이스.
interface Target {
Response request();
}
// Adapter는 Adaptee을 상속받고, Target 인터페이스를 구현한다.
class Adapter extends Adaptee, implements Target {
@Override
public Response request() {
// 전처리
return super.specificRequest()
//후처리
}
}
상속받은 Adaptee의 기능을 Override 해도 되지만, 전 후 처리를 하고 super클래스에 메소드 호출을 위임해도 된다.
상속보다는 Composite(합성)를 사용하여 객체를 래핑하는 어댑터 패턴 구현 방법이 조금 더 안전하다고 볼 수 있다.
상속이 가진 단점을 어댑터도 가질 수 있기 때문이다.
언제 사용?
- Adpater 클래스는 기존 클래스(Adaptee를 사용하고 싶지만 그 인터페이스가 나머지 코드와 호환되지 않을 때 사용한다..
- 어댑터 패턴은 나의 코드와
레거시 클래스
,타사 클래스
또는특이한 인터페이스가 있는 다른 클래스
간의 변환기 역할을 하는 중간 레이어 클래스를 만들 수 있게 도와준다. - 이 패턴은 부모 클래스에 추가할 수 없는 어떤 공통 기능들이 없는 여러 기존 자식 클래스들을 재사용하려는 경우에 사용하면 된다.
- 각 자식 클래스를 확장한 후 누락된 기능들을 새 자식 클래스들에 넣을 수 있다. 하지만 해당 코드를 모든 새 클래스들에 복제해야 하며, 그건 정말 많은 중복코드를 야기하는 것이다.
- 이보다 훨씬 더 깔끔한 해결책은 누락된 기능을 어댑터 클래스에 넣는 것.
- 그 후 어댑터 내부에 누락된 기능이 있는 객체들을 래핑하면 필요한 기능들을 동적으로 얻을 수 있다.
- 이 해결책이 작동하려면 대상 클래스들에는 반드시 공통 인터페이스가 있어야 하며 어댑터의 필드는 해당 인터페이스를 따라야 한다.
위 접근 방식은 데코레이터 패턴과 매우 유사하다.
- 두 클래스가 동일하거나 유사한 작업을 수행하지만 인터페이스가 서로 다른 경우.
- 외부 라이브러리라서 인터페이스를 바꾸고 싶어도 쉽게 바꿀 수 없는 경우, 또는 인터페이스가 프레임워크의 일부라서 이미 많은 클라이언트에서 사용되고 있는 경우, 또는 소스 코드를 갖고 있지 않는 경우.
실무에서 어떻게 쓰이나?
자바
- java.util.Arrays#asList(T…)
- java.util.Collections#list(Enumeration), java.util.Collections#enumeration()
- java.io.InputStreamReader(InputStream)
- java.io.OutputStreamWriter(OutputStream)
스프링
- HandlerAdpter: 우리가 작성하는 다양한 형태의 핸들러 코드를 스프링 MVC가 실행할 수 있는 형태로 변환해주는 어댑터용 인터페이스.
- 스프링 시큐리티의 UserDetailsService에서 사용하기 위한, UserDetails를 구현한 엔티티 클래스.
public class Member implements UserDetails {
...
}
- 단일책임원칙을 지키기 위해서는 Member 엔티티가 직접 구현하는 것보다 다른 클래스를 만들어서 컴포지트로 이용하는 것이 낫다.
public class MemberDetails implements UserDetails {
private Member member;
...
}
장 / 단점
장점
- 기존 코드를 변경하지 않고 원하는 인터페이스 구현체를 만들어 재사용할 수 있다. (개방 폐쇄 원칙, OCP)
- 프로그램의 기본 비즈니스 로직에서 인터페이스 또는 데이터 변환 코드를 분리할 수 있다. (단일 책임 원칙. SRP)
- 클라이언트 코드가 인터페이스를 통해 어댑터와 작동하는 한, 기존 코드를 변경하지 않고 어댑터들을 원하는 인터페이스 구현체를 만들어서 재사용할 수 있다. (개방 폐쇄 원칙. OCP)
- 기존 코드가 하던 일과 특정 인터페이스 구현체로 변환하는 작업을 각기 다른 클래스로 분리하여 관리할 수 있다.
단점
- 수많은 새로운 인터페이스와 클래스들을 도입해야 하므로 코드의 전반적인 복잡도가 증가할 수 있다.
- 경우에 따라서는 기존 코드가 해당 인터페이스를 구현하도록 수정하는 것이 좋은 선택이 될 수도 있다.
결론
어댑터 패턴은 기존 클래스의 소스코드를 수정해서 인터페이스에 맞추는 작업보다는 기존 클래스의
소스코드의 수정을 전혀 하지 않고 타겟 인터페이스에 맞춰서 동작을 가능하게 한다.
즉, 기존 클래스의 명세(사양)만 알면 얼마든지 새로운 클래스도 작성할 수 있으므로 소스코드가 간단해지고 유지보수도 원활하게 되는 이점도 있다.
다른 패턴과의 관계
- 브리지는 일반적으로 사전에 설계되며, 앱의 다양한 부분을
독립적으로
개발할 수 있도록 한다.- 반면에 어댑터는 일반적으로 기존 앱과 사용되어 원래 호환되지 않던 일부 클래스들이 서로 잘 작동하도록 한다.
- 어댑터는 기존 객체의 인터페이스를 변경하는 반면, 데코레이터는 객체를 해당 객체의 인터페이스를 변경하지 않고 향상시킨다.
- 어댑터 패턴은 재귀적 합성이 불가능하다. -> 어댑터는 하나의 객체만 래핑하기 때문
- 데코레이터는 어댑터를 사용할 때는 불가능한
재귀적 합성
을 지원한다. - 재귀적 합성이란 연속적인 중첩구조를 의미한다.
- 어댑터는 다른 인터페이스를, 프록시는 같은 인터페이스를, 데코레이터는 향상된 인터페이스를
래핑된 객체(내부 객체)
에 제공한다. - 퍼사드는 기존 객체들을 위한 새 인터페이스를 정의하는 반면, 어댑터는 기존의 인터페이스를 사용할 수 있게 만들려고 이용한다. 또 어댑터는 일반적으로 하나의 객체만 래핑하는 반면 퍼사드는 많은 객체의 하위시스템과 함께 작동한다.
- 브리지, 상태, 전략 패턴은 매우 유사한 구조로 되어 있으며, 어댑터 패턴도 이들과 어느 정도 유사한 구조로 되어 있다.
- 위 모든 패턴은 다른 객체에 작업을 위임하는
coposite(합성)
을 기반으로 한다. - 하지만 이 패턴들은
모두 다른 문제
들을 해결하려고 한다.
- 위 모든 패턴은 다른 객체에 작업을 위임하는
참조
'디자인 패턴' 카테고리의 다른 글
브릿지 패턴(Bridge Pattern) (0) | 2022.12.04 |
---|---|
데코레이터 패턴(Decorator Pattern) (1) | 2022.11.11 |