책정리/GoF 디자인 패턴

9장 기존 모듈 재사용을 위한 인터페이스 변경 문제(Adapter 패턴)

GONII 2019. 1. 26. 14:34
문제 사례 설명
그래픽 편집기를 만들고 있다고 가정해보자.
모든 형태의 그림을 대표하기 위해 Shape 클래스를 추상 클래스로 정의했다. 그리고 하위 클래스로 그림 형태에 따라 Line, Rectangle, Circle 등의 클래스들을 정의했다고 하자. 그런데 선이나 다각형이 아닌 텍스트를 그리거나 편집할 있는 기능을 추가로 구현해달라는 요청을 받았다. 이런 요청을 수용하기 위해 텍스트를 그리거나 편집하는 기능 구현을 검토해보았더니 여러 가지 면에서 훨씬 구현이 까다로워서 주어진 개발 기간 내에는 개발을 완료하기가 힘들다는 결론이 났다. 그런데 마침 원하는 기능이 구현된 TextView 클래스 라이브러라 존재한다는 사실을 발견
그러나 막상 TextView 클래스를 사용하려고 보니, 모든 그림을 대표하는 Shape 클래스와는 인터페이스가 달라 Shape 클래스의 하위 클래스로 TextView 클래스를 정의하는 것은 다형성(Polymorphism) 적용에 문제가 있는 것으로 판명되었다. 더구나 TextView 클래스는 바이너리 코드 형태의 라이브러리로만 존재하기 때문에 소스코드의 수정도 불가능하다.
이와 같은 경우 어떻게 하면 TextView 클래스를 활용하면서도 Shape 클래스에서 원하는 인터페이스를 만족시켜줄 있을까?
다양한 접근 방법 및 ADAPTER 패턴


기본적인 방법: CLIENT에 의한 구분 사용
원하는 기능 구현만을 염두에 경우 먼저 생각해볼 있는 방법은 별도의 TextShape 클래스를 정의하지 않고 Client 일반적인 그림을 그리고 편집하려고 때와 텍스트를 그리고 편집하려고 때를 구분해서 객체를 생성하고 상요하게 하는 것이다. , 선이나 사각형 등을 그리려고 경우에는 Shape 하위 클래스인 LineShape RectangleShape 사용하고, 텍스트를 그리고 편집하려고 때에는 TextView 클래스를 사용하게 만드는 것이다.
그러나 문제가 되는 것은 Client에서 매번 작업을 수행하기에 앞서 작업 수행의 대상이 되는 객체를 구분해주어야 한다는 점이다. 이는 Client 소스코드의 구현을 지저분하고 귀찮게 만들 것이다. 더구나 이와 같이 사용하는 클래스를 구분하게 되면 Client 입장에서는 클래스간의 다형성(Polymorphism) 활용할 없게 되는 단점이 존재한다.
예를 들어, 그래픽 편집기에서 개별 객체가 선택되었을 경우 선택된 객체를 표시하기 위해 해당 객체가 차지하는 공간의 범위를 사각형으로 나타내고 싶다고 하자. 이를 위해서는 먼저 객체가 차지하는 공간을 사각형으로 되돌려받을 있는 방법이 필요할 것이다. [그림 9-1] 클래스 내부 인터페이스를 살펴보면 Shape 하위 클래스들은 BoundingBox() 라는 인터페이스를 통해 자신이 차지하는 공간을 Rectangle 객체로 되돌리고 있다. 반면, TextView 클래스는 GetExtent() 인터페이스를 통해 자신이 차지하는 공간을 Rectangle 객체로 되돌리고 있다. 따라서 어떤 객체에 대해서든 객체가 차지하는 공간을 사각형으로 표시하는 것은 Client 입장에서 가능하다.
그러나 실제 이를 구현한 형태를 살펴보면 [소스 9-1] DisplayBoundingBox() 같이 소스 코드가 지저분해짐을 있다. 왜냐하면 TextView Shape 서로 다른 자료형이기 때문에 함수 인자부터 개가 전달되어야 하며, 인자로 전달된 객체에 대해 사각형을 표시하는 내부 구현도 TextView 객체일 때와 일반 Shape 객체일 호출해주는 인터페이스가 다르므로 구분이 필요하기 때문이다.
[소스 9-1] Shape TextView 클래스를 각각 구분해서 사용할 경우
class Rectangle
{
public :
Rectangle(int x1, int y1, int x2, int y2)
{
x1_ = x1;
y1_ = y1;
x2_ = x2;
y2_ = y2;
}
void Draw() {}
private:
int x1_, y1_, x2_, y2_;
};
 
