본문 바로가기

study/design pattern

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

728x90

[모던 C++ 디자인 패턴] 책을 바탕으로 공부하는 내용을 정리한 내용이다.


Adapter pattern

어댑터 패턴은 일상생활에서 많이 보이는 어댑터와 같은 용도로 사용된다. 어떤 인터페이스가 용도에 맞지 않을 때 변환하는 역할로 사용하는 패턴이다.


시나리오

픽셀을 그리는 그리기 라이브러리가 있고, 이 라이브러리를 이용해야만 그림을 그릴 수 있다. 그런데 선분, 사각형과 같은 기하학적 모양을 그려야 하는 상황이다. 픽셀을 그리는 라이브러리는 요구사항에 비해 너무 저수준의 작업이기 때문에 기하학적 도형을 픽셀 기반의 표현이 가능하도록 바꿔주는 어댑터가 필요하다.

 

기본적인 그리기 객체를 정의한다.

struct Point {
	int x, y;
};

struct Line {
	Point start, end;
};

 

일반적인 기하학적 도형을 담을 수 있도록 정의한다.

struct VectorObject {
	virtual std::vector<Line>::iterator begin() = 0;
	virtual std::vector<Line>::iterator end() = 0;
};

 

기하학적 도형을 담을 수 있는 구조체를 이용해 아래와 같이 사각형 구조체를 만들 수 있다. 시작점과 크기를 입력받아 사각형을 구성하는 선분을 멤버 변수에 저장해 꼭짓점만 노출하는 방식이다.

struct VectorRectangle : public VectorObject {
	VectorRectangle(int x, int y, int width, int height) {
		lines.emplace_back(Line{ Point{x, y}, Point{x + width, y} });
		lines.emplace_back(Line{ Point{x + width, y}, Point{x + width, y + height} });
		lines.emplace_back(Line{ Point{x, y}, Point{x, y + height} });
		lines.emplace_back(Line{ Point{x, y + height}, Point{x + width, y + height} });
	}

	std::vector<Line>::iterator begin() override {
		return lines.begin();
	}

	std::vector<Line>::iterator end() override {
		return lines.end();
	}

private:
	std::vector<Line> lines;
};

 

그러나, 그리기 인터페이스가 아래와 같은 함수(MFC의 CPaintDC 클래스 참조) 하나뿐이라면, 아래의 점을 찍는 인터페이스를 이용해 선분을 그려야 한다. 이때 어댑터가 필요하게 된다.

void DrawPoints(CPaintDC &dc, std::vector<Point>::iterator start, std::vector<Point>::iterator end) {
	for ( auto i = start; i != end; ++i ) {
		dc.SetPixel(i->x, i->y, 0);
	}
}

어댑터

사각형을 몇 개 그려야 한다고 가정해보자. 여러 객체를 그리기 위해 사각형을 이루는 선분의 집합에서 각 선분마다 많은 수의 점이 변환돼야 한다. 이를 위해 선분 하나를 점의 집합으로 만들어 저장하고 각 점을 순회할 수 있도록 반복자로 노출하는 클래스를 만든다.

아래의 클래스에서는 선분을 점의 집합으로 변환하는 과정이 생성자에서 이루어진다. 이를 "성급한" 접근법이라 한다.

struct LineToPointAdapter {
	typedef std::vector<Point> Points;

	LineToPointAdapter(Line &line) {
		int left = std::min(line.start.x, line.end.x);
		int right = std::max(line.start.x, line.end.x);
		int top = std::min(line.start.y, line.end.y);
		int bottom = std::max(line.start.y, line.end.y);
		int dx = right - left;
		int dy = line.end.y - line.start.y;

		if ( 0 == dx ) {
			for ( int y = top; y <= bottom; ++y ) {
				points.emplace_back(Point{ left, y });
			}
		}
		else if ( 0 == dy ) {
			for ( int x = left; x <= right; ++x ) {
				points.emplace_back(Point{ x, top });
			}
		}
	}

	virtual Points::iterator begin() {
		return points.begin();
	}

	virtual Points::iterator end() {
		return points.end();
	}

private:
	Points points;
};

 

위의 코드를 통해 수직, 수평 선분을 점의 집합으로 변환시킬 수 있다. 이를 통해 간단한 기하 도형의 표현이 가능하다.

	std::vector<std::shared_ptr<VectorObject> > vectorObjects{
		std::make_shared<VectorRectangle>(10, 10, 100, 100),
		std::make_shared<VectorRectangle>(30, 30, 60, 60)
	};

	for ( auto &obj : vectorObjects ) {
		for ( auto &line : *obj ) {
			LineToPointAdapter lpo{ line };
			DrawPoints(dc, lpo.begin(), lpo.end());
		}
	}

일시적 어댑터

위에서 작성한 방식은 문제가 있다. 화면이 업데이트 될 때마다 바뀐 게 없더라도 어댑터에 의해 선분들이 점으로 변환되고 있다. 이러한 비효율적인 리소스 낭비를 막기 위해 캐싱하는 방법이 있다. 모든 Point를 애플리케이션이 동작할 때 미리 어댑터를 이용해 정의해두고 재활용하는 것이다.

	std::vector<Point> points;
	for ( auto &o : vectorObjects ) {
		for ( auto &l : *o ) {
			LineToPointAdapter lpo{ l };
			for ( auto &p : lpo ) {
				points.push_back(p);
			}
		}
	}

	DrawPoints(dc, points.begin(), points.end());

 

위의 코드는 이제 다시 변환이 필요한 경우에도 재활용 가능하게 변경할 수 있다. 이때는 해싱을 이용한 캐싱을 사용한다. 책에서는 Reshaper와 boost를 이용해 예로 들고 있다.


요약

어댑터는 개념 자체는 단순하다. 사용할 수 밖에 없는 인터페이스를 사용하고 싶은 인터페이스로 감싸는 방식이다. 서로 다른 인터페이스를 붙여 나갈 때 어려움이 있고, 데이터 표현 방식을 맞추기 위해 임시 데이터를 만들어야 할 수도 있다. 이때는 캐싱을 이용해 필요한 임시 데이터가 생성되게 한다.

본문에는 아니지만 중요한 개념으로 "느긋한" 동작 방식도 고려해야 한다. 어댑터 생성 시점이 아닌 실 사용 시점에 변환이 이루어지게끔 하는 방법이다.

728x90