반응형
의도
- 클래스의 인터페이스를 사용자가 기대하는 인터페이스 형태로 적응(변환)시킨다.
- 서로 일치하지 않는 인터페이스를 갖는 클래스들을 함께 동작시킨다.
활용성
- 기존 클래스를 사용하고 싶은데 인터페이스가 맞지 않을 때
- 아직 예측하지 못한 클래스나 실제 관련되지 않는 클래스들이 기존 클래스를 재사용하고자 하지만, 이미 정의된 재사용 가능한 클래스가 지금 요청하는 인터페이스를 정의하고 있지 않을 때, 즉, 이미 만든 것을 재사용하고자 하나 이 재사용 가능한 라이브러리를 수정할 수 없을 때
- 객체 적응자에 해당하는 사항
- 이미 존재하는 여러 개의 서브클래스를 사용해야 하는데, 서브클래스들의 상속을 통해서 이들의 인터페이스를 다 개조한다는 것이 현실성이 없을 때
- 객체 적응자를 써서 부모 클래스의 인터페이스를 변경하는 것이 더 바람직하다.
결과
- 클래스 적응자
- Adapter 클래스는 Adaptee 클래스를 Target 클래스로 변형하는데, 이를 위해서 Adaptee 클래스를 상속받아야 하기 때문에, 하나의 클래스와 이 클래스의 모든 서브클래스들을 개조할 때라면 클래스 적응자 방식을 사용할 수 없다.
- Adapter는 명시적으로 Adaptee를 상속받고 있을 뿐, Adaptee의 서브클래스들을 상속받는 것은 아니다.
- Adaptee의 서브클래스에 정의된 기능들을 사용할 수 없다.
- Adapter는 명시적으로 Adaptee를 상속받고 있을 뿐, Adaptee의 서브클래스들을 상속받는 것은 아니다.
- Adapter 클래스는 Adaptee 클래스를 상속하기 때문에 Adapter에 정의된 행동을 재정의 할 수도 있다.
- 한 개의 객체(Adapter)만 사용하며, Adaptee로 가기 위한 추가적인 포인터 간접화는 필요하지 않다.
- Adapter 클래스는 Adaptee 클래스를 Target 클래스로 변형하는데, 이를 위해서 Adaptee 클래스를 상속받아야 하기 때문에, 하나의 클래스와 이 클래스의 모든 서브클래스들을 개조할 때라면 클래스 적응자 방식을 사용할 수 없다.
- 객체 적응자
- Adapter 클래스는 하나만 존재해도 수 많은 Adaptee 클래스들과 동작할 수 있다. 그 이유는 Adapter 객체가 포함하는 Adaptee에 대한 참조자는 Adaptee의 인스턴스를 관리할 수도 있고, Adaptee 클래스를 상속받는 다른 서브클래스들의 인스턴스 또한 관리할 수 있기 때문이다.
- 하나의 Adapter 클래스로 모든 Adaptee 클래스와 이를 상속받는 서브클래스 모두를 이용할 수 있다.
- Adaptee 클래스의 행동을 재정의 하기가 매우 어렵다.
- 이를 위해서는 Adaptee 클래스를 상속받아서 새로운 서브클래스를 만들고, Adapter 클래스는 Adaptee 클래스가 아닌 Adaptee 클래스의 해당 서브클래스를 참조하도록 해야한다.
협력 방법
- 사용자는 적응자에 해당하는 클래스의 인스턴스에게 연산을 호출하고, 적응자는 해당 요청을 수행하기 위해 적응 대상자의 연산을 호출한다.
구현 고려사항
- 클래스 적응자 구현방안
- Adapter 클래스는 Target 클래스에서 public으로 상속받고, Adaptee는 private로 상속받아야 한다.
- Target에 정의된 인터페이스는 Adapter에서도 public으로 공개되지만, Adaptee는 내부구현에 필요한 것이므로, Adaptee가 사용자에 알려질 필요가 없다.
- 이렇게 되면 Adapter는 Target의 서브클래스이기는 하지만, Adaptee의 서브클래스는 아니다.
- Adapter 클래스는 Target 클래스에서 public으로 상속받고, Adaptee는 private로 상속받아야 한다.
- 대체 가능(pluggable) 적응자
- 인터페이스의 변경이 필요하면 해당 내용을 담은 클래스를 만들어 해결할 수 있다.
- 즉, 적응자 클래스를 만든다.
- 내가 개발한 클래스를 사용할 모든 사용자에게 동일한 인터페이스를 제공하여야한다는 가정을 배제할 수 있다.
- 인터페이스 개조를 통해 내가 만들 클래스는 기존에 존재하는 시스템과 함께 연동할 수 있게 된다.
- 이런 인터페이스 개조를 담당하는 클래스를 대체 가능 적응자라고 한다.
- 적응자를 구현할 수 있는 방법
- Adaptee에 정의된 인터페이스들 중 적응이 필요한 연산의 최소 집합을 만든다.
- 수십 개 연산을 갖는 인터페이스를 적응시키기보다는 한 두개 인터페이스만을 적응시키는 것이 더 쉽기 때문이다.
- 구현방법 세 가지
- 추상 연산을 사용하는 방법
- 위임 객체를 사용하는 방법
- 매개변수화된 적응자를 사용하는 방법
- Adaptee에 정의된 인터페이스들 중 적응이 필요한 연산의 최소 집합을 만든다.
- 인터페이스의 변경이 필요하면 해당 내용을 담은 클래스를 만들어 해결할 수 있다.
- 양방향 적응자를 통한 투명성 제공
- 적응자의 잠재적인 문제는 적응자가 모든 사용자에게 투명하지 않다는 것이다.
- 적응된 객체는 Target 인터페이스를 만족하고 Adaptee 인터페이스는 만족하지 않는다. 이렇게 되면 Target이 필요한 사용자에게는 적응된 클래스를 사용할 수 있겠지만 Adaptee 객체를 통해 Target을 사용해야하는 사용자라면 적응된 객체를 사용할 수 없다.
- 양방향 적응이 되려면 이러한 상황 모두 지원해야 한다.
- 이는 서로 다른 두 개의 사용자가 객체를 서로 다르게 바라봐야 할 때 필요한 기능이다.
- 양방향 적응자는 개조되는 두 클래스의 인터페이스를 모두 상속받아 정의하도록 하는 것이다
- 다중 상속을 이용하는 것이 좋은 방법이다.
- 적응자의 잠재적인 문제는 적응자가 모든 사용자에게 투명하지 않다는 것이다.
구조
- 클래스 적응자
- 객체 적응자
실제 구현 구조
- 클래스 적응자
- 객체 적응자
소스코드
- 클래스 적응자
//Client
public class DrawingEditor {
private Shape shape;
public Shape getShape() {
return shape;
}
public void setShape(Shape shape) {
this.shape = shape;
}
public void boundingBox() {
shape.boundingBox();
}
public Manipulator createManipulator() {
return shape.createManipulator();
}
}
//Target
public interface Shape {
void boundingBox();
Manipulator createManipulator();
}
//Adaptee
//기존에 존재하던 텍스트 관련 그리기 기능(적응대상자)
public class TextView {
protected void getExtent() {
System.out.println("TextView.getExtent");
}
}
//Adapter
//Shape 인터페이스에 호환되지 않는 기존에 존재하던 텍스트 관련 그리기 기능을 이용하는 클래스
//즉, 호환되지 않는 기능을 Adapter를 통하여 호환성 있도록 변경
public class TextShape extends TextView implements Shape {
@Override
public void boundingBox() {
getExtent();
}
@Override
protected void getExtent() {//Adaptee 메서드 재정의가 쉽다.
super.getExtent();
}
@Override
public Manipulator createManipulator() {
//애초에 Shape의 Manipulator와 조작방식이 다르기 때문에 해당 텍스트를 관리하는 조작자 동작을 별도로 정의 해야한다.
return new TextManipulator();
}
}
public class Main {
public static void main(String[] args) {
DrawingEditor drawingEditor = new DrawingEditor();
//기존 인터페이스에 맞게 구현된 기능
Shape lineShape = new LineShape();
drawingEditor.setShape(lineShape);
lineShape.boundingBox();
//기존과 호환되지않는 TextView 기능을 Adapter를 적용하여 호환되도록 만든다
Shape textShape = new TextShape();
drawingEditor.setShape(textShape);
drawingEditor.boundingBox();
}
}
- 결과
- 객체 적응자
//Client
public class DrawingEditor {
private Shape shape;
public Shape getShape() {
return shape;
}
public void setShape(Shape shape) {
this.shape = shape;
}
public void boundingBox() {
shape.boundingBox();
}
public Manipulator createManipulator() {
return shape.createManipulator();
}
}
//Target
public interface Shape {
void boundingBox();
Manipulator createManipulator();
}
//Adaptee
//기존에 존재하던 텍스트 관련 그리기 기능(적응대상자)
public class TextView {
public void getExtent() {
System.out.println("TextView.getExtent");
}
}
//Adapter
//Shape 인터페이스에 호환되지 않는 기존에 존재하던 텍스트 관련 그리기 기능을 이용하는 클래스
//즉, 호환되지 않는 기능을 Adapter를 통하여 호환성 있도록 변경
public class TextShape implements Shape {
private TextView textView;
public TextShape() {
this.textView = new TextView();
}
@Override
public void boundingBox() {
textView.getExtent();
}
@Override
public Manipulator createManipulator() {
//애초에 Shape의 Manipulator와 조작방식이 다르기 떄문에 해당 텍스트를 관리하는 조작자 동작을 별도로 정의 해야한다.
return new TextManipulator();
}
}
public class Main {
public static void main(String[] args) {
DrawingEditor drawingEditor = new DrawingEditor();
//기존 인터페이스에 맞게 구현된 기능
LineShape lineShape = new LineShape();
drawingEditor.setShape(lineShape);
lineShape.boundingBox();
//기존과 호환되지않는 TextView 기능을 Adapter를 적용하여 호환되도록 만든다
Shape textShape = new TextShape();
drawingEditor.setShape(textShape);
drawingEditor.boundingBox();
}
}
- 결과
관련 패턴
- 가교 패턴은 객체 적응자와 클래스 구조가 유사하나 사용 목적이 다르다.
- 가교 패턴은 구현과 이 구현이 만족할 추상 개념을 분리하여 서로에게 영향을 주지 않고 각각 확장할 수 있도록 하려는 것
- 적응자 패턴은 존재하는 객체의 인터페이스를 변경하려는 것이다.
- 장식자 패턴은 다른 인터페이스의 변경 없이도 객체에 새로운 행동을 추가할 수 있도록 한다.
- 적응자 보다 응용프로그램을 위해 좋은 방법이며 순수한 적응자로는 불가능한 재귀적 합성을 가능하게 한다.
- 프록시 패턴은 다른 객체에 대한 대표자 또는 대리인의 역할을 수행하지만 인터페이스를 변경하는 책임은 없다.
[참고자료]
리처드 헬름, 랄프 존슨, 존 블리시디스, 『GoF의 디자인 패턴 : 재사용성을 지닌 객체지향 소프트웨어의 핵심요소』, 김정아 번역, 프로텍미디어(2015)
http://www.cs.unc.edu/~stotts/GOF/hires/pat4afso.htm
https://yaboong.github.io/design-pattern/2018/10/15/adapter-pattern/
반응형