class Shape
{
public:
virtual Rectangle BoundingBox() = 0;
};
 
class LineShape : public Shape
{
public:
Rectangle BoundingBox()
{
return Rectangle(x1_, y1_, x2_, y2_);
}
private:
int x1_, y1_, x2_, y2_;
};
 
class TextView
{
public:
Rectangle GetExtent()
{
return Rectangle(x1_, y1_, x1_ + width_, y1_ + height_);
}
private:
int x1_, y1_;
int width_, height_;
};
 
void DisplayBoundingBox(Shape* pSelectedShape, TextView* pSelectedText)
{
if (pSelectedText != 0)
{
(pSelectedText->GetExtent()).Draw();
}
else if (pSelectedShape != 0)
{
(pSelectedShape->BoundingBox()).Draw();
}
else
{ }
}
 
void main()
{
TextView text;
DisplayBoundingBox(0, &text);
}
이처럼 원하는 기능만을 구현해주면 된다는 입장에서 Client 필요할 경우 TextView 객체와 Shape 객체를 구분해서 사용하게 하는 방식은 많은 단점을 가진다.
패턴 활용 방법: ADAPTER 패턴
문제의 원인은 Client 사용하는 객체의 자료형이 다르다는데 있다. 따라서 이를 해결하기 위한 유일한 방법은 Client 사용하는 객체의 자료형을 통일시켜주는 것이다. , 그래픽 편집기에서 다루는 모든 객체는 Shape 클래스의 자료형이 되게 하는 것이다. 이를 위해 텍스트를 다루기 위한 클래스도 TextShape 같은 이름으로 Shape 클래스의 하위 클래스로 정의하는 것이 바람직하다. 이렇게 정의한 TextShape 클래스는 다형성이 적용될 있게 Shape클래스와 동일한 인터페이스를 가져야 것이다.
가지 형태의 구현 방식을 생각해볼 있다.
먼저 생각해볼 방법은 TextShape 클래스의 인터페이스가 호출되면 내부에서 다시 TextView 클래스의 인터페이스를 호출해서 원하는 기능을 구현해주는 것이다.


 
번째로 생각해 있는 방식은 TextShape 클래스를 정의하되 Shape 클래스와 TextView 클래스로부터 동시에 상속을 받아 정의하는 것이다. 이때 TextView 클래스로부터 상속받은 인터페이스는 외부에 숨겨질 있게 private 형태로 상속을 받아야 것이다. 방법이 첫번째 방법과 다른 점은 TextShape 클래스 내부에 별도로 TextView 객체를 참조하기 위한 자료구조를 두지 않아도 된다는 점이다. 다만, 방식은 다중 상속이 가능한 경우에만 적용할 있는 방법이다.


 
이처럼 가지 형태의 접근 방식을 모두 Adapter 패턴이라고 하는데, 첫번째 방식을 객체를 참조한다고 하여 Object Adapter 패턴이라고 하고, 두번째 방식을 다중 상속을 통해 클래스를 참조한다고 하여 Class Adapter 패턴이라고 한다. 그리고 TextShape 같은 클래스를 Adapter 클래스라고 한다.
샘플 코드
[소스 9-2] Object Adapter패턴에 의한 TextShape 클래스의 구현
#include <iostream>
using namespace std;
 
class Rectangle
{
public:
Rectangle(int x1, int y1, int x2, int y2)
{
x1_ = x1;
y1_ = y1;
x2_ = x2;
y2_ = y2;
}
void Draw() {}
private:
int x1_, y1_, x2_, y2_;
};
 
class TextView
{
public:
Rectangle GetExtent()
{
return Rectangle(x1_, y1_, x1_ + width_, y1_ + height_);
}
private:
int x1_, y1_;
int width_, height_;
};
 
class Shape
{
public:
virtual Rectangle BoundingBox() = 0;
};
 
class LineShape : public Shape
{
public:
Rectangle BoundingBox()
{
return Rectangle(x1_, y1_, x2_, y2_);
}
private:
int x1_, y1_, x2_, y2_;
};
 
class TextShape : public Shape
{
public:
TextShape()
{
pText_ = new TextView;
}
Rectangle BoundingBox()
{
return pText_->GetExtent();
}
private:
TextView* pText_;
};
 
void DisplayBoundingBox(Shape* pSelectedShape)
{
(pSelectedShape->BoundingBox()).Draw();
}
 
void main()
{
TextShape text;
DisplayBoundingBox(&text);
}
[소스 9-3] Class Adapter패턴에 의한 TextShape 클래스의 구현
#include <iostream>
using namespace std;
 
class Rectangle
{
public:
Rectangle(int x1, int y1, int x2, int y2)
{
x1_ = x1; y1_ = y1; x2_ = x2; y2_ = y2;
}
void Draw(){}
private:
int x1_, y1_, x2_, y2;
};
 
class TextView
{
public:
Rectangle GetExtent()
{
return Rectangle(x1_, y1_, x1 + width_, y1_ + height_);
}
private:
int x1_, y1_;
int width_, height_;
};
 
class Shape
{
public:
virtual Rectangle BoundingBox() = 0;
};
 
class LineShape : public Shape
{
public:
Rectangle BoudngingBox()
{
return Rectangle(x1_, y1_, x2_, y2_);
}
private:
int x1_, y1_, x2_, y2_;
};
 
class TextShape : public Shape, private TextView
{
public:
Rectangle BoundingBox()
{
return GetExtent();
}
};
 
void DisplayBoundingBox(Shape* pSelectedShape)
{
(pSelectedShape->BoundingBox()).Draw();
}
 
void main()
{
TextShape text;
DisplayBoundingBox(&text);
}
[소스 9-2] [소스 9-3]에서 보듯이 Object Adapter패턴와 Class Adapter 패턴의 차이점은 TextShape 클래스 구현 시에 객체 참조를 사용하느냐, 다중 상속을 하느냐이다.
한편 어떤 방식을 사용하든지간에 이들을 이용하는 입장에서는 프로그램이 훨씬 간편해진 것을 있다. , DisplayBoundingBox() 함수를 살펴보면 우선 함수 인자가 하나로 통일되었음을 있으며, 함수 내부의 구현 또한 간단하게 정리된 것을 있다. 이처럼 Adapter 패턴을 적용하면 기존 클래스를 활용해서 원하는 기능을 쉽고 빠르게 구현할 있는 장점이 있다.
구현 관련 사항
Class Adapter 패턴의 구현은 다중 상속으로 이루어지는데, 이때 Client에게 공개될 인터페이스를 가진 클래스는 public으로 상속하고, 내부 구현을 위해 사용할 클래스를 private 형태로 상속하는 것이 일반적인 방법이다. [소스 9-3]에서 TextShape 클래스가 Shape클래스는 public으로 상속하고, TextView 클래스는 private 형태로 상속한 것이 좋은 예일 것이다.
그러나 같은 방식으로 상속을 경우 상속받는 클래스는 중에서 public 형태로 상속받은 클래스의 자료형으로만 사용될 있지만, TextView 클래스 자료형을 필요로 하는 곳에는 사용 없다. 이는 바로 private상속의 특성 때문이다.
Adapter 패턴의 구현에 대해 두번째로 고려해볼만한 사항은 Pluggable Adapter 대한 것이다. 여기서 Pluggable Adapter 서로 다른 형태의 인터페이스를 요청하는 기존 시스템에 우리가 개발한 클래스를 꽂아넣기 위해 Adapter 패턴 형태로 정의한 클래스를 말한다. 이는 지금까지 언급한 Adapter 클래스가 기존 클래스를 재사용하되 Client 원하는 형태의 인터페이스로 변경시켜주는 추점을 맞춘 것과는 달리 기존 클래스에게 새로운 기능을 추가하기 위한 클래스 정의 방법이라는 점에서 개념상 차이가 있다고 있다. , Pluggable Adapter 이미 존재하는 클래스를 수정하지 않고 클래스의 기능을 확장시킬 있는 클래스를 추가로 정의해주는 것을 말한다.
Adapter패턴의 구현시 고려해야 세번째 사항으로는 Objjject Adapter 패턴 구현 내부적으로 구현을 위해 참조하는 객체는 언제 생성할 것인가이다. 예를 들어 [소스 9-2] TextShape 클래스에서 pText_ 포인터 데이터 멤버에 객체를 언제 생성해줄 것인가라는 것이다.
간단히 [소스 9-2]에서처럼 객체의 생성자에서 데이터 멤버에 객체를 생성할 있다. 방식은 TextShape객체가 생성되는 동시에 TextView 객체도 생성되어 참조되도록 하는 방식으로 TextShape 객체와 TextView 객체의 생명 주기를 동일하게 관리할 있는 장점이 있다. 반면 방식은 불필요한 시점에서도 TextView 객체를 유지, 관리해야 한다는 단점이 있다.
이러한 단점을 회피하기 위해 TextShape 클래스에 TextView 객체를 생성하고 소멸할 있는 인터페이스를 별도로 정의해줄 수도 있다. 그러나 경우 반대로 TextShape 객체에 대해 BoundingBox() 멤버 함수를 호출하기 전에 반드시 TextView 대한 객체가 생성되어 있는지를 확인해야 하는 불편이 있다.
밖에 Adapter 패턴의 구현 감안해야 사항은 Adapter 클래스의 구현이 단순히 Client 원하는 인터페이스 형태로 이름만 바꾸어주는 형태로부터 Client 원하는 인터페이스의 내용을 완전히 새롭게 구현해야 하는 경우까지 다양할 있다는 사실이다. Adapter 패턴에서 구현의 복잡도는 사용하려는 기존 클래스의 인터페이스가 얼마나 Client 원하는 인터페이스와 닮았는지 여부에 달려 있다.
ADAPTER 패턴 정리



 Adapter패턴의 활용
  • 기존의 클래스를 재사용하려고 하나, 인터페이스가 원하는 것과 동일하지 않을 이를 변경시켜주기에 편리하다.
  • 서로 관계가 적고, 호환되는 인터페이스가 별로 없는 클래스들을 활용해서 새로운 클래스를 생성하려고 유용하다.
  • Object Adapter 어떤 클래스에 하위 클래스 여러 개가 존재해서 모든 하위 클래스들을 상속하는 것이 비현실적인 경우에 Class Adapter보다 훨씬 유용하다. 왜냐하면 Object Adapter 경우에는 모든 하위 클래스를 상속할 필요없이 최상위 클래스에 대한 객체 참조 데이터 멤버만 정의하면 어떤 하위 클래스의 객체에 대해서도 동작이 가능하기 때문이다. 반면 Class Adapter 반드시 원하는 하위 클래스를 상속해야 한다.
Class Adapter 패턴의 장단점
  • Class Adapter 패턴에서는 Adapter 클래스가 Adaptee 클래스의 하위 클래스기 때문에 Adapter 클래스에서 필요할 경우 곧바로 Adaptee 클래스의 멤버 함수들을 Override 있다.
  • Class Adapter 패턴의 경우에는 하나의 객체만 생성하면 되고, Adaptee 클래스에 해당하는 별도의 객체를 생성해서 포인터 변수 등에 저장하는 것이 불필요하다.
  • 단점은 Adapter 클래스에 의해 접목되어지는 Adaptee 클래스가 고정적이기 때문에 Adaptee 클래스의 하위 클래스가 있는 경우, 이들에 대해서는 동작하지 않는 것이다.
Object Adapter 패턴의 장단점
  • Object Adapter 패턴의 가장 장점은 Adaptee 하위 클래스에 대해서도 소스코드 변경 없이 동작이 가능하다는 사실이다. 또한 Adaptee 클래스에 새로운 기능을 추가해주기만 하면 모든 하위 클래스에서도 동일한 기능이 수행되고, 이를 Adapter 클래스에서 활용할 있기 때문에 새로운 기능의 추가가 편리하다.
  • 단점은 Class Adapter 패턴과는 달리 Adaptee 클래스의 멤버 함수를 Override 하려면 Adaptee 클래스를 상속한 하위클래스를 정의하고, 이를 다시 사용하는 형태가 되어야 한다.

반응